From 4a775d7c646b543dd95a69d10b183dc4d1b90448 Mon Sep 17 00:00:00 2001 From: so1n Date: Sat, 27 Jul 2024 00:22:35 +0800 Subject: [PATCH 01/10] Refactor, comment template, rename name and add custom template param --- README.md | 27 ++++++----- README_ZH.md | 45 ++++++++++--------- example/gen_p2p_code.py | 6 +-- example/gen_text_comment_code.py | 12 ++--- example/p2p_validate_by_comment_gen_code.py | 12 ++--- example/plugin_config.py | 6 +-- protobuf_to_pydantic/gen_model.py | 10 +++-- protobuf_to_pydantic/plugin/config.py | 12 ++--- .../plugin/field_desc_proto_to_code.py | 4 +- .../{desc_template => template}/__init__.py | 14 ++++-- tests/base/base_p2p_validate.py | 16 +++---- .../test_p2p_validate.py | 4 +- 12 files changed, 91 insertions(+), 77 deletions(-) rename protobuf_to_pydantic/{desc_template => template}/__init__.py (82%) diff --git a/README.md b/README.md index 59d5343..8c9d1f8 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ The `protobuf-to-pydantic` plugin supports loading configuration by reading a `P > In order to ensure that the variables of the configuration file can be introduced normally, the configuration file must be stored in the current path of the running command.。 An example configuration that can be read by `protobuf-to-pydantic` is as follows: + ```Python import logging from typing import List, Type @@ -82,31 +83,31 @@ from google.protobuf.any_pb2 import Any # type: ignore from pydantic import confloat, conint from pydantic.fields import FieldInfo -from protobuf_to_pydantic.desc_template import DescTemplate +from protobuf_to_pydantic.template import CommentTemplate # Configure the log output format and log level of the plugin, which is very useful when debugging logging.basicConfig(format="[%(asctime)s %(levelname)s] %(message)s", datefmt="%y-%m-%d %H:%M:%S", level=logging.DEBUG) class CustomerField(FieldInfo): - pass + pass def customer_any() -> Any: - return Any # type: ignore + return Any # type: ignore # For the configuration of the local template, see the use of the local template for details local_dict = { - "CustomerField": CustomerField, - "confloat": confloat, - "conint": conint, - "customer_any": customer_any, + "CustomerField": CustomerField, + "confloat": confloat, + "conint": conint, + "customer_any": customer_any, } # Specifies the start of key comments comment_prefix = "p2p" # Specify the class of the template, can extend the template by inheriting this class, see the chapter on custom templates for details -desc_template: Type[DescTemplate] = DescTemplate +desc_template: Type[CommentTemplate] = CommentTemplate # Specify the protobuf files of which packages to ignore, and the messages of the ignored packages will not be parsed ignore_pkg_list: List[str] = ["validate", "p2p_validate"] # Specifies the generated file name suffix (without .py) @@ -751,11 +752,13 @@ message TimestampTest{ } ``` As you can see, the Protobuf file customizes the syntax of `p2p@timestamp|{x}`, where `x` has only two values, 10 and 13. The next step is to write code based on this template behavior, which looks like this. + ```python import time -from protobuf_to_pydantic.gen_model import DescTemplate +from protobuf_to_pydantic.gen_model import CommentTemplate + -class CustomDescTemplate(DescTemplate): +class CustomDescTemplate(CommentTemplate): def template_timestamp(self, length_str: str) -> int: timestamp: float = time.time() if length_str == "10": @@ -766,12 +769,12 @@ class CustomDescTemplate(DescTemplate): raise KeyError(f"timestamp template not support value:{length_str}") -from .demo_pb2 import TimestampTest # fake code +from .demo_pb2 import TimestampTest # fake code from protobuf_to_pydantic import msg_to_pydantic_model msg_to_pydantic_model( TimestampTest, - desc_template=CustomDescTemplate # <-- Use a custom template class + desc_template=CustomDescTemplate # <-- Use a custom template class ) ``` This code first creates a class `CustomDescTemplate` that inherits from `DescTemplate`. diff --git a/README_ZH.md b/README_ZH.md index 0421f20..20a3f1f 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -69,6 +69,7 @@ protoc -I. --protobuf-to-pydantic_out=. example.proto > 为了保证能够正常的引入配置文件的变量,配置文件必须存放在运行命令的当前路径下。 一个可以被`protobuf-to-pydantic`读取的示例配置内容如下: + ```Python import logging from typing import List, Type @@ -77,31 +78,31 @@ from google.protobuf.any_pb2 import Any # type: ignore from pydantic import confloat, conint from pydantic.fields import FieldInfo -from protobuf_to_pydantic.desc_template import DescTemplate +from protobuf_to_pydantic.template import CommentTemplate # 配置插件的日志输出格式,和日志等级,在DEBUG的时候非常有用 logging.basicConfig(format="[%(asctime)s %(levelname)s] %(message)s", datefmt="%y-%m-%d %H:%M:%S", level=logging.DEBUG) class CustomerField(FieldInfo): - pass + pass def customer_any() -> Any: - return Any # type: ignore + return Any # type: ignore # local模板的配置,详见local模板的使用 local_dict = { - "CustomerField": CustomerField, - "confloat": confloat, - "conint": conint, - "customer_any": customer_any, + "CustomerField": CustomerField, + "confloat": confloat, + "conint": conint, + "customer_any": customer_any, } # 指定关键注释的开头 comment_prefix = "p2p" # 指定模板的类,可以通过继承该类拓展模板,详见自定义模板章节 -desc_template: Type[DescTemplate] = DescTemplate +desc_template: Type[CommentTemplate] = CommentTemplate # 指定要忽略的哪些package的protobuf文件,被忽略的package的message不会被解析 ignore_pkg_list: List[str] = ["validate", "p2p_validate"] # 指定生成的文件名后缀(不包含.py) @@ -739,27 +740,29 @@ message TimestampTest{ } ``` 在这个文件中使用了自定义的`p2p@timestamp|{x}`的语法,其中`x`只有10和13两个值,接下来就可以根据这个模板行为编写代码了,代码如下: + ```python import time -from protobuf_to_pydantic.gen_model import DescTemplate +from protobuf_to_pydantic.gen_model import CommentTemplate + -class CustomDescTemplate(DescTemplate): - def template_timestamp(self, length_str: str) -> int: - timestamp: float = time.time() - if length_str == "10": - return int(timestamp) - elif length_str == "13": - return int(timestamp * 100) - else: - raise KeyError(f"timestamp template not support value:{length_str}") +class CustomDescTemplate(CommentTemplate): + def template_timestamp(self, length_str: str) -> int: + timestamp: float = time.time() + if length_str == "10": + return int(timestamp) + elif length_str == "13": + return int(timestamp * 100) + else: + raise KeyError(f"timestamp template not support value:{length_str}") -from .demo_pb2 import TimestampTest # fake code +from .demo_pb2 import TimestampTest # fake code from protobuf_to_pydantic import msg_to_pydantic_model msg_to_pydantic_model( - TimestampTest, - desc_template=CustomDescTemplate # <-- 使用自定义的模板类 + TimestampTest, + desc_template=CustomDescTemplate # <-- 使用自定义的模板类 ) ``` 这段代码先是创建了一个继承于`DescTemplate`的`CustomDescTemplate`的类, diff --git a/example/gen_p2p_code.py b/example/gen_p2p_code.py index 20ee136..5a8a850 100644 --- a/example/gen_p2p_code.py +++ b/example/gen_p2p_code.py @@ -9,7 +9,7 @@ from pydantic import confloat, conint from pydantic.fields import FieldInfo -from protobuf_to_pydantic import _pydantic_adapter, desc_template, msg_to_pydantic_model, pydantic_model_to_py_file +from protobuf_to_pydantic import _pydantic_adapter, msg_to_pydantic_model, pydantic_model_to_py_file, template # use pydantic v1 method, pydantic will print warning, ignore!~ warnings.filterwarnings("ignore") @@ -42,7 +42,7 @@ def customer_any() -> Any: now_path: pathlib.Path = pathlib.Path(__file__).absolute() -class CustomDescTemplate(desc_template.DescTemplate): +class CustomCommentTemplate(template.CommentTemplate): def template_timestamp(self, length_str: str) -> int: timestamp: float = 1600000000 if length_str == "10": @@ -65,7 +65,7 @@ def gen_code() -> None: "conint": conint, "customer_any": customer_any, }, - desc_template=CustomDescTemplate, + desc_template=CustomCommentTemplate, ) for model in message_class_list ], diff --git a/example/gen_text_comment_code.py b/example/gen_text_comment_code.py index 8757857..039900d 100644 --- a/example/gen_text_comment_code.py +++ b/example/gen_text_comment_code.py @@ -9,10 +9,10 @@ from google.protobuf.message import Message from protobuf_to_pydantic import _pydantic_adapter, msg_to_pydantic_model, pydantic_model_to_py_file -from protobuf_to_pydantic.desc_template import DescTemplate +from protobuf_to_pydantic.template import CommentTemplate -class CustomDescTemplate(DescTemplate): +class CustomCommentTemplate(CommentTemplate): def template_timestamp(self, length_str: str) -> int: timestamp: float = 1600000000 if length_str == "10": @@ -59,7 +59,7 @@ def gen_code() -> None: model, parse_msg_desc_method=module, local_dict=local_dict, - desc_template=CustomDescTemplate, + desc_template=CustomCommentTemplate, ) for model in message_class_list ], @@ -72,7 +72,7 @@ def gen_code() -> None: model, parse_msg_desc_method=str(now_path.parent.parent), local_dict=local_dict, - desc_template=CustomDescTemplate, + desc_template=CustomCommentTemplate, ) for model in message_class_list ], @@ -85,7 +85,7 @@ def gen_code() -> None: model, parse_msg_desc_method=module, local_dict=local_dict, - desc_template=CustomDescTemplate, + desc_template=CustomCommentTemplate, ) for model in message_class_list ], @@ -97,7 +97,7 @@ def gen_code() -> None: model, parse_msg_desc_method=str(now_path.parent.parent), local_dict=local_dict, - desc_template=CustomDescTemplate, + desc_template=CustomCommentTemplate, ) for model in message_class_list ], diff --git a/example/p2p_validate_by_comment_gen_code.py b/example/p2p_validate_by_comment_gen_code.py index 7457dc2..38519e3 100644 --- a/example/p2p_validate_by_comment_gen_code.py +++ b/example/p2p_validate_by_comment_gen_code.py @@ -12,10 +12,10 @@ from pydantic.fields import FieldInfo from protobuf_to_pydantic import _pydantic_adapter, msg_to_pydantic_model, pydantic_model_to_py_file -from protobuf_to_pydantic.desc_template import DescTemplate +from protobuf_to_pydantic.template import CommentTemplate -class CustomDescTemplate(DescTemplate): +class CustomCommentTemplate(CommentTemplate): def template_timestamp(self, length_str: str) -> int: timestamp: float = 1600000000 if length_str == "10": @@ -77,7 +77,7 @@ def gen_code() -> None: model, parse_msg_desc_method=module, local_dict=local_dict, - desc_template=CustomDescTemplate, + desc_template=CustomCommentTemplate, ) for model in message_class_list ], @@ -92,7 +92,7 @@ def gen_code() -> None: model, parse_msg_desc_method=str(now_path.parent.parent), local_dict=local_dict, - desc_template=CustomDescTemplate, + desc_template=CustomCommentTemplate, ) for model in message_class_list ], @@ -105,7 +105,7 @@ def gen_code() -> None: model, parse_msg_desc_method=module, local_dict=local_dict, - desc_template=CustomDescTemplate, + desc_template=CustomCommentTemplate, ) for model in message_class_list ], @@ -119,7 +119,7 @@ def gen_code() -> None: model, parse_msg_desc_method=str(now_path.parent.parent), local_dict=local_dict, - desc_template=CustomDescTemplate, + desc_template=CustomCommentTemplate, ) for model in message_class_list ], diff --git a/example/plugin_config.py b/example/plugin_config.py index 95340e7..ee6f2a8 100644 --- a/example/plugin_config.py +++ b/example/plugin_config.py @@ -7,7 +7,7 @@ from pydantic import confloat, conint from pydantic.fields import FieldInfo -from protobuf_to_pydantic.desc_template import DescTemplate +from protobuf_to_pydantic.template import CommentTemplate logging.basicConfig(format="[%(asctime)s %(levelname)s] %(message)s", datefmt="%y-%m-%d %H:%M:%S", level=logging.INFO) @@ -20,7 +20,7 @@ def customer_any() -> Any: return Any # type: ignore -class CustomDescTemplate(DescTemplate): +class CustomCommentTemplate(CommentTemplate): def template_timestamp(self, length_str: str) -> int: timestamp: float = 1600000000 if length_str == "10": @@ -44,5 +44,5 @@ def exp_time() -> float: "uuid4": uuid4, } comment_prefix = "p2p" -desc_template: Type[DescTemplate] = CustomDescTemplate +desc_template: Type[CommentTemplate] = CustomCommentTemplate ignore_pkg_list: List[str] = ["validate", "p2p_validate"] diff --git a/protobuf_to_pydantic/gen_model.py b/protobuf_to_pydantic/gen_model.py index a3a1871..6aa05db 100644 --- a/protobuf_to_pydantic/gen_model.py +++ b/protobuf_to_pydantic/gen_model.py @@ -13,7 +13,6 @@ from protobuf_to_pydantic import _pydantic_adapter, constant from protobuf_to_pydantic.customer_validator import check_one_of -from protobuf_to_pydantic.desc_template import DescTemplate from protobuf_to_pydantic.exceptions import WaitingToCompleteException from protobuf_to_pydantic.field_param import ( FieldParamModel, @@ -28,6 +27,7 @@ ) from protobuf_to_pydantic.get_desc.from_pb_option.base import field_comment_handler, protobuf_common_type_dict from protobuf_to_pydantic.grpc_types import AnyMessage, Descriptor, FieldDescriptor, Message +from protobuf_to_pydantic.template import CommentTemplate from protobuf_to_pydantic.util import create_pydantic_model if TYPE_CHECKING: @@ -115,7 +115,7 @@ def __init__( pydantic_base: Optional[Type["BaseModel"]] = None, pydantic_module: Optional[str] = None, local_dict: Optional[Dict[str, Any]] = None, - desc_template: Optional[Type[DescTemplate]] = None, + desc_template: Optional[Type[CommentTemplate]] = None, message_type_dict_by_type_name: Optional[Dict[str, Any]] = None, message_default_factory_dict_by_type_name: Optional[Dict[str, Any]] = None, create_model_cache: Optional[CREATE_MODEL_CACHE_T] = None, @@ -158,7 +158,9 @@ def __init__( self._creat_cache: CREATE_MODEL_CACHE_T = create_model_cache or _create_model_cache self._pydantic_base: Type["BaseModel"] = pydantic_base or BaseModel self._pydantic_module: str = pydantic_module or __name__ - self._desc_template: DescTemplate = (desc_template or DescTemplate)(local_dict or {}, self._comment_prefix) + self._desc_template: CommentTemplate = (desc_template or CommentTemplate)( + local_dict or {}, self._comment_prefix + ) self._message_type_dict_by_type_name: Dict[str, Any] = ( message_type_dict_by_type_name or constant.message_name_type_dict ) @@ -618,7 +620,7 @@ def msg_to_pydantic_model( local_dict: Optional[Dict[str, Any]] = None, pydantic_base: Optional[Type["BaseModel"]] = None, pydantic_module: Optional[str] = None, - desc_template: Optional[Type[DescTemplate]] = None, + desc_template: Optional[Type[CommentTemplate]] = None, message_type_dict_by_type_name: Optional[Dict[str, Any]] = None, message_default_factory_dict_by_type_name: Optional[Dict[str, Any]] = None, create_model_cache: Optional[CREATE_MODEL_CACHE_T] = None, diff --git a/protobuf_to_pydantic/plugin/config.py b/protobuf_to_pydantic/plugin/config.py index 86bd369..5ba508f 100644 --- a/protobuf_to_pydantic/plugin/config.py +++ b/protobuf_to_pydantic/plugin/config.py @@ -4,8 +4,8 @@ from pydantic import BaseModel, Field from protobuf_to_pydantic import _pydantic_adapter -from protobuf_to_pydantic.desc_template import DescTemplate from protobuf_to_pydantic.plugin.field_desc_proto_to_code import FileDescriptorProtoToCode +from protobuf_to_pydantic.template import CommentTemplate ConfigT = TypeVar("ConfigT", bound="ConfigModel") @@ -21,8 +21,8 @@ class ProtobufTypeConfigModel(BaseModel): class ConfigModel(BaseModel): local_dict: dict = Field(default_factory=dict, description="Dict for local variables") - desc_template: Type[DescTemplate] = Field( - default=DescTemplate, description="Support more templates by customizing 'Desc Template'" + desc_template: Type[CommentTemplate] = Field( + default=CommentTemplate, description="Support more templates by customizing 'Desc Template'" ) comment_prefix: str = Field(default="p2p", description="Comment prefix") parse_comment: bool = Field( @@ -79,8 +79,8 @@ class ConfigModel(BaseModel): ``` """, ) - desc_template_instance: DescTemplate = Field( - default_factory=lambda: DescTemplate({}, ""), + desc_template_instance: CommentTemplate = Field( + default_factory=lambda: CommentTemplate({}, ""), description="This variable does not support configuration and will be overwritten even if configured", ) @@ -91,7 +91,7 @@ class Config: def after_init(cls, values: Any) -> Any: if _pydantic_adapter.is_v1: # values: Dict[str, Any] - values["desc_template_instance"] = values["desc_template"](values["local_dict"], values["comment_prefix"]) + values["desc_template_instance"] = values["template"](values["local_dict"], values["comment_prefix"]) return values else: # values: "ConfigModel" diff --git a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py index bf621ac..8dcfeaa 100644 --- a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py +++ b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py @@ -12,7 +12,6 @@ from protobuf_to_pydantic import _pydantic_adapter from protobuf_to_pydantic.constant import protobuf_desc_python_type_dict, python_type_default_value_dict -from protobuf_to_pydantic.desc_template import DescTemplate from protobuf_to_pydantic.exceptions import WaitingToCompleteException from protobuf_to_pydantic.field_param import ( FieldParamModel, @@ -33,6 +32,7 @@ FileDescriptorProto, ) from protobuf_to_pydantic.plugin.my_types import ProtobufTypeModel +from protobuf_to_pydantic.template import CommentTemplate from protobuf_to_pydantic.util import camel_to_snake, gen_dict_from_desc_str if TYPE_CHECKING: @@ -79,7 +79,7 @@ def __init__(self, fd: FileDescriptorProto, descriptors: Descriptors, config: "C self.config = config self._fd: FileDescriptorProto = fd self._descriptors: Descriptors = descriptors - self._desc_template: DescTemplate = config.desc_template_instance + self._desc_template: CommentTemplate = config.desc_template_instance self.source_code_info_by_scl = {tuple(location.path): location for location in fd.source_code_info.location} if config.base_model_class is BaseModel: diff --git a/protobuf_to_pydantic/desc_template/__init__.py b/protobuf_to_pydantic/template/__init__.py similarity index 82% rename from protobuf_to_pydantic/desc_template/__init__.py rename to protobuf_to_pydantic/template/__init__.py index f6ebc28..57060a3 100644 --- a/protobuf_to_pydantic/desc_template/__init__.py +++ b/protobuf_to_pydantic/template/__init__.py @@ -1,14 +1,20 @@ import json from importlib import import_module -from typing import Any, Callable, List, Optional +from typing import Any, Callable, Dict, List, Optional from protobuf_to_pydantic.grpc_types import RepeatedCompositeContainer, RepeatedScalarContainer -class DescTemplate(object): - def __init__(self, local_dict: dict, comment_prefix: str) -> None: - self._local_dict: dict = local_dict +class CommentTemplate(object): + def __init__(self, local_dict: Dict[str, Any], comment_prefix: str, **kwargs: Any) -> None: + """ + :param local_dict: local template var + :param comment_prefix: comment prefix, The comment prefix that needs to be resolved + :param kwargs: Extended parameters for custom templates + """ + self._local_dict = local_dict self._comment_prefix: str = comment_prefix + self._kwargs: Dict[str, Any] = kwargs self._support_template_list: List[str] = [i for i in dir(self) if i.startswith("template")] def template_import_instance(self, module_str: str, var_str: str, json_param: str) -> Any: diff --git a/tests/base/base_p2p_validate.py b/tests/base/base_p2p_validate.py index 7968d66..d192661 100644 --- a/tests/base/base_p2p_validate.py +++ b/tests/base/base_p2p_validate.py @@ -6,7 +6,7 @@ import pytest from pydantic import ValidationError -from example.gen_p2p_code import CustomDescTemplate, CustomerField, confloat, conint, customer_any +from example.gen_p2p_code import CustomCommentTemplate, CustomerField, confloat, conint, customer_any from protobuf_to_pydantic._pydantic_adapter import is_v1 from protobuf_to_pydantic.grpc_types import AnyMessage @@ -34,29 +34,29 @@ def test_number_model_in_validator(self) -> None: for model_class in self.number_model_class_list: for i in [1, 2, 3]: - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomDescTemplate)(in_test=i, miss_default_test=1.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(in_test=i, miss_default_test=1.0, required_test=1.0) for i in [0, 4]: with pytest.raises(ValidationError): - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomDescTemplate)(in_test=i, miss_default_test=1.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(in_test=i, miss_default_test=1.0, required_test=1.0) def test_number_model_not_in_validator(self) -> None: for model_class in self.number_model_class_list: for i in [1, 2, 3]: with pytest.raises(ValidationError): - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomDescTemplate)(not_in_test=i, miss_default_test=1.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(not_in_test=i, miss_default_test=1.0, required_test=1.0) for i in [0, 4]: - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomDescTemplate)(not_in_test=i, miss_default_test=1.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(not_in_test=i, miss_default_test=1.0, required_test=1.0) def test_number_model_miss_default_validator(self) -> None: for model_class in self.number_model_class_list: with pytest.raises(ValidationError): - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomDescTemplate)() + self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)() def test_number_model_miss_multiple_of_validator(self) -> None: for model_class in self.number_model_class_list: - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomDescTemplate)(miss_default_test=1.0, multiple_of_test=6.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(miss_default_test=1.0, multiple_of_test=6.0, required_test=1.0) with pytest.raises(ValidationError): - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomDescTemplate)(miss_default_test=1.0, multiple_of_test=7.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(miss_default_test=1.0, multiple_of_test=7.0, required_test=1.0) def _test_bool(self, model_class: Type) -> None: model_class(bool_1_test=True, bool_2_test=False, miss_default_test=True, required_test=True) diff --git a/tests/test_gen_code_output_by_cli/test_p2p_validate_proto/test_p2p_validate.py b/tests/test_gen_code_output_by_cli/test_p2p_validate_proto/test_p2p_validate.py index 5f0cc00..27ca150 100644 --- a/tests/test_gen_code_output_by_cli/test_p2p_validate_proto/test_p2p_validate.py +++ b/tests/test_gen_code_output_by_cli/test_p2p_validate_proto/test_p2p_validate.py @@ -17,7 +17,7 @@ else: from example.proto_3_20_pydanticv2.example.example_proto.p2p_validate import demo_pb2 # type: ignore[no-redef] -from example.gen_p2p_code import CustomDescTemplate, CustomerField, confloat, conint, customer_any +from example.gen_p2p_code import CustomCommentTemplate, CustomerField, confloat, conint, customer_any from protobuf_to_pydantic import msg_to_pydantic_model, pydantic_model_to_py_code from protobuf_to_pydantic.util import format_content @@ -35,7 +35,7 @@ def _model_output(msg: Any) -> str: "conint": conint, "customer_any": customer_any, } - return pydantic_model_to_py_code(msg_to_pydantic_model(msg, local_dict=local_dict, desc_template=CustomDescTemplate)) + return pydantic_model_to_py_code(msg_to_pydantic_model(msg, local_dict=local_dict, desc_template=CustomCommentTemplate)) @staticmethod def assert_contains(content: str, other_content: str) -> None: From d6c11a7073172d0f7c29acea44f15a8ad3be2e4f Mon Sep 17 00:00:00 2001 From: so1n Date: Tue, 30 Jul 2024 01:44:44 +0800 Subject: [PATCH 02/10] Refactor, get_desc -> get_message_option, and add parse_rule module --- .pre-commit-config.yaml | 2 +- .../demo_gen_code_by_p2p.py | 2 +- .../demo_gen_code_by_pgv.py | 2 +- .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 2 +- .../demo_pb2_by_pyi.py | 2 +- .../example_proto/validate/demo_p2p.py | 2 +- .../demo_gen_code_by_p2p.py | 2 +- .../demo_gen_code_by_pgv.py | 2 +- .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 2 +- .../demo_pb2_by_pyi.py | 2 +- .../example_proto/validate/demo_p2p.py | 2 +- .../proto_pydanticv1/demo_gen_code_by_p2p.py | 2 +- .../proto_pydanticv1/demo_gen_code_by_pgv.py | 2 +- .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 2 +- .../demo_pb2_by_pyi.py | 2 +- .../example_proto/validate/demo_p2p.py | 2 +- example/proto_pydanticv2/demo_gen_code.py | 2 +- .../proto_pydanticv2/demo_gen_code_by_p2p.py | 34 +- .../proto_pydanticv2/demo_gen_code_by_pgv.py | 34 +- ...gen_code_by_text_comment_protobuf_field.py | 2 +- .../demo_gen_code_by_text_comment_pyi.py | 2 +- .../example_proto/common/single_p2p.py | 2 +- .../example/example_proto/demo/demo_p2p.py | 2 +- .../example_proto/p2p_validate/demo_p2p.py | 34 +- .../p2p_validate_by_comment/demo_p2p.py | 36 +- .../demo_pb2_by_protobuf.py | 36 +- .../demo_pb2_by_pyi.py | 36 +- .../example_proto/validate/demo_p2p.py | 34 +- protobuf_to_pydantic/constant.py | 18 + protobuf_to_pydantic/gen_model.py | 37 +- protobuf_to_pydantic/get_desc/__init__.py | 3 - .../get_desc/from_pb_option/__init__.py | 4 - .../get_desc/from_pb_option/base.py | 625 ------------------ .../get_desc/from_pb_option/from_p2p.py | 16 - protobuf_to_pydantic/get_desc/utils.py | 23 - .../get_message_option/__init__.py | 6 + .../from_message_option/__init__.py | 4 + .../from_message_option/base.py | 118 ++++ .../from_message_option/from_p2p.py | 13 + .../from_message_option}/from_pgv.py | 4 +- .../from_proto_file.py | 39 +- .../from_pyi_file.py | 64 +- .../get_message_option/utils.py | 29 + protobuf_to_pydantic/parse_rule/__init__.py | 0 .../protobuf_option_to_field_info/__init__.py | 0 .../protobuf_option_to_field_info/base.py | 221 +++++++ .../protobuf_option_to_field_info/comment.py | 111 ++++ .../protobuf_option_to_field_info/desc.py | 134 ++++ .../protobuf_option_to_field_info}/types.py | 2 +- .../plugin/field_desc_proto_to_code.py | 25 +- protobuf_to_pydantic/types.py | 4 +- protobuf_to_pydantic/util.py | 24 +- 58 files changed, 898 insertions(+), 924 deletions(-) delete mode 100644 protobuf_to_pydantic/get_desc/__init__.py delete mode 100644 protobuf_to_pydantic/get_desc/from_pb_option/__init__.py delete mode 100644 protobuf_to_pydantic/get_desc/from_pb_option/base.py delete mode 100644 protobuf_to_pydantic/get_desc/from_pb_option/from_p2p.py delete mode 100644 protobuf_to_pydantic/get_desc/utils.py create mode 100644 protobuf_to_pydantic/get_message_option/__init__.py create mode 100644 protobuf_to_pydantic/get_message_option/from_message_option/__init__.py create mode 100644 protobuf_to_pydantic/get_message_option/from_message_option/base.py create mode 100644 protobuf_to_pydantic/get_message_option/from_message_option/from_p2p.py rename protobuf_to_pydantic/{get_desc/from_pb_option => get_message_option/from_message_option}/from_pgv.py (54%) rename protobuf_to_pydantic/{get_desc => get_message_option}/from_proto_file.py (67%) rename protobuf_to_pydantic/{get_desc => get_message_option}/from_pyi_file.py (63%) create mode 100644 protobuf_to_pydantic/get_message_option/utils.py create mode 100644 protobuf_to_pydantic/parse_rule/__init__.py create mode 100644 protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/__init__.py create mode 100644 protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/base.py create mode 100644 protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/comment.py create mode 100644 protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/desc.py rename protobuf_to_pydantic/{get_desc/from_pb_option => parse_rule/protobuf_option_to_field_info}/types.py (98%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a380c51..a0f5ea1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.0 + rev: v1.8.0 hooks: - id: mypy exclude: "^example/example_proto|^example/proto|^protobuf_to_pydantic/protos|test_p2p_validate.py$|test_pgv_validate.py$|protobuf_to_pydantic/contrib/proto_parser.py|example/validate_example/demo_gen_code_by_pgv.py|example/p2p_validate_example/demo_gen_code_by_p2p.py|protobuf_to_pydantic/_pydantic_adapter.py|protobuf_to_pydantic/customer_validator/__init__.py|protobuf_to_pydantic/customer_validator/v1.py|protobuf_to_pydantic/customer_validator/v2.py|protobuf_to_pydantic/customer_con_type/__init__.py|protobuf_to_pydantic/customer_con_type/v1.py|protobuf_to_pydantic/customer_con_type/v2.py|protobuf_to_pydantic/grpc_types.py" diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py index f3976a7..653b583 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py @@ -42,7 +42,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py index 436deff..e18bb5d 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py @@ -39,7 +39,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py index d7bc702..95ce0a6 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py @@ -45,7 +45,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 42d32e6..697d76d 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -45,7 +45,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index a8b2dcd..db4a682 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index a8b2dcd..db4a682 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py index 9133b94..b0a11bb 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py @@ -44,7 +44,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py index 78c1670..47e2151 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py @@ -44,7 +44,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py index ee1fd2a..677b474 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py index f0a8e0c..b79cbba 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py @@ -49,7 +49,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 68796df..0b5aa68 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -49,7 +49,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index a297a48..4ad72b8 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -43,7 +43,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index a297a48..4ad72b8 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -43,7 +43,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py index c1adc2c..e760b05 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py @@ -47,7 +47,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_pydanticv1/demo_gen_code_by_p2p.py b/example/proto_pydanticv1/demo_gen_code_by_p2p.py index 42d2c89..3ad1fbd 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_p2p.py +++ b/example/proto_pydanticv1/demo_gen_code_by_p2p.py @@ -42,7 +42,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_pydanticv1/demo_gen_code_by_pgv.py b/example/proto_pydanticv1/demo_gen_code_by_pgv.py index 1b19d40..079c753 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_pgv.py +++ b/example/proto_pydanticv1/demo_gen_code_by_pgv.py @@ -39,7 +39,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py index 5d1a765..eca2965 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py @@ -45,7 +45,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 0b0f968..4161ae1 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -45,7 +45,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index 16981f6..0d0f812 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index 16981f6..0d0f812 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py index 0758e69..bea3cc9 100644 --- a/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py @@ -44,7 +44,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_pydanticv2/demo_gen_code.py b/example/proto_pydanticv2/demo_gen_code.py index b856fa8..a654b9b 100644 --- a/example/proto_pydanticv2/demo_gen_code.py +++ b/example/proto_pydanticv2/demo_gen_code.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/demo_gen_code_by_p2p.py b/example/proto_pydanticv2/demo_gen_code_by_p2p.py index e311906..7e974c6 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_p2p.py +++ b/example/proto_pydanticv2/demo_gen_code_by_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing @@ -44,7 +44,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator @@ -129,7 +129,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -154,7 +154,7 @@ class DoubleTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -252,7 +252,7 @@ class EnumTest(BaseModel): title_test: State = Field(default=0, title="title_test") extra_test: State = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -277,7 +277,7 @@ class Fixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -302,7 +302,7 @@ class Fixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -327,7 +327,7 @@ class FloatTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -352,7 +352,7 @@ class Int32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -377,7 +377,7 @@ class Int64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -465,7 +465,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -581,7 +581,7 @@ class Sfixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -606,7 +606,7 @@ class Sfixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -631,7 +631,7 @@ class Sint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -656,7 +656,7 @@ class Sint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -736,7 +736,7 @@ class Uint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -761,5 +761,5 @@ class Uint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/example/proto_pydanticv2/demo_gen_code_by_pgv.py b/example/proto_pydanticv2/demo_gen_code_by_pgv.py index 48d2568..b51af1b 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_pgv.py +++ b/example/proto_pydanticv2/demo_gen_code_by_pgv.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator @@ -95,7 +95,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -107,7 +107,7 @@ class DoubleTest(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1.0, 2.0, 3.0]) ignore_test: float = Field(default=0.0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -168,7 +168,7 @@ class EnumTest(BaseModel): in_test: State = Field(default=0, in_=[0, 2]) not_in_test: State = Field(default=0, not_in=[0, 2]) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -180,7 +180,7 @@ class Fixed32Test(BaseModel): not_in_test: float = Field(default=0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -192,7 +192,7 @@ class Fixed64Test(BaseModel): not_in_test: float = Field(default=0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -204,7 +204,7 @@ class FloatTest(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1.0, 2.0, 3.0]) ignore_test: float = Field(default=0.0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -216,7 +216,7 @@ class Int32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -228,7 +228,7 @@ class Int64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -302,7 +302,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -381,7 +381,7 @@ class Sfixed32Test(BaseModel): not_in_test: float = Field(default=0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -393,7 +393,7 @@ class Sfixed64Test(BaseModel): not_in_test: float = Field(default=0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -405,7 +405,7 @@ class Sint32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -417,7 +417,7 @@ class Sint64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -473,7 +473,7 @@ class Uint32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -485,5 +485,5 @@ class Uint64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py index 45154b7..5046229 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py b/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py index 45154b7..5046229 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/example/example_proto/common/single_p2p.py b/example/proto_pydanticv2/example/example_proto/common/single_p2p.py index 02ec71c..32ba63e 100644 --- a/example/proto_pydanticv2/example/example_proto/common/single_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/common/single_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 from enum import IntEnum diff --git a/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py index 26eb37f..68e414d 100644 --- a/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py index e74cebd..28fc635 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing @@ -49,7 +49,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta @@ -80,7 +80,7 @@ class FloatTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -105,7 +105,7 @@ class DoubleTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -130,7 +130,7 @@ class Int32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -155,7 +155,7 @@ class Int64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -180,7 +180,7 @@ class Uint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -205,7 +205,7 @@ class Sint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -230,7 +230,7 @@ class Uint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -255,7 +255,7 @@ class Sint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -280,7 +280,7 @@ class Fixed32Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -305,7 +305,7 @@ class Fixed64Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -330,7 +330,7 @@ class Sfixed32Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -355,7 +355,7 @@ class Sfixed64Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -416,7 +416,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -446,7 +446,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -464,7 +464,7 @@ class EnumTest(BaseModel): title_test: State = Field(default=0, title="title_test") extra_test: State = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py index e082279..f78397e 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing @@ -49,7 +49,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta @@ -80,7 +80,7 @@ class FloatTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -105,7 +105,7 @@ class DoubleTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -130,7 +130,7 @@ class Int32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -155,7 +155,7 @@ class Int64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -180,7 +180,7 @@ class Uint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -205,7 +205,7 @@ class Sint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -230,7 +230,7 @@ class Uint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -255,7 +255,7 @@ class Sint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -280,7 +280,7 @@ class Fixed32Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -305,7 +305,7 @@ class Fixed64Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -330,7 +330,7 @@ class Sfixed32Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -355,7 +355,7 @@ class Sfixed64Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -416,7 +416,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -446,7 +446,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -464,7 +464,7 @@ class EnumTest(BaseModel): title_test: State = Field(default=0, title="title_test") extra_test: State = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -509,7 +509,7 @@ class RepeatedTest(BaseModel): items_string_test: typing.List[ typing_extensions.Annotated[str, MinLen(min_length=1), MaxLen(max_length=5)] ] = Field(default_factory=list, min_length=1, max_length=5) - items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1), Lt(lt=5)]] = Field( + items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1.0), Lt(lt=5.0)]] = Field( default_factory=list, min_length=1, max_length=5 ) items_int32_test: typing.List[typing_extensions.Annotated[int, Gt(gt=1), Lt(lt=5)]] = Field( diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index 3137a28..b8c61ba 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing @@ -43,7 +43,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator @@ -128,7 +128,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -153,7 +153,7 @@ class DoubleTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -251,7 +251,7 @@ class EnumTest(BaseModel): title_test: State = Field(default=0, title="title_test") extra_test: State = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -276,7 +276,7 @@ class Fixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -301,7 +301,7 @@ class Fixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -326,7 +326,7 @@ class FloatTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -351,7 +351,7 @@ class Int32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -376,7 +376,7 @@ class Int64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -464,7 +464,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -532,7 +532,7 @@ class RepeatedTest(BaseModel): items_string_test: typing.List[ typing_extensions.Annotated[str, MinLen(min_length=1), MaxLen(max_length=5)] ] = Field(default_factory=list, min_length=1, max_length=5) - items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1), Lt(lt=5)]] = Field( + items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1.0), Lt(lt=5.0)]] = Field( default_factory=list, min_length=1, max_length=5 ) items_int32_test: typing.List[typing_extensions.Annotated[int, Gt(gt=1), Lt(lt=5)]] = Field( @@ -580,7 +580,7 @@ class Sfixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -605,7 +605,7 @@ class Sfixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -630,7 +630,7 @@ class Sint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -655,7 +655,7 @@ class Sint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -735,7 +735,7 @@ class Uint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -760,5 +760,5 @@ class Uint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index 3137a28..b8c61ba 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing @@ -43,7 +43,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator @@ -128,7 +128,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -153,7 +153,7 @@ class DoubleTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -251,7 +251,7 @@ class EnumTest(BaseModel): title_test: State = Field(default=0, title="title_test") extra_test: State = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -276,7 +276,7 @@ class Fixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -301,7 +301,7 @@ class Fixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -326,7 +326,7 @@ class FloatTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -351,7 +351,7 @@ class Int32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -376,7 +376,7 @@ class Int64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -464,7 +464,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -532,7 +532,7 @@ class RepeatedTest(BaseModel): items_string_test: typing.List[ typing_extensions.Annotated[str, MinLen(min_length=1), MaxLen(max_length=5)] ] = Field(default_factory=list, min_length=1, max_length=5) - items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1), Lt(lt=5)]] = Field( + items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1.0), Lt(lt=5.0)]] = Field( default_factory=list, min_length=1, max_length=5 ) items_int32_test: typing.List[typing_extensions.Annotated[int, Gt(gt=1), Lt(lt=5)]] = Field( @@ -580,7 +580,7 @@ class Sfixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -605,7 +605,7 @@ class Sfixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -630,7 +630,7 @@ class Sint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -655,7 +655,7 @@ class Sint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -735,7 +735,7 @@ class Uint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -760,5 +760,5 @@ class Uint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py index da9c02f..e8cc533 100644 --- a/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing @@ -47,7 +47,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.get_desc.from_pb_option.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta @@ -65,7 +65,7 @@ class FloatTest(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1.0, 2.0, 3.0]) ignore_test: float = Field(default=0.0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -77,7 +77,7 @@ class DoubleTest(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1.0, 2.0, 3.0]) ignore_test: float = Field(default=0.0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -89,7 +89,7 @@ class Int32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -101,7 +101,7 @@ class Uint32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -113,7 +113,7 @@ class Sint32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -125,7 +125,7 @@ class Int64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -137,7 +137,7 @@ class Uint64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -149,7 +149,7 @@ class Sint64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -161,7 +161,7 @@ class Fixed32Test(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0.0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -173,7 +173,7 @@ class Fixed64Test(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0.0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -185,7 +185,7 @@ class Sfixed32Test(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0.0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -197,7 +197,7 @@ class Sfixed64Test(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0.0) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -238,7 +238,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -259,7 +259,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -269,7 +269,7 @@ class EnumTest(BaseModel): in_test: State = Field(default=0, in_=[0, 2]) not_in_test: State = Field(default=0, not_in=[0, 2]) - in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/protobuf_to_pydantic/constant.py b/protobuf_to_pydantic/constant.py index eb90a01..e8500c0 100644 --- a/protobuf_to_pydantic/constant.py +++ b/protobuf_to_pydantic/constant.py @@ -58,3 +58,21 @@ FieldDescriptor.TYPE_SINT32: int, FieldDescriptor.TYPE_SINT64: int, } +protobuf_common_type_dict: Dict[Any, str] = { + FieldDescriptor.TYPE_DOUBLE: "double", + FieldDescriptor.TYPE_FLOAT: "float", + FieldDescriptor.TYPE_INT64: "int64", + FieldDescriptor.TYPE_UINT64: "uint64", + FieldDescriptor.TYPE_INT32: "int32", + FieldDescriptor.TYPE_FIXED64: "fixed64", + FieldDescriptor.TYPE_FIXED32: "fixed32", + FieldDescriptor.TYPE_BOOL: "bool", + FieldDescriptor.TYPE_STRING: "string", + FieldDescriptor.TYPE_BYTES: "bytes", + FieldDescriptor.TYPE_UINT32: "uint32", + FieldDescriptor.TYPE_ENUM: "enum", + FieldDescriptor.TYPE_SFIXED32: "sfixed32", + FieldDescriptor.TYPE_SFIXED64: "sfixed64", + FieldDescriptor.TYPE_SINT32: "sint32", + FieldDescriptor.TYPE_SINT64: "sint64", +} diff --git a/protobuf_to_pydantic/gen_model.py b/protobuf_to_pydantic/gen_model.py index 6aa05db..dedea41 100644 --- a/protobuf_to_pydantic/gen_model.py +++ b/protobuf_to_pydantic/gen_model.py @@ -12,6 +12,7 @@ from typing_extensions import Annotated, get_origin from protobuf_to_pydantic import _pydantic_adapter, constant +from protobuf_to_pydantic.constant import protobuf_common_type_dict from protobuf_to_pydantic.customer_validator import check_one_of from protobuf_to_pydantic.exceptions import WaitingToCompleteException from protobuf_to_pydantic.field_param import ( @@ -19,19 +20,21 @@ field_param_dict_handle, field_param_dict_migration_v2_handler, ) -from protobuf_to_pydantic.get_desc import ( - get_desc_from_p2p, - get_desc_from_pgv, - get_desc_from_proto_file, - get_desc_from_pyi_file, +from protobuf_to_pydantic.get_message_option import ( + get_message_option_dict_from_message_with_p2p, + get_message_option_dict_from_message_with_pgv, + get_message_option_dict_from_proto_file, + get_message_option_dict_from_pyi_file, ) -from protobuf_to_pydantic.get_desc.from_pb_option.base import field_comment_handler, protobuf_common_type_dict from protobuf_to_pydantic.grpc_types import AnyMessage, Descriptor, FieldDescriptor, Message +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.comment import ( + gen_field_rule_info_dict_from_field_comment_dict, +) from protobuf_to_pydantic.template import CommentTemplate from protobuf_to_pydantic.util import create_pydantic_model if TYPE_CHECKING: - from protobuf_to_pydantic.types import DescFromOptionTypedDict, FieldInfoTypedDict, UseOneOfTypedDict + from protobuf_to_pydantic.types import FieldInfoTypedDict, MessageOptionTypedDict, UseOneOfTypedDict def replace_file_name_to_class_name(filename: str) -> str: @@ -121,7 +124,7 @@ def __init__( create_model_cache: Optional[CREATE_MODEL_CACHE_T] = None, ): proto_file_name = msg.DESCRIPTOR.file.name # type: ignore - message_field_dict: Dict[str, "DescFromOptionTypedDict"] = {} + message_field_dict: Dict[str, "MessageOptionTypedDict"] = {} if proto_file_name.endswith("empty.proto") or parse_msg_desc_method == "ignore": pass @@ -130,7 +133,7 @@ def __init__( file_str: str = parse_msg_desc_method if not file_str.endswith("/"): file_str += "/" - message_field_dict = get_desc_from_proto_file(file_str + proto_file_name, comment_prefix) + message_field_dict = get_message_option_dict_from_proto_file(file_str + proto_file_name, comment_prefix) elif inspect.ismodule(parse_msg_desc_method): # get field dict from pyi file if getattr(parse_msg_desc_method, msg.__name__, None) is not msg: # type: ignore @@ -138,10 +141,10 @@ def __init__( pyi_file_name = parse_msg_desc_method.__file__ + "i" # type: ignore if not Path(pyi_file_name).exists(): raise RuntimeError(f"Can not found {msg} pyi file") - message_field_dict = get_desc_from_pyi_file(pyi_file_name, comment_prefix) + message_field_dict = get_message_option_dict_from_pyi_file(pyi_file_name, comment_prefix) elif parse_msg_desc_method == "PGV": # get field dict from pgv - message_field_dict = get_desc_from_pgv(message=msg) # type: ignore + message_field_dict = get_message_option_dict_from_message_with_pgv(message=msg) # type: ignore elif parse_msg_desc_method is not None: raise ValueError( f"parse_msg_desc_method param must be exist path, `ignore` or `PGV`," @@ -149,10 +152,10 @@ def __init__( ) else: # get field dict from p2p - message_field_dict = get_desc_from_p2p(message=msg) # type: ignore + message_field_dict = get_message_option_dict_from_message_with_p2p(message=msg) # type: ignore self._parse_msg_desc_method: Optional[str] = parse_msg_desc_method - self._field_doc_dict: Dict[str, DescFromOptionTypedDict] = message_field_dict + self._field_doc_dict: Dict[str, MessageOptionTypedDict] = message_field_dict self._default_field = default_field self._comment_prefix = comment_prefix self._creat_cache: CREATE_MODEL_CACHE_T = create_model_cache or _create_model_cache @@ -197,7 +200,7 @@ def _get_field_info_dict_by_full_name(self, full_name: str) -> Optional["FieldIn message_name, *key_list = split_full_name[1:] # ignore package name if message_name not in self._field_doc_dict: return None - desc_dict: "DescFromOptionTypedDict" = self._field_doc_dict[message_name] + desc_dict: "MessageOptionTypedDict" = self._field_doc_dict[message_name] if desc_dict["metadata"].get("ignore", False): return None @@ -213,7 +216,7 @@ def _get_field_info_dict_by_full_name(self, full_name: str) -> Optional["FieldIn return None def _one_of_handle(self, descriptor: Descriptor) -> Tuple[Dict[str, "UseOneOfTypedDict"], Dict[str, Any]]: - desc_dict: "DescFromOptionTypedDict" = self._field_doc_dict.get(descriptor.name, {}) # type: ignore + desc_dict: "MessageOptionTypedDict" = self._field_doc_dict.get(descriptor.name, {}) # type: ignore ignore_parse_rule = desc_dict.get("metadata", {}).get("ignored", False) one_of_desc_dict = {} if not ignore_parse_rule: @@ -269,7 +272,7 @@ def _get_pydantic_base(self, config_dict: Dict[str, Any]) -> Type[BaseModel]: else: from pydantic import ConfigDict - _config_dict = {"model_config": ConfigDict(**config_dict)} # type: ignore[misc] + _config_dict = {"model_config": ConfigDict(**config_dict)} # type: ignore[typeddict-item] # Changing the configuration of Config by inheritance pydantic_base: Type[BaseModel] = type( # type: ignore @@ -439,7 +442,7 @@ def _gen_field_info(self, field_dataclass: FieldDataClass, skip_validate_rule: b field_doc_dict = self._desc_template.handle_template_var(field_doc_dict) if not (self._parse_msg_desc_method is None or self._parse_msg_desc_method == "PGV"): # comment rule need handler - field_doc_dict = field_comment_handler( + field_doc_dict = gen_field_rule_info_dict_from_field_comment_dict( field_doc_dict, # type:ignore[arg-type] field=field_dataclass.protobuf_field, type_name=field_dataclass.field_type_name, diff --git a/protobuf_to_pydantic/get_desc/__init__.py b/protobuf_to_pydantic/get_desc/__init__.py deleted file mode 100644 index b776887..0000000 --- a/protobuf_to_pydantic/get_desc/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .from_pb_option import get_desc_from_p2p, get_desc_from_pgv -from .from_proto_file import get_desc_from_proto_file -from .from_pyi_file import get_desc_from_pyi_file diff --git a/protobuf_to_pydantic/get_desc/from_pb_option/__init__.py b/protobuf_to_pydantic/get_desc/from_pb_option/__init__.py deleted file mode 100644 index 7795d0b..0000000 --- a/protobuf_to_pydantic/get_desc/from_pb_option/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .from_p2p import get_desc_from_p2p -from .from_pgv import get_desc_from_pgv - -__all__ = ["get_desc_from_pgv", "get_desc_from_p2p"] diff --git a/protobuf_to_pydantic/get_desc/from_pb_option/base.py b/protobuf_to_pydantic/get_desc/from_pb_option/base.py deleted file mode 100644 index 7840ccb..0000000 --- a/protobuf_to_pydantic/get_desc/from_pb_option/base.py +++ /dev/null @@ -1,625 +0,0 @@ -import inspect -import logging -from datetime import datetime, timedelta -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union - -from protobuf_to_pydantic import _pydantic_adapter -from protobuf_to_pydantic.customer_con_type import ( - conbytes, - confloat, - conint, - conlist, - constr, - contimedelta, - contimestamp, -) -from protobuf_to_pydantic.customer_validator import validate_validator_dict -from protobuf_to_pydantic.grpc_types import Descriptor, FieldDescriptor, FieldDescriptorProto, Message, Timestamp -from protobuf_to_pydantic.types import DescFromOptionTypedDict, FieldInfoTypedDict, OneOfTypedDict -from protobuf_to_pydantic.util import replace_protobuf_type_to_python_type - -from .types import column_pydantic_type_dict - -_logger: logging.Logger = logging.getLogger(__name__) - -protobuf_common_type_dict: Dict[Any, str] = { - FieldDescriptor.TYPE_DOUBLE: "double", - FieldDescriptor.TYPE_FLOAT: "float", - FieldDescriptor.TYPE_INT64: "int64", - FieldDescriptor.TYPE_UINT64: "uint64", - FieldDescriptor.TYPE_INT32: "int32", - FieldDescriptor.TYPE_FIXED64: "fixed64", - FieldDescriptor.TYPE_FIXED32: "fixed32", - FieldDescriptor.TYPE_BOOL: "bool", - FieldDescriptor.TYPE_STRING: "string", - FieldDescriptor.TYPE_BYTES: "bytes", - FieldDescriptor.TYPE_UINT32: "uint32", - FieldDescriptor.TYPE_ENUM: "enum", - FieldDescriptor.TYPE_SFIXED32: "sfixed32", - FieldDescriptor.TYPE_SFIXED64: "sfixed64", - FieldDescriptor.TYPE_SINT32: "sint32", - FieldDescriptor.TYPE_SINT64: "sint64", -} - -type_not_support_dict: Dict[Any, Set[str]] = { - FieldDescriptor.TYPE_BYTES: {"pattern"}, - FieldDescriptor.TYPE_STRING: {"min_bytes", "max_bytes", "well_known_regex", "strict"}, - "Any": {"ignore_empty", "defined_only", "no_sparse"}, -} - -pgv_column_to_pydantic_dict: Dict[str, str] = { - "min_len": "min_length", - "min_bytes": "min_length", - "max_len": "max_length", - "max_bytes": "max_length", - "pattern": "regex", - "unique": "unique_items", - "gte": "ge", - "lte": "le", - "len_bytes": "len", - "miss_default": "required", -} - - -def _has_raw_message_field(column: str, message: Message) -> bool: - """Determine whether the field held by the Message is a field defined by Protobuf""" - if column.startswith("_"): - return False - if getattr(Message, column, None): - return False - try: - if not message.HasField(column): - return False - except ValueError: - if not getattr(message, column, None) or column[0].lower() != column[0]: - return False - return True - - -def get_type_hints_from_type_name(type_name: str) -> Optional[Type]: - if type_name == "string": - return str - elif "double" in type_name or "float" in type_name: - return float - elif "int" in type_name: - return int - elif type_name == "duration": - return timedelta - elif type_name == "timestamp": - return datetime - elif type_name == "bytes": - return bytes - else: - return None - - -def get_con_type_func_from_type_name(type_name: str) -> Optional[Callable]: - if type_name == "string": - return constr - elif "double" in type_name or "float" in type_name: - return confloat - elif "int" in type_name: - return conint - elif type_name == "duration": - return contimedelta - elif type_name == "timestamp": - return contimestamp - elif type_name == "bytes": - return conbytes - else: - return None - - -def field_dict_handler( - field_dict: Dict[str, Any], - *, - field_name: str, - field_type: int, - type_name: str, - full_name: str, - sub_value_handler: Callable[[Any], Dict[str, Any]], - sub_type_name_handler: Callable[[Any], str], - rule_value_to_field_value_handler: Callable[[str, str, Any], str], - value_type_conversion_handler: Optional[Callable[[str, str, Any], Tuple[bool, Any]]] = None, -) -> FieldInfoTypedDict: - """ - Adapt some Field verification information to convert it into verification information that is compatible with - Protobuf's special type of pydantic - """ - field_info_type_dict: FieldInfoTypedDict = {"extra": {}, "skip": False} - for column, column_value in field_dict.items(): - if column in type_not_support_dict.get(field_type, type_not_support_dict["Any"]): - # Exclude unsupported fields - rule_name = column - if field_type in protobuf_common_type_dict: - rule_name = f"{protobuf_common_type_dict[field_type]}.{rule_name}" - elif field_type == 1: - rule_name = f"Message.{rule_name}" - - msg: str = f"{__name__} not support `{rule_name}` rule." - if full_name: - msg = msg + f"(field:{full_name})" - _logger.warning(msg) - continue - if value_type_conversion_handler: - is_change, new_column_value = value_type_conversion_handler(type_name, column, column_value) - if is_change: - column_value = new_column_value - if column in pgv_column_to_pydantic_dict: - # Field Conversion - column = pgv_column_to_pydantic_dict[column] - - if type_name in ("duration", "any", "timestamp", "map") and column in ( - "lt", - "le", - "gt", - "ge", - "const", - "in", - "not_in", - "lt_now", - "gt_now", - "within", - "min_pairs", - "max_pairs", - ): - # The verification of these parameters is handed over to the validator, - # see protobuf_to_pydantic/customer_validator for details - - # Types of priority treatment for special cases - if "validator" not in field_info_type_dict: - field_info_type_dict["validator"] = {} - - _column: str = f"{type_name}_{column}" - field_info_type_dict["extra"][_column] = rule_value_to_field_value_handler(type_name, column, column_value) - field_info_type_dict["validator"][f"{field_name}_{_column}_validator"] = _pydantic_adapter.field_validator( - field_name, allow_reuse=True - )(validate_validator_dict[f"{_column}_validator"]) - continue - elif column in ("in", "not_in", "len", "prefix", "suffix", "contains", "not_contains"): - # The verification of these parameters is handed over to the validator, - # see protobuf_to_pydantic/customer_validator for details - - # Compatible with PGV attributes that are not supported by pydantic - if "validator" not in field_info_type_dict: - field_info_type_dict["validator"] = {} - _column = column + "_" if column in ("in",) else column - field_info_type_dict["extra"][_column] = rule_value_to_field_value_handler(type_name, column, column_value) - field_info_type_dict["validator"][f"{field_name}_{column}_validator"] = _pydantic_adapter.field_validator( - field_name, allow_reuse=True - )(validate_validator_dict[f"{column}_validator"]) - continue - elif column in column_pydantic_type_dict: - # Support some built-in type judgments of PGV - field_info_type_dict["type"] = column_pydantic_type_dict[column] - continue - elif column in ("keys", "values"): - # Parse the field data of the key and value in the map - type_name = sub_type_name_handler(column_value) - # Nested types are supported via like constr - - # Generate information corresponding to the nested type - sub_dict = field_dict_handler( - sub_value_handler(column_value), - field_name=field_name, - field_type=field_type, - type_name=type_name, - full_name=full_name, - sub_value_handler=sub_value_handler, - sub_type_name_handler=sub_type_name_handler, - rule_value_to_field_value_handler=rule_value_to_field_value_handler, - ) - con_type = get_con_type_func_from_type_name(type_name) - if not con_type: - # TODO nested message - _logger.warning(f"{__name__} not support sub type `{type_name}`, please reset {full_name}") - continue - if "map_type" not in field_info_type_dict: - field_info_type_dict["map_type"] = {} - con_type_param_dict: dict = {} - for _key in inspect.signature(con_type).parameters.keys(): - param_value = sub_dict.get(_key, None) # type: ignore - if param_value is not None: - con_type_param_dict[_key] = param_value - elif "extra" in sub_dict: - if sub_dict["extra"].get(_key, None) is not None: - con_type_param_dict[_key] = sub_dict["extra"][_key] - - field_info_type_dict["map_type"][column] = con_type(**con_type_param_dict) - # if _pydantic_adapter.is_v1: - # con_type = get_con_type_func_from_type_name(type_name) - # if not con_type: - # # TODO nested message - # _logger.warning(f"{__name__} not support sub type `{type_name}`, please reset {full_name}") - # continue - # if "map_type" not in field_info_type_dict: - # field_info_type_dict["map_type"] = {} - # con_type_param_dict: dict = {} - # for _key in inspect.signature(con_type).parameters.keys(): - # param_value = sub_dict.get(_key, None) # type: ignore - # if param_value is not None: - # con_type_param_dict[_key] = param_value - # elif "extra" in sub_dict: - # if sub_dict["extra"].get(_key, None) is not None: - # con_type_param_dict[_key] = sub_dict["extra"][_key] - # - # field_info_type_dict["map_type"][column] = con_type(**con_type_param_dict) - # else: - # _type = get_type_hints_from_type_name(type_name) - # if not _type: - # # TODO nested message - # _logger.warning(f"{__name__} not support sub type `{type_name}`, please reset {full_name}") - # continue - # if "map_type" not in field_info_type_dict: - # field_info_type_dict["map_type"] = {} - # - # from pydantic import Field - # - # with_validator_param_field = Field(**sub_dict) - # if not with_validator_param_field.metadata: - # field_info_type_dict["map_type"][column] = _type - # else: - # field_info_type_dict["map_type"][column] = Annotated.__class_getitem__( - # (_type, *with_validator_param_field.metadata) - # ) - # TODO extra handler - # - # con_type_param_dict: dict = {} - # for _key in inspect.signature(con_type).parameters.keys(): - # param_value = sub_dict.get(_key, None) # type: ignore - # if param_value is not None: - # con_type_param_dict[_key] = param_value - # elif "extra" in sub_dict: - # if sub_dict["extra"].get(_key, None) is not None: - # con_type_param_dict[_key] = sub_dict["extra"][_key] - # - # field_info_type_dict["map_type"][column] = con_type(**con_type_param_dict) - elif column == "items": - # Process array data - type_name = sub_type_name_handler(column_value) - # Nested types are supported via like constr - # TODO pydantic v2 - con_type = get_con_type_func_from_type_name(type_name) - # if _pydantic_adapter.is_v1: - # con_type = get_con_type_func_from_type_name(type_name) - # else: - # con_type = get_type_hints_from_type_name(type_name) - if not con_type: - # TODO nested message - _logger.warning(f"{__name__} not support sub type `{type_name}`, please reset {full_name}") - field_info_type_dict["type"] = List - continue - # sub_dict = {"extra": {}, "type": con_type, "skip": False} - sub_dict = field_dict_handler( - sub_value_handler(column_value), - field_name=field_name, - field_type=field_type, - type_name=type_name, - full_name=full_name, - sub_value_handler=sub_value_handler, - sub_type_name_handler=sub_type_name_handler, - rule_value_to_field_value_handler=rule_value_to_field_value_handler, - ) - if "type" not in sub_dict: - sub_dict["type"] = con_type - - field_info_type_dict["type"] = conlist - field_info_type_dict["sub"] = sub_dict - - field_info_type_dict[column] = column_value # type: ignore - return field_info_type_dict - - -def field_comment_handler( # noqa: C901 - field_dict: Dict[str, Any], - field: Union[FieldDescriptor, FieldDescriptorProto], - type_name: str, - full_name: str, -) -> FieldInfoTypedDict: - """Parse the data of option and store it in dict. - Since array and map are supported, the complexity is relatively high - - The field names used by validator need to be prefixed with type_name, such as timestamp_gt, - while the built-in validation of Pydantic V2 does not require type_name prefix, such as gt - """ - - def _duration_handler(_value: Any) -> Optional[timedelta]: - if isinstance(_value, timedelta): - return _value - elif not isinstance(_value, dict): - return _value - seconds = _value.get("seconds", 0) - nanos = _value.get("nanos", 0) - if nanos: - nanos = nanos / 1000 - if not seconds and not nanos: - return None - return timedelta(seconds=seconds, microseconds=nanos) - - def sub_value_handler(column_value: Any) -> Dict[str, Any]: - sub_type_name = sub_type_name_handler(column_value) - return column_value[sub_type_name] - - def sub_type_name_handler(column_value: Any) -> str: - sub_type_name = list(column_value.keys())[0] - return sub_type_name - - def value_type_conversion_handler(column_type_name: str, column_name: str, column_value: Any) -> Tuple[bool, Any]: - if type_name in ("double", "float") and isinstance(column_value, (float, int)): - return True, float(column_value) - elif type_name == "bytes" and isinstance(column_value, str): - return True, column_value.encode("utf-8") - elif isinstance(column_value, list): - new_value = [] - is_change: bool = False - for i in column_value: - _is_change, i = value_type_conversion_handler(column_type_name, column_name, i) - new_value.append(i) - if _is_change: - is_change = _is_change - if is_change: - return is_change, new_value - return is_change, column_value - elif column_name not in ( - "lt", - "le", - "gt", - "ge", - "const", - "in", - "not_in", - "lt_now", - "gt_now", - "within", - "min_pairs", - "max_pairs", - ): - if type_name == "duration" and isinstance(column_value, dict): - new_column_value = _duration_handler(column_value) - if new_column_value: - return True, new_column_value - return False, column_value - elif type_name == "timestamp" and isinstance(column_value, dict): - if "seconds" in column_value or "nanos" in column_value: - return True, Timestamp(**column_value) - else: - return False, column_value - return False, column_value - - def rule_value_to_field_value_handler(column_type_name: str, column_name: str, column_value: Any) -> Any: - if isinstance(column_value, list): - return [rule_value_to_field_value_handler(column_type_name, column_name, i) for i in column_value] - elif column_type_name == "duration": - return _duration_handler(column_value) - elif column_type_name == "timestamp": - if column_name in ("const", "gt", "lt", "ge", "le") and isinstance(column_value, dict): - return Timestamp(**column_value) - elif column_name in ("within",): - return _duration_handler(column_value) - return column_value - - return field_dict_handler( - field_dict, - field_name=field.name, - field_type=field.type, - type_name=type_name, - full_name=full_name, - sub_value_handler=sub_value_handler, - sub_type_name_handler=sub_type_name_handler, - rule_value_to_field_value_handler=rule_value_to_field_value_handler, - value_type_conversion_handler=value_type_conversion_handler, - ) - - -def option_descriptor_to_desc_dict( - option_descriptor: Union[Descriptor, FieldDescriptor], - field: Union[FieldDescriptor, FieldDescriptorProto], - type_name: str, - full_name: str, -) -> FieldInfoTypedDict: - """Parse the data of option and store it in dict. - Since array and map are supported, the complexity is relatively high - - The field names used by validator need to be prefixed with type_name, such as timestamp_gt, - while the built-in validation of Pydantic V2 does not require type_name prefix, such as gt - """ - - def get_column_list(column_descriptor: Union[Descriptor, FieldDescriptor]) -> List[str]: - if hasattr(column_descriptor, "ListFields"): - column_list = [column[0].name for column in column_descriptor.ListFields()] # type: ignore - else: - column_list = [ - column - for column in column_descriptor.__dir__() - if _has_raw_message_field(column, column_descriptor) # type: ignore - ] - return column_list - - def get_field_dict(column_list: List[str], field_descriptor: Union[Descriptor, FieldDescriptor]) -> Dict[str, Any]: - field_dict = {} - for column in column_list: - value = getattr(field_descriptor, column) - field_dict[column] = value - return field_dict - - def sub_value_handler(column_value: Any) -> Dict[str, Any]: - sub_type_name = sub_type_name_handler(column_value) - _descriptor = getattr(column_value, sub_type_name) - return get_field_dict(get_column_list(_descriptor), _descriptor) - - def sub_type_name_handler(column_value: Any) -> str: - sub_type_name = column_value.ListFields()[0][0].full_name.split(".")[-1] - return sub_type_name - - def rule_value_to_field_value_handler(column_type_name: str, column_name: str, column_value: Any) -> Any: - return replace_protobuf_type_to_python_type(column_value) - - # print(get_column_list(option_descriptor), file=sys.stderr) - # print(get_field_dict(get_column_list(option_descriptor), option_descriptor), file=sys.stderr) - return field_dict_handler( - get_field_dict(get_column_list(option_descriptor), option_descriptor), - field_name=field.name, - field_type=field.type, - type_name=type_name, - full_name=full_name, - sub_value_handler=sub_value_handler, - sub_type_name_handler=sub_type_name_handler, - rule_value_to_field_value_handler=rule_value_to_field_value_handler, - ) - - -def field_option_handle( - type_name: str, - full_name: str, - field: Union[FieldDescriptor, FieldDescriptorProto], - protobuf_pkg: str = "", -) -> FieldInfoTypedDict: - """Parse the information for each filed""" - field_dict: FieldInfoTypedDict = {"extra": {}, "skip": False} - required: bool = False - skip: bool = False - if isinstance(field, FieldDescriptor): - field_list = field.GetOptions().ListFields() - elif isinstance(field, FieldDescriptorProto): - field_list = field.options.ListFields() - else: - raise RuntimeError(f"Not support type:{field.type}") - for option_descriptor, option_value in field_list: - # filter unwanted Option - if protobuf_pkg: - if not option_descriptor.full_name.endswith(f"{protobuf_pkg}.rules"): - continue - elif not option_descriptor.full_name.endswith("validate.rules"): - continue - - rule_message: Any = option_value.message - if rule_message: - if getattr(rule_message, "skip", None): - skip = True - if getattr(rule_message, "required", None): - required = True - type_value: Optional[Descriptor] = getattr(option_value, type_name, None) - if not type_value: - continue - if not type_value.ListFields(): # type: ignore - type_value = getattr(option_value, "message", None) - if not type_value or not type_value.ListFields(): # type: ignore - _logger.warning(f"{__name__} Can not found {full_name}'s {type_name} from {option_value}") - - field_dict.update( - option_descriptor_to_desc_dict(type_value, field, type_name, full_name) # type:ignore - ) - - if required: - field_dict["required"] = required - if skip: - field_dict["skip"] = skip - return field_dict - - -_global_desc_dict: Dict[str, Dict[str, DescFromOptionTypedDict]] = {} - - -class ParseFromPbOption(object): - protobuf_pkg: str # Extend the package name of protobuf - - def __init__(self, message: Type[Message]): - self.message = message - self._msg_desc_dict: Dict[str, DescFromOptionTypedDict] = {} - if self.protobuf_pkg not in _global_desc_dict: - _global_desc_dict[self.protobuf_pkg] = self._msg_desc_dict - else: - self._msg_desc_dict = _global_desc_dict[self.protobuf_pkg] - - def parse(self) -> Dict[str, DescFromOptionTypedDict]: - descriptor: Descriptor = self.message.DESCRIPTOR - if descriptor.name in self._msg_desc_dict: - return self._msg_desc_dict - - self._msg_desc_dict[descriptor.name] = self.get_desc_from_options(descriptor) - return self._msg_desc_dict - - def get_desc_from_options(self, descriptor: Descriptor) -> DescFromOptionTypedDict: # noqa:C901 - """Extract the information of each field through the Options of Protobuf Message""" - if descriptor.name in self._msg_desc_dict: - return self._msg_desc_dict[descriptor.name] - message_field_dict: DescFromOptionTypedDict = {"message": {}, "one_of": {}, "nested": {}, "metadata": {}} - - # Options for processing Messages - for option_descriptor, option_value in descriptor.GetOptions().ListFields(): - # If parsing is disabled, Options will not continue to be parsed, and empty information will be set - if (option_descriptor.full_name == f"{self.protobuf_pkg}.disabled" and option_value) or ( - option_descriptor.full_name == f"{self.protobuf_pkg}.ignored" and option_value - ): - return message_field_dict - # Handle one_ofs of Message - one_of_dict: Dict[str, OneOfTypedDict] = {} - for one_of in descriptor.oneofs: - required: bool = False - optional_fields: Set[str] = set() - for one_of_descriptor, one_ov_value in one_of.GetOptions().ListFields(): - if one_of_descriptor.full_name == f"{self.protobuf_pkg}.required": - required = True - # if one_of.full_name in self._msg_desc_dict: - # continue - # Support one of is required - elif one_of_descriptor.full_name == f"{self.protobuf_pkg}.oneof_extend": - # Support one of is optional - for one_of_extend_field_descriptor, result in one_ov_value.ListFields(): - for one_of_optional_name in result: - optional_fields.add(one_of_optional_name) - - if required or optional_fields: - one_of_dict[one_of.full_name] = { - "required": required, - "optional_fields": optional_fields, - # optional cannot get the value of the current one of, - # so it needs to be processed in subsequent processing - "fields": set(), - } - if one_of_dict: - message_field_dict["one_of"] = one_of_dict - # Process all fields of Message - for field in descriptor.fields: - type_name: str = "" - if field.type == FieldDescriptor.TYPE_MESSAGE: - # Convert some types of Protobuf - message_type_name: str = field.message_type.name - if message_type_name in ("Duration", "Any", "Timestamp", "Struct"): - type_name = message_type_name.lower() - elif message_type_name.endswith("Entry"): - type_name = "map" - elif message_type_name == "Empty": - continue - else: - type_name = "message" - if field.label == FieldDescriptor.LABEL_REPEATED and not ( - field.message_type and field.message_type.name.endswith("Entry") - ): - # Support Protobuf.RepeatedXXX - type_name = "repeated" - if not type_name: - # Support Protobuf common type - type_name = protobuf_common_type_dict.get(field.type, "") - if not type_name: - _logger.warning( - f"{__name__} not support protobuf type id:{field.type} from field name{field.full_name}" - ) - continue - field_dict: FieldInfoTypedDict = field_option_handle(type_name, field.full_name, field, self.protobuf_pkg) - if field_dict["skip"]: - # If skip is True, the corresponding validation rule is not applied - continue - message_field_dict["message"][field.name] = field_dict - if type_name == "message": - message_field_dict["nested"][field.message_type.name] = self.get_desc_from_options(field.message_type) - elif type_name == "map": - for sub_field in field.message_type.fields: - if not sub_field.message_type: - continue - # keys and values - message_field_dict["nested"][sub_field.message_type.name] = self.get_desc_from_options( - sub_field.message_type - ) - self._msg_desc_dict[descriptor.name] = message_field_dict - return message_field_dict diff --git a/protobuf_to_pydantic/get_desc/from_pb_option/from_p2p.py b/protobuf_to_pydantic/get_desc/from_pb_option/from_p2p.py deleted file mode 100644 index 6399b75..0000000 --- a/protobuf_to_pydantic/get_desc/from_pb_option/from_p2p.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Dict, Type - -from protobuf_to_pydantic.grpc_types import Message - -from .base import DescFromOptionTypedDict, ParseFromPbOption - - -class _ParseFromPbOption(ParseFromPbOption): - protobuf_pkg = "p2p_validate" - - -def get_desc_from_p2p(message: Type[Message]) -> Dict[str, DescFromOptionTypedDict]: - """Parse data through Message and return info dict - Note: The returned dict includes the data of one of - """ - return _ParseFromPbOption(message).parse() diff --git a/protobuf_to_pydantic/get_desc/utils.py b/protobuf_to_pydantic/get_desc/utils.py deleted file mode 100644 index 042a917..0000000 --- a/protobuf_to_pydantic/get_desc/utils.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from protobuf_to_pydantic.types import DescFromOptionTypedDict - - -def one_of_message_dict_handler( - raw_message_dict: dict, desc_from_option_dict: "DescFromOptionTypedDict", full_name_prefix: str -) -> None: - for key, value in raw_message_dict.items(): - if key == "ignored": - desc_from_option_dict["metadata"]["ignored"] = value - elif key.startswith("oneof"): - # Special support for OneOf - field_full_name = f"{full_name_prefix}.{key.split(':')[1]}" - if field_full_name not in desc_from_option_dict["one_of"]: - desc_from_option_dict["one_of"][field_full_name] = {} # type: ignore - if "required" in value: - desc_from_option_dict["one_of"][field_full_name]["required"] = value["required"] - if "optional" in value.get("oneof_extend", {}): - desc_from_option_dict["one_of"][field_full_name]["optional_fields"] = set( - value["oneof_extend"].pop("optional", []) - ) diff --git a/protobuf_to_pydantic/get_message_option/__init__.py b/protobuf_to_pydantic/get_message_option/__init__.py new file mode 100644 index 0000000..478df9c --- /dev/null +++ b/protobuf_to_pydantic/get_message_option/__init__.py @@ -0,0 +1,6 @@ +from .from_message_option import ( + get_message_option_dict_from_message_with_p2p, + get_message_option_dict_from_message_with_pgv, +) +from .from_proto_file import get_message_option_dict_from_proto_file +from .from_pyi_file import get_message_option_dict_from_pyi_file diff --git a/protobuf_to_pydantic/get_message_option/from_message_option/__init__.py b/protobuf_to_pydantic/get_message_option/from_message_option/__init__.py new file mode 100644 index 0000000..3fc64fe --- /dev/null +++ b/protobuf_to_pydantic/get_message_option/from_message_option/__init__.py @@ -0,0 +1,4 @@ +from .from_p2p import get_message_option_dict_from_message_with_p2p +from .from_pgv import get_message_option_dict_from_message_with_pgv + +__all__ = ["get_message_option_dict_from_message_with_pgv", "get_message_option_dict_from_message_with_p2p"] diff --git a/protobuf_to_pydantic/get_message_option/from_message_option/base.py b/protobuf_to_pydantic/get_message_option/from_message_option/base.py new file mode 100644 index 0000000..63253c5 --- /dev/null +++ b/protobuf_to_pydantic/get_message_option/from_message_option/base.py @@ -0,0 +1,118 @@ +import logging +from typing import Dict, Set, Type + +from protobuf_to_pydantic.constant import protobuf_common_type_dict +from protobuf_to_pydantic.grpc_types import Descriptor, FieldDescriptor, Message +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.desc import gen_field_info_dict_from_field_desc +from protobuf_to_pydantic.types import MessageOptionTypedDict, OneOfTypedDict + +_logger: logging.Logger = logging.getLogger(__name__) + +_global_message_option_dict: Dict[str, Dict[str, MessageOptionTypedDict]] = {} + + +class ParseFromPbOption(object): + protobuf_pkg: str # Extend the package name of protobuf + + def __init__(self, message: Type[Message]): + self.message = message + self._message_option_dict: Dict[str, MessageOptionTypedDict] = {} + if self.protobuf_pkg not in _global_message_option_dict: + _global_message_option_dict[self.protobuf_pkg] = self._message_option_dict + else: + self._message_option_dict = _global_message_option_dict[self.protobuf_pkg] + + def parse(self) -> Dict[str, MessageOptionTypedDict]: + descriptor: Descriptor = self.message.DESCRIPTOR + if descriptor.name in self._message_option_dict: + return self._message_option_dict + + self._message_option_dict[descriptor.name] = self.get_message_option_dict_from_desc(descriptor) + return self._message_option_dict + + def get_message_option_dict_from_desc(self, descriptor: Descriptor) -> MessageOptionTypedDict: # noqa:C901 + """Extract the information of each field through the Options of Protobuf Message""" + if descriptor.name in self._message_option_dict: + return self._message_option_dict[descriptor.name] + message_option_dict: MessageOptionTypedDict = {"message": {}, "one_of": {}, "nested": {}, "metadata": {}} + + # Options for processing Messages + for option_descriptor, option_value in descriptor.GetOptions().ListFields(): + # If parsing is disabled, Options will not continue to be parsed, and empty information will be set + if (option_descriptor.full_name == f"{self.protobuf_pkg}.disabled" and option_value) or ( + option_descriptor.full_name == f"{self.protobuf_pkg}.ignored" and option_value + ): + return message_option_dict + # Handle one_ofs of Message + one_of_dict: Dict[str, OneOfTypedDict] = {} + for one_of in descriptor.oneofs: + required: bool = False + optional_fields: Set[str] = set() + for one_of_descriptor, one_ov_value in one_of.GetOptions().ListFields(): + if one_of_descriptor.full_name == f"{self.protobuf_pkg}.required": + required = True + # if one_of.full_name in self._msg_desc_dict: + # continue + # Support one of is required + elif one_of_descriptor.full_name == f"{self.protobuf_pkg}.oneof_extend": + # Support one of is optional + for one_of_extend_field_descriptor, result in one_ov_value.ListFields(): + for one_of_optional_name in result: + optional_fields.add(one_of_optional_name) + + if required or optional_fields: + one_of_dict[one_of.full_name] = { + "required": required, + "optional_fields": optional_fields, + # optional cannot get the value of the current one of, + # so it needs to be processed in subsequent processing + "fields": set(), + } + if one_of_dict: + message_option_dict["one_of"] = one_of_dict + # Process all fields of Message + for field in descriptor.fields: + type_name: str = "" + if field.type == FieldDescriptor.TYPE_MESSAGE: + # Convert some types of Protobuf + message_type_name: str = field.message_type.name + if message_type_name in ("Duration", "Any", "Timestamp", "Struct"): + type_name = message_type_name.lower() + elif message_type_name.endswith("Entry"): + type_name = "map" + elif message_type_name == "Empty": + continue + else: + type_name = "message" + if field.label == FieldDescriptor.LABEL_REPEATED and not ( + field.message_type and field.message_type.name.endswith("Entry") + ): + # Support Protobuf.RepeatedXXX + type_name = "repeated" + if not type_name: + # Support Protobuf common type + type_name = protobuf_common_type_dict.get(field.type, "") + if not type_name: + _logger.warning( + f"{__name__} not support protobuf type id:{field.type} from field name{field.full_name}" + ) + continue + field_info_dict = gen_field_info_dict_from_field_desc(type_name, field.full_name, field, self.protobuf_pkg) + if field_info_dict["skip"]: + # If skip is True, the corresponding validation rule is not applied + continue + message_option_dict["message"][field.name] = field_info_dict + if type_name == "message": + message_option_dict["nested"][field.message_type.name] = self.get_message_option_dict_from_desc( + field.message_type + ) + elif type_name == "map": + for sub_field in field.message_type.fields: + if not sub_field.message_type: + continue + # keys and values + message_option_dict["nested"][sub_field.message_type.name] = self.get_message_option_dict_from_desc( + sub_field.message_type + ) + self._message_option_dict[descriptor.name] = message_option_dict + return message_option_dict diff --git a/protobuf_to_pydantic/get_message_option/from_message_option/from_p2p.py b/protobuf_to_pydantic/get_message_option/from_message_option/from_p2p.py new file mode 100644 index 0000000..0e8852b --- /dev/null +++ b/protobuf_to_pydantic/get_message_option/from_message_option/from_p2p.py @@ -0,0 +1,13 @@ +from typing import Dict, Type + +from protobuf_to_pydantic.grpc_types import Message + +from .base import MessageOptionTypedDict, ParseFromPbOption + + +class _ParseFromPbOption(ParseFromPbOption): + protobuf_pkg = "p2p_validate" + + +def get_message_option_dict_from_message_with_p2p(message: Type[Message]) -> Dict[str, MessageOptionTypedDict]: + return _ParseFromPbOption(message).parse() diff --git a/protobuf_to_pydantic/get_desc/from_pb_option/from_pgv.py b/protobuf_to_pydantic/get_message_option/from_message_option/from_pgv.py similarity index 54% rename from protobuf_to_pydantic/get_desc/from_pb_option/from_pgv.py rename to protobuf_to_pydantic/get_message_option/from_message_option/from_pgv.py index 0d5e3b5..684f742 100644 --- a/protobuf_to_pydantic/get_desc/from_pb_option/from_pgv.py +++ b/protobuf_to_pydantic/get_message_option/from_message_option/from_pgv.py @@ -2,12 +2,12 @@ from protobuf_to_pydantic.grpc_types import Message -from .base import DescFromOptionTypedDict, ParseFromPbOption +from .base import MessageOptionTypedDict, ParseFromPbOption class _ParseFromPbOption(ParseFromPbOption): protobuf_pkg = "validate" -def get_desc_from_pgv(message: Type[Message]) -> Dict[str, DescFromOptionTypedDict]: +def get_message_option_dict_from_message_with_pgv(message: Type[Message]) -> Dict[str, MessageOptionTypedDict]: return _ParseFromPbOption(message).parse() diff --git a/protobuf_to_pydantic/get_desc/from_proto_file.py b/protobuf_to_pydantic/get_message_option/from_proto_file.py similarity index 67% rename from protobuf_to_pydantic/get_desc/from_proto_file.py rename to protobuf_to_pydantic/get_message_option/from_proto_file.py index f4ece4a..5889514 100644 --- a/protobuf_to_pydantic/get_desc/from_proto_file.py +++ b/protobuf_to_pydantic/get_message_option/from_proto_file.py @@ -1,54 +1,54 @@ from typing import TYPE_CHECKING, Dict, Optional -from protobuf_to_pydantic.util import gen_dict_from_desc_str +from protobuf_to_pydantic.util import get_dict_from_comment -from .utils import one_of_message_dict_handler +from .utils import rule_dict_handler if TYPE_CHECKING: from protobuf_to_pydantic.contrib.proto_parser import Message, ProtoFile - from protobuf_to_pydantic.types import DescFromOptionTypedDict + from protobuf_to_pydantic.types import MessageOptionTypedDict -_filename_desc_dict: Dict[str, Dict[str, "DescFromOptionTypedDict"]] = {} +_filename_desc_dict: Dict[str, Dict[str, "MessageOptionTypedDict"]] = {} -def _parse_message_result_dict( +def _protobuf_msg_handler( protobuf_msg: "Message", - parse_result: "ProtoFile", - container: Dict[str, "DescFromOptionTypedDict"], + proto_file: "ProtoFile", + container: Dict[str, "MessageOptionTypedDict"], comment_prefix: str, - msg_cache: Dict[str, "DescFromOptionTypedDict"], + msg_cache: Dict[str, "MessageOptionTypedDict"], ) -> None: message_name: str = protobuf_msg.name if message_name in msg_cache: return - new_container: "DescFromOptionTypedDict" = {"message": {}, "one_of": {}, "nested": {}, "metadata": {}} + new_container: "MessageOptionTypedDict" = {"message": {}, "one_of": {}, "nested": {}, "metadata": {}} container[message_name] = new_container msg_cache[message_name] = new_container if protobuf_msg.comment: - message_dict = gen_dict_from_desc_str( # type: ignore[assignment] + message_dict = get_dict_from_comment( # type: ignore[assignment] comment_prefix, protobuf_msg.comment.content.replace("//", "") ) - one_of_message_dict_handler(message_dict, new_container, f"{parse_result.package}.{message_name}") + rule_dict_handler(message_dict, new_container, f"{proto_file.package}.{message_name}") for field in protobuf_msg.fields: - new_container["message"][field.name] = gen_dict_from_desc_str( # type: ignore[assignment] + new_container["message"][field.name] = get_dict_from_comment( # type: ignore[assignment] comment_prefix, field.comment.content.replace("//", "") if field.comment else "" ) # parse nested message by map for sub_type_str in [field.type, field.key_type, field.val_type]: - if sub_type_str in parse_result.messages: - sub_message = parse_result.messages[sub_type_str] + if sub_type_str in proto_file.messages: + sub_message = proto_file.messages[sub_type_str] elif sub_type_str in protobuf_msg.messages: sub_message = protobuf_msg.messages[sub_type_str] else: continue if sub_message is protobuf_msg: continue - _parse_message_result_dict(sub_message, parse_result, new_container["nested"], comment_prefix, msg_cache) + _protobuf_msg_handler(sub_message, proto_file, new_container["nested"], comment_prefix, msg_cache) -def get_desc_from_proto_file(filename: str, comment_prefix: str) -> Dict[str, "DescFromOptionTypedDict"]: +def get_message_option_dict_from_proto_file(filename: str, comment_prefix: str) -> Dict[str, "MessageOptionTypedDict"]: """Obtain corresponding information through protobuf file protobuf file name: demo.proto, message e.g: @@ -95,14 +95,13 @@ def get_desc_from_proto_file(filename: str, comment_prefix: str) -> Dict[str, "D except ImportError: raise ImportError("Can not parse protobuf file, please install lark") - message_field_dict: Dict[str, "DescFromOptionTypedDict"] = {} + message_field_dict: Dict[str, "MessageOptionTypedDict"] = {} _proto_file: Optional[ProtoFile] = parse_from_file(filename) if _proto_file: # Currently only used protobuf file message - # proto_file: ProtoFile = _proto_file for _, protobuf_msg in _proto_file.messages.items(): - msg_cache: Dict[str, "DescFromOptionTypedDict"] = {} - _parse_message_result_dict(protobuf_msg, _proto_file, message_field_dict, comment_prefix, msg_cache) + msg_cache: Dict[str, "MessageOptionTypedDict"] = {} + _protobuf_msg_handler(protobuf_msg, _proto_file, message_field_dict, comment_prefix, msg_cache) # cache data and return _filename_desc_dict[filename] = message_field_dict return message_field_dict diff --git a/protobuf_to_pydantic/get_desc/from_pyi_file.py b/protobuf_to_pydantic/get_message_option/from_pyi_file.py similarity index 63% rename from protobuf_to_pydantic/get_desc/from_pyi_file.py rename to protobuf_to_pydantic/get_message_option/from_pyi_file.py index 61f2dce..ad20c32 100644 --- a/protobuf_to_pydantic/get_desc/from_pyi_file.py +++ b/protobuf_to_pydantic/get_message_option/from_pyi_file.py @@ -1,17 +1,17 @@ import re from typing import TYPE_CHECKING, Dict, List, Tuple -from protobuf_to_pydantic.util import gen_dict_from_desc_str +from protobuf_to_pydantic.util import get_dict_from_comment -from .utils import one_of_message_dict_handler +from .utils import rule_dict_handler if TYPE_CHECKING: - from protobuf_to_pydantic.types import DescFromOptionTypedDict, FieldInfoTypedDict + from protobuf_to_pydantic.types import FieldInfoTypedDict, MessageOptionTypedDict -_filename_desc_dict: Dict[str, Dict[str, "DescFromOptionTypedDict"]] = {} +_filename_message_option_dict: Dict[str, Dict[str, "MessageOptionTypedDict"]] = {} -def get_desc_from_pyi_file(filename: str, comment_prefix: str) -> Dict[str, "DescFromOptionTypedDict"]: +def get_message_option_dict_from_pyi_file(filename: str, comment_prefix: str) -> Dict[str, "MessageOptionTypedDict"]: """ For a Protobuf message as follows: ```protobuf @@ -73,9 +73,9 @@ class UserMessage(google.protobuf.message.Message): } } """ - if filename in _filename_desc_dict: + if filename in _filename_message_option_dict: # get protobuf message info by cache - return _filename_desc_dict[filename] + return _filename_message_option_dict[filename] with open(filename, "r") as f: pyi_content: str = f.read() @@ -84,44 +84,44 @@ class UserMessage(google.protobuf.message.Message): _comment_mode: bool = False # Whether to enable parsing comment mode _doc: str = "" _field_name: str = "" - message_str_stack: List[Tuple[str, int, DescFromOptionTypedDict]] = [] + message_str_stack: List[Tuple[str, int, MessageOptionTypedDict]] = [] indent: int = 0 - global_message_field_dict: Dict[str, "DescFromOptionTypedDict"] = {} + global_message_option_dict: Dict[str, "MessageOptionTypedDict"] = {} for index, line in enumerate(line_list): if "class" in line: if not line.endswith("google.protobuf.message.Message):"): continue - match_list = re.findall(r"class (.+)\(google.protobuf.message.Message", line) - if not match_list: + message_name_match_list = re.findall(r"class (.+)\(google.protobuf.message.Message", line) + if not message_name_match_list: continue - message_str: str = match_list[0] + message_name_str: str = message_name_match_list[0] new_indent: int = line.index("class") - if message_str_stack and message_str != message_str_stack[-1][0] and new_indent <= indent: + if message_str_stack and message_name_str != message_str_stack[-1][0] and new_indent <= indent: # When you encounter the same indentation of different classes, # need to pop off the previous one and insert the current one message_str_stack.pop() - message_field_dict: Dict[str, FieldInfoTypedDict] = {} - global_message_field_dict[message_str] = { - "message": message_field_dict, + message_field_option_dict: Dict[str, FieldInfoTypedDict] = {} + global_message_option_dict[message_name_str] = { + "message": message_field_option_dict, "one_of": {}, "nested": {}, # type: ignore "metadata": {}, } if message_str_stack: parent_message_field_dict = message_str_stack[-1][2] - parent_message_field_dict["nested"][message_str] = global_message_field_dict[message_str] + parent_message_field_dict["nested"][message_name_str] = global_message_option_dict[message_name_str] indent = new_indent - message_str_stack.append((message_str, indent, global_message_field_dict[message_str])) + message_str_stack.append((message_name_str, indent, global_message_option_dict[message_name_str])) elif indent: if line and message_str_stack and line[indent] != " ": # The current class has been scanned, go back to the previous class message_str_stack.pop() if message_str_stack: - message_str, indent, desc_dict = message_str_stack[-1] + message_name_str, indent, message_option_dict = message_str_stack[-1] line = line.strip() if _comment_mode: _doc += "\n" + line @@ -140,25 +140,11 @@ class UserMessage(google.protobuf.message.Message): if (line.endswith('"""') or line == '"""') and _comment_mode: # end add doc _comment_mode = False - gen_desc_dict = gen_dict_from_desc_str(comment_prefix, _doc.replace('"""', "")) + field_rule_dict = get_dict_from_comment(comment_prefix, _doc.replace('"""', "")) if _field_name: - desc_dict["message"][_field_name] = gen_desc_dict # type: ignore[assignment] + message_option_dict["message"][_field_name] = field_rule_dict # type: ignore[assignment] else: - one_of_message_dict_handler(gen_desc_dict, desc_dict, f"{message_str}") - # for key, value in gen_desc_dict.items(): - # if key == "ignore": - # desc_dict["metadata"]["ignore"] = value - # elif key.startswith("oneof"): - # # Special support for OneOf - # field_full_name = f"{message_str}.{key.split(':')[1]}" - # if field_full_name not in desc_dict["one_of"]: - # desc_dict["one_of"][field_full_name] = {} - # if "required" in value: - # desc_dict["one_of"][field_full_name]["required"] = value["required"] - # if "optional" in value.get("oneof_extend", {}): - # desc_dict["one_of"][field_full_name]["optional_fields"] = ( - # set(value["oneof_extend"].pop("optional", [])) - # ) - - _filename_desc_dict[filename] = global_message_field_dict - return global_message_field_dict + rule_dict_handler(field_rule_dict, message_option_dict, f"{message_name_str}") + + _filename_message_option_dict[filename] = global_message_option_dict + return global_message_option_dict diff --git a/protobuf_to_pydantic/get_message_option/utils.py b/protobuf_to_pydantic/get_message_option/utils.py new file mode 100644 index 0000000..de175b2 --- /dev/null +++ b/protobuf_to_pydantic/get_message_option/utils.py @@ -0,0 +1,29 @@ +from typing import TYPE_CHECKING, Any, Dict +from warnings import warn + +if TYPE_CHECKING: + from protobuf_to_pydantic.types import MessageOptionTypedDict + + +def rule_dict_handler( + rule_dict: Dict[str, Any], message_option_dict: "MessageOptionTypedDict", field_full_name_prefix: str +) -> None: + """ + parse message_rule_dict data and save to message_option_dict + """ + for key, value in rule_dict.items(): + if key == "ignored": + message_option_dict["metadata"]["ignored"] = value + elif key.startswith("oneof"): + # Special support for OneOf + field_full_name = f"{field_full_name_prefix}.{key.split(':')[1]}" + if field_full_name not in message_option_dict["one_of"]: + message_option_dict["one_of"][field_full_name] = {} # type: ignore + if "required" in value: + message_option_dict["one_of"][field_full_name]["required"] = value["required"] + if "optional" in value.get("oneof_extend", {}): + message_option_dict["one_of"][field_full_name]["optional_fields"] = set( + value["oneof_extend"].pop("optional", []) + ) + else: + warn(f"Message rule not support key:{key}") diff --git a/protobuf_to_pydantic/parse_rule/__init__.py b/protobuf_to_pydantic/parse_rule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/__init__.py b/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/base.py b/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/base.py new file mode 100644 index 0000000..4f2dde2 --- /dev/null +++ b/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/base.py @@ -0,0 +1,221 @@ +import inspect +import logging +from typing import Any, Callable, Dict, List, Optional, Set, Tuple + +from google.protobuf.descriptor import FieldDescriptor + +from protobuf_to_pydantic import _pydantic_adapter +from protobuf_to_pydantic.constant import protobuf_common_type_dict +from protobuf_to_pydantic.customer_con_type import ( + conbytes, + confloat, + conint, + conlist, + constr, + contimedelta, + contimestamp, +) +from protobuf_to_pydantic.customer_validator import validate_validator_dict +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import rule_name_pydantic_type_dict +from protobuf_to_pydantic.types import FieldInfoTypedDict + +logger: logging.Logger = logging.getLogger(__name__) + +pgv_column_to_pydantic_dict: Dict[str, str] = { + "min_len": "min_length", + "min_bytes": "min_length", + "max_len": "max_length", + "max_bytes": "max_length", + "pattern": "regex", + "unique": "unique_items", + "gte": "ge", + "lte": "le", + "len_bytes": "len", + "miss_default": "required", +} + +special_type_rule_name_set = { + "lt", + "le", + "gt", + "ge", + "const", + "in", + "not_in", + "lt_now", + "gt_now", + "within", + "min_pairs", + "max_pairs", +} + + +def get_con_type_func_from_type_name(type_name: str) -> Optional[Callable]: + if type_name == "string": + return constr + elif "double" in type_name or "float" in type_name: + return confloat + elif "int" in type_name: + return conint + elif type_name == "duration": + return contimedelta + elif type_name == "timestamp": + return contimestamp + elif type_name == "bytes": + return conbytes + else: + return None + + +class BaseProtobufOptionToFieldInfo(object): + @property + def rule_dict(self) -> FieldInfoTypedDict: + raise NotImplementedError + + def sub_value_handler(self, rule_value: Any) -> Dict[str, Any]: + raise NotImplementedError + + def sub_type_name_handler(self, rule_value: Any) -> str: + raise NotImplementedError + + def rule_value_to_field_value_handler(self, field_type_name: str, rule_name: str, rule_value: Any) -> Any: + raise NotImplementedError + + def value_type_conversion_handler(self, field_type_name: str, rule_name: str, rule_value: Any) -> Tuple[bool, Any]: + return False, rule_value + + def _core_handler( + self, + rule_dict: Dict[str, Any], + *, + field_name: str, + field_type: int, + type_name: str, + full_name: str, + ) -> FieldInfoTypedDict: + """ + Adapt some Field verification information to convert it into verification information that is compatible with + Protobuf's special type of pydantic + """ + field_info_type_dict: FieldInfoTypedDict = {"extra": {}, "skip": False} + for rule_name, rule_value in rule_dict.items(): + if rule_name in type_not_support_dict.get(field_type, type_not_support_dict["Any"]): + # Exclude unsupported fields + if field_type in protobuf_common_type_dict: + rule_name = f"{protobuf_common_type_dict[field_type]}.{rule_name}" + elif field_type == 1: + rule_name = f"Message.{rule_name}" + + msg: str = f"{__name__} not support `{rule_name}` rule." + if full_name: + msg = msg + f"(field:{full_name})" + logger.warning(msg) + continue + is_change, new_rule_value = self.value_type_conversion_handler(type_name, rule_name, rule_value) + if is_change: + rule_value = new_rule_value + + if rule_name in pgv_column_to_pydantic_dict: + # Field Conversion + rule_name = pgv_column_to_pydantic_dict[rule_name] + + if type_name in ("duration", "any", "timestamp", "map") and rule_name in special_type_rule_name_set: + # The verification of these parameters is handed over to the validator, + # see protobuf_to_pydantic/customer_validator for details + + # Types of priority treatment for special cases + if "validator" not in field_info_type_dict: + field_info_type_dict["validator"] = {} + + _rule_name: str = f"{type_name}_{rule_name}" + validator_name = f"{field_name}_{_rule_name}_validator" + + field_info_type_dict["extra"][_rule_name] = self.rule_value_to_field_value_handler( + type_name, rule_name, rule_value + ) + field_info_type_dict["validator"][validator_name] = _pydantic_adapter.field_validator( + field_name, allow_reuse=True + )(validate_validator_dict[f"{_rule_name}_validator"]) + continue + elif rule_name in ("in", "not_in", "len", "prefix", "suffix", "contains", "not_contains"): + # The verification of these parameters is handed over to the validator, + # see protobuf_to_pydantic/customer_validator for details + + # Compatible with PGV attributes that are not supported by pydantic + if "validator" not in field_info_type_dict: + field_info_type_dict["validator"] = {} + _rule_name = rule_name + "_" if rule_name in ("in",) else rule_name + validator_name = f"{field_name}_{_rule_name}_validator" + field_info_type_dict["extra"][_rule_name] = self.rule_value_to_field_value_handler( + type_name, rule_name, rule_value + ) + field_info_type_dict["validator"][validator_name] = _pydantic_adapter.field_validator( + field_name, allow_reuse=True + )(validate_validator_dict[f"{rule_name}_validator"]) + continue + elif rule_name in rule_name_pydantic_type_dict: + # Support some built-in type judgments of PGV + field_info_type_dict["type"] = rule_name_pydantic_type_dict[rule_name] + continue + elif rule_name in ("keys", "values"): + # Parse the field data of the key and value in the map + type_name = self.sub_type_name_handler(rule_value) + # Nested types are supported via like constr + + con_type = get_con_type_func_from_type_name(type_name) + if not con_type: + # TODO nested message + logger.warning(f"{__name__} not support sub type `{type_name}`, please reset {full_name}") + continue + if "map_type" not in field_info_type_dict: + field_info_type_dict["map_type"] = {} + con_type_param_dict: dict = {} + # Generate information corresponding to the nested type + sub_dict = self._core_handler( + self.sub_value_handler(rule_value), + field_name=field_name, + field_type=field_type, + type_name=type_name, + full_name=full_name, + ) + for _key in inspect.signature(con_type).parameters.keys(): + param_value = sub_dict.get(_key, None) # type: ignore + if param_value is not None: + con_type_param_dict[_key] = param_value + elif "extra" in sub_dict: + if sub_dict["extra"].get(_key, None) is not None: + con_type_param_dict[_key] = sub_dict["extra"][_key] + + field_info_type_dict["map_type"][rule_name] = con_type(**con_type_param_dict) + elif rule_name == "items": + # Process array data + type_name = self.sub_type_name_handler(rule_value) + # Nested types are supported via like constr + con_type = get_con_type_func_from_type_name(type_name) + if not con_type: + # TODO nested message + logger.warning(f"{__name__} not support sub type `{type_name}`, please reset {full_name}") + field_info_type_dict["type"] = List + continue + sub_dict = self._core_handler( + self.sub_value_handler(rule_value), + field_name=field_name, + field_type=field_type, + type_name=type_name, + full_name=full_name, + ) + if "type" not in sub_dict: + sub_dict["type"] = con_type + + field_info_type_dict["type"] = conlist + field_info_type_dict["sub"] = sub_dict + + field_info_type_dict[rule_name] = rule_value # type: ignore + return field_info_type_dict + + +type_not_support_dict: Dict[Any, Set[str]] = { + FieldDescriptor.TYPE_BYTES: {"pattern"}, + FieldDescriptor.TYPE_STRING: {"min_bytes", "max_bytes", "well_known_regex", "strict"}, + "Any": {"ignore_empty", "defined_only", "no_sparse"}, +} diff --git a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/comment.py b/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/comment.py new file mode 100644 index 0000000..85b8a0f --- /dev/null +++ b/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/comment.py @@ -0,0 +1,111 @@ +from datetime import timedelta +from typing import Any, Dict, Optional, Tuple, Union + +from protobuf_to_pydantic.grpc_types import FieldDescriptor, FieldDescriptorProto, Timestamp +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.base import ( + BaseProtobufOptionToFieldInfo, + special_type_rule_name_set, +) +from protobuf_to_pydantic.types import FieldInfoTypedDict + + +class ProtobufOptionToFieldInfoWithCommentDict(BaseProtobufOptionToFieldInfo): + """Parse the data of option and store it in dict. + Since array and map are supported, the complexity is relatively high + + The field names used by validator need to be prefixed with type_name, such as timestamp_gt, + while the built-in validation of Pydantic V2 does not require type_name prefix, such as gt + """ + + def __init__( + self, + rule_dict: Dict[str, Any], + field: Union[FieldDescriptor, FieldDescriptorProto], + type_name: str, + full_name: str, + ): + self._rule_dict = self._core_handler( + rule_dict=rule_dict, + field_name=field.name, + field_type=field.type, + type_name=type_name, + full_name=full_name, + ) + + @property + def rule_dict(self) -> FieldInfoTypedDict: + return self._rule_dict + + @staticmethod + def _duration_handler(_value: Any) -> Optional[timedelta]: + if isinstance(_value, timedelta): + return _value + elif not isinstance(_value, dict): + return _value + seconds = _value.get("seconds", 0) + nanos = _value.get("nanos", 0) + if nanos: + nanos = nanos / 1000 + if not seconds and not nanos: + return None + return timedelta(seconds=seconds, microseconds=nanos) + + def sub_value_handler(self, rule_value: Any) -> Dict[str, Any]: + sub_type_name = self.sub_type_name_handler(rule_value) + return rule_value[sub_type_name] + + def sub_type_name_handler(self, rule_value: Any) -> str: + sub_type_name = list(rule_value.keys())[0] + return sub_type_name + + def rule_value_to_field_value_handler(self, field_type_name: str, rule_name: str, rule_value: Any) -> Any: + if isinstance(rule_value, list): + return [self.rule_value_to_field_value_handler(field_type_name, rule_name, i) for i in rule_value] + elif field_type_name == "duration": + return self._duration_handler(rule_value) + elif field_type_name == "timestamp": + if rule_name in ("const", "gt", "lt", "ge", "le") and isinstance(rule_value, dict): + return Timestamp(**rule_value) + elif rule_name in ("within",): + return self._duration_handler(rule_value) + return rule_value + + def value_type_conversion_handler(self, field_type_name: str, rule_name: str, rule_value: Any) -> Tuple[bool, Any]: + if field_type_name in ("double", "float") and isinstance(rule_value, (float, int)): + return True, float(rule_value) + elif field_type_name == "bytes" and isinstance(rule_value, str): + return True, rule_value.encode("utf-8") + elif isinstance(rule_value, list): + new_value = [] + is_change: bool = False + for i in rule_value: + _is_change, i = self.value_type_conversion_handler(field_type_name, rule_name, i) + new_value.append(i) + if _is_change: + is_change = _is_change + if is_change: + return is_change, new_value + return is_change, rule_value + elif rule_name not in special_type_rule_name_set: + if field_type_name == "duration" and isinstance(rule_value, dict): + new_column_value = self._duration_handler(rule_value) + if new_column_value: + return True, new_column_value + return False, rule_value + elif field_type_name == "timestamp" and isinstance(rule_value, dict): + if "seconds" in rule_value or "nanos" in rule_value: + return True, Timestamp(**rule_value) + else: + return False, rule_value + return False, rule_value + + +def gen_field_rule_info_dict_from_field_comment_dict( # noqa: C901 + rule_dict: Dict[str, Any], + field: Union[FieldDescriptor, FieldDescriptorProto], + type_name: str, + full_name: str, +) -> FieldInfoTypedDict: + return ProtobufOptionToFieldInfoWithCommentDict( + rule_dict=rule_dict, field=field, type_name=type_name, full_name=full_name + ).rule_dict diff --git a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/desc.py b/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/desc.py new file mode 100644 index 0000000..e584306 --- /dev/null +++ b/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/desc.py @@ -0,0 +1,134 @@ +import logging +from typing import Any, Dict, List, Optional, Union + +from google.protobuf.descriptor import Descriptor, FieldDescriptor +from google.protobuf.descriptor_pb2 import FieldDescriptorProto +from google.protobuf.message import Message + +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.base import BaseProtobufOptionToFieldInfo +from protobuf_to_pydantic.types import FieldInfoTypedDict +from protobuf_to_pydantic.util import replace_protobuf_type_to_python_type + +logger: logging.Logger = logging.getLogger(__name__) + + +def _has_raw_message_field(field_name: str, message: Message) -> bool: + """Check if the field is valid in Message(or field is set in Message)""" + if field_name.startswith("_"): + return False + if getattr(Message, field_name, None): + return False + try: + if not message.HasField(field_name): + return False + except ValueError: + if not getattr(message, field_name, None) or field_name[0].lower() != field_name[0]: + return False + return True + + +class ProtobufOptionToFieldInfoWithFieldDesc(BaseProtobufOptionToFieldInfo): + """Parse the data of option and store it in dict. + Since array and map are supported, the complexity is relatively high + + The field names used by validator need to be prefixed with type_name, such as timestamp_gt, + while the built-in validation of Pydantic V2 does not require type_name prefix, such as gt + """ + + def __init__( + self, + field_descriptor: Union[Descriptor, FieldDescriptor], + field: Union[FieldDescriptor, FieldDescriptorProto], + type_name: str, + full_name: str, + ): + self._rule_dict = self._core_handler( + self.get_rule_dict(self.get_rule_name_list(field_descriptor), field_descriptor), + field_name=field.name, + field_type=field.type, + type_name=type_name, + full_name=full_name, + ) + + def get_rule_name_list(self, rule_descriptor: Union[Descriptor, FieldDescriptor]) -> List[str]: + if hasattr(rule_descriptor, "ListFields"): + _rule_name_list = [rule_field[0].name for rule_field in rule_descriptor.ListFields()] # type: ignore + else: + _rule_name_list = [ + rule_name + for rule_name in rule_descriptor.__dir__() + if _has_raw_message_field(rule_name, rule_descriptor) # type: ignore + ] + return _rule_name_list + + def get_rule_dict( + self, rule_name_list: List[str], _field_descriptor: Union[Descriptor, FieldDescriptor] + ) -> Dict[str, Any]: + return {_rule_name: getattr(_field_descriptor, _rule_name) for _rule_name in rule_name_list} + + @property + def rule_dict(self) -> FieldInfoTypedDict: + return self._rule_dict + + def sub_value_handler(self, rule_value: Any) -> Dict[str, Any]: + sub_type_name = self.sub_type_name_handler(rule_value) + _descriptor = getattr(rule_value, sub_type_name) + return self.get_rule_dict(self.get_rule_name_list(_descriptor), _descriptor) + + def sub_type_name_handler(self, rule_value: Any) -> str: + sub_type_name = rule_value.ListFields()[0][0].full_name.split(".")[-1] + return sub_type_name + + def rule_value_to_field_value_handler(self, field_type_name: str, rule_name: str, rule_value: Any) -> Any: + return replace_protobuf_type_to_python_type(rule_value) + + +def gen_field_info_dict_from_field_desc( + type_name: str, + full_name: str, + field: Union[FieldDescriptor, FieldDescriptorProto], + protobuf_pkg: str = "", +) -> FieldInfoTypedDict: + """Parse the information for each filed + + :param type_name: field type name + :param full_name: field fullname + :param field: message field + :param protobuf_pkg: protobuf rule pkg name + """ + field_info_dict: FieldInfoTypedDict = {"extra": {}, "skip": False, "required": False} + + if isinstance(field, FieldDescriptor): + field_list = field.GetOptions().ListFields() + elif isinstance(field, FieldDescriptorProto): + field_list = field.options.ListFields() + else: + raise RuntimeError(f"Not support type:{field.type}") + + for option_descriptor, option_value in field_list: + # filter unwanted Option + if protobuf_pkg: + if not option_descriptor.full_name.endswith(f"{protobuf_pkg}.rules"): + continue + elif not option_descriptor.full_name.endswith("validate.rules"): + continue + + rule_message: Any = option_value.message + if rule_message: + if getattr(rule_message, "skip", None): + field_info_dict["skip"] = True + if getattr(rule_message, "required", None): + field_info_dict["required"] = True + type_value: Optional[Descriptor] = getattr(option_value, type_name, None) + if not type_value: + continue + if not type_value.ListFields(): # type: ignore + type_value = getattr(option_value, "message", None) + if not type_value or not type_value.ListFields(): # type: ignore + logger.warning(f"{__name__} Can not found {full_name}'s {type_name} from {option_value}") + + field_info_dict.update( + ProtobufOptionToFieldInfoWithFieldDesc(type_value, field, type_name, full_name).rule_dict # type: ignore + ) + + return field_info_dict diff --git a/protobuf_to_pydantic/get_desc/from_pb_option/types.py b/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/types.py similarity index 98% rename from protobuf_to_pydantic/get_desc/from_pb_option/types.py rename to protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/types.py index 9efd6dc..e64270f 100644 --- a/protobuf_to_pydantic/get_desc/from_pb_option/types.py +++ b/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/types.py @@ -122,7 +122,7 @@ def _validate( ) -column_pydantic_type_dict: Dict[str, Any] = { +rule_name_pydantic_type_dict: Dict[str, Any] = { "email": EmailStr, "hostname": HostNameStr, "ip": IPvAnyAddress, diff --git a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py index 8dcfeaa..8142060 100644 --- a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py +++ b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py @@ -11,7 +11,11 @@ from typing_extensions import NotRequired, TypedDict from protobuf_to_pydantic import _pydantic_adapter -from protobuf_to_pydantic.constant import protobuf_desc_python_type_dict, python_type_default_value_dict +from protobuf_to_pydantic.constant import ( + protobuf_common_type_dict, + protobuf_desc_python_type_dict, + python_type_default_value_dict, +) from protobuf_to_pydantic.exceptions import WaitingToCompleteException from protobuf_to_pydantic.field_param import ( FieldParamModel, @@ -19,11 +23,6 @@ field_param_dict_migration_v2_handler, ) from protobuf_to_pydantic.gen_code import BaseP2C -from protobuf_to_pydantic.get_desc.from_pb_option.base import ( - field_comment_handler, - field_option_handle, - protobuf_common_type_dict, -) from protobuf_to_pydantic.grpc_types import ( AnyMessage, DescriptorProto, @@ -31,9 +30,13 @@ FieldDescriptorProto, FileDescriptorProto, ) +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.comment import ( + gen_field_rule_info_dict_from_field_comment_dict, +) +from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.desc import gen_field_info_dict_from_field_desc from protobuf_to_pydantic.plugin.my_types import ProtobufTypeModel from protobuf_to_pydantic.template import CommentTemplate -from protobuf_to_pydantic.util import camel_to_snake, gen_dict_from_desc_str +from protobuf_to_pydantic.util import camel_to_snake, get_dict_from_comment if TYPE_CHECKING: from protobuf_to_pydantic.plugin.config import ConfigModel @@ -128,7 +131,7 @@ def _comment_handler(self, leading_comments: str, trailing_comments: str) -> Tup (trailing_comments_list, trailing_comments), ): for line in comments.split("\n"): - field_dict = gen_dict_from_desc_str(self.config.comment_prefix, line) + field_dict = get_dict_from_comment(self.config.comment_prefix, line) if not field_dict: container.append(line) else: @@ -361,11 +364,11 @@ def _message_field_handle( # if list(desc.options.ListFields()): if len(field.options.ListFields()) != 0 and rule_type_str: # protobuf option support - field_option_info_dict.update(field_option_handle(rule_type_str, field.name, field)) # type: ignore + field_option_info_dict.update(gen_field_info_dict_from_field_desc(rule_type_str, field.name, field)) # type: ignore field_option_info_dict = self._desc_template.handle_template_var(field_option_info_dict) elif field_option_info_dict: field_option_info_dict = self._desc_template.handle_template_var(field_option_info_dict) - field_option_info_dict = field_comment_handler( # type: ignore + field_option_info_dict = gen_field_rule_info_dict_from_field_comment_dict( # type: ignore field_option_info_dict, field, rule_type_str, field.name ) @@ -598,7 +601,7 @@ def _gen_one_of_dict( comment = self.source_code_info_by_scl.get(tuple(scl_prefix + [index])) if self.config.parse_comment and comment: for line in comment.leading_comments.split("\n"): - one_of_comment_dict = gen_dict_from_desc_str(self.config.comment_prefix, line) + one_of_comment_dict = get_dict_from_comment(self.config.comment_prefix, line) if not one_of_comment_dict: continue for one_of_comment_rule_name, one_of_comment_option_value in one_of_comment_dict.items(): diff --git a/protobuf_to_pydantic/types.py b/protobuf_to_pydantic/types.py index dbe580b..9ce305e 100644 --- a/protobuf_to_pydantic/types.py +++ b/protobuf_to_pydantic/types.py @@ -52,10 +52,10 @@ class FieldInfoTypedDict(TypedDict): regex: NotRequired[Optional[str]] -class DescFromOptionTypedDict(TypedDict): +class MessageOptionTypedDict(TypedDict): message: Dict[str, FieldInfoTypedDict] one_of: Dict[str, OneOfTypedDict] - nested: Dict[str, "DescFromOptionTypedDict"] + nested: Dict[str, "MessageOptionTypedDict"] metadata: Dict[str, Any] diff --git a/protobuf_to_pydantic/util.py b/protobuf_to_pydantic/util.py index 3a2f455..bed3835 100644 --- a/protobuf_to_pydantic/util.py +++ b/protobuf_to_pydantic/util.py @@ -79,10 +79,10 @@ def replace_protobuf_type_to_python_type(value: Any) -> Any: return value -def gen_dict_from_desc_str(comment_prefix: str, desc: str) -> dict: - pait_dict: dict = {} +def get_dict_from_comment(comment_prefix: str, comment: str) -> dict: + _dict: dict = {} try: - for line in desc.split("\n"): + for line in comment.split("\n"): if line.startswith("#"): line = line[1:] line = line.strip() @@ -90,22 +90,22 @@ def gen_dict_from_desc_str(comment_prefix: str, desc: str) -> dict: continue line = line.replace(f"{comment_prefix}:", "") for key, value in json.loads(line.replace("\\\\", "\\")).items(): - if not pait_dict.get(key): - pait_dict[key] = value + if not _dict.get(key): + _dict[key] = value else: - if not isinstance(value, type(pait_dict[key])): + if not isinstance(value, type(_dict[key])): raise TypeError(f"Two different types of values were detected for Key:{key}") elif isinstance(value, list): - pait_dict[key].extend(value) + _dict[key].extend(value) elif isinstance(value, dict): - pait_dict[key].update(value) + _dict[key].update(value) else: raise TypeError(f"A key:{key} that does not support merging has been detected") - if "miss_default" in pait_dict: - pait_dict["required"] = pait_dict.pop("miss_default") + if "miss_default" in _dict: + _dict["required"] = _dict.pop("miss_default") except Exception as e: - logging.warning(f"Can not gen dict by desc:{desc}, error: {e}") - return pait_dict # type: ignore + logging.warning(f"Can not gen dict by desc:{comment}, error: {e}") + return _dict # type: ignore def get_pyproject_content(pyproject_file_path: str) -> str: From 193d32d495c2ce89c044b2b54a23b91654c500f0 Mon Sep 17 00:00:00 2001 From: so1n Date: Tue, 30 Jul 2024 20:10:39 +0800 Subject: [PATCH 03/10] Refactor, move field_param to field_info_rule module --- .../demo_gen_code_by_p2p.py | 2 +- .../demo_gen_code_by_pgv.py | 2 +- .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 2 +- .../demo_pb2_by_pyi.py | 2 +- .../example_proto/validate/demo_p2p.py | 2 +- .../demo_gen_code_by_p2p.py | 2 +- .../demo_gen_code_by_pgv.py | 2 +- .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 2 +- .../demo_pb2_by_pyi.py | 2 +- .../example_proto/validate/demo_p2p.py | 2 +- .../proto_pydanticv1/demo_gen_code_by_p2p.py | 2 +- .../proto_pydanticv1/demo_gen_code_by_pgv.py | 2 +- .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 2 +- .../demo_pb2_by_pyi.py | 2 +- .../example_proto/validate/demo_p2p.py | 2 +- .../proto_pydanticv2/demo_gen_code_by_p2p.py | 2 +- .../proto_pydanticv2/demo_gen_code_by_pgv.py | 2 +- .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 2 +- .../demo_pb2_by_pyi.py | 2 +- .../example_proto/validate/demo_p2p.py | 2 +- .../customer_validator/rule.py | 2 +- protobuf_to_pydantic/customer_validator/v1.py | 2 +- protobuf_to_pydantic/customer_validator/v2.py | 2 +- .../__init__.py | 0 .../field_info_param.py} | 141 ++++++++++-------- .../protobuf_option_to_field_info/__init__.py | 0 .../protobuf_option_to_field_info/base.py | 4 +- .../protobuf_option_to_field_info/comment.py | 6 +- .../protobuf_option_to_field_info/desc.py | 4 +- .../protobuf_option_to_field_info/types.py | 0 .../{ => field_info_rule}/types.py | 0 protobuf_to_pydantic/gen_model.py | 22 +-- .../from_message_option/base.py | 4 +- .../get_message_option/from_proto_file.py | 2 +- .../get_message_option/from_pyi_file.py | 2 +- .../get_message_option/utils.py | 2 +- .../plugin/field_desc_proto_to_code.py | 22 +-- 45 files changed, 143 insertions(+), 128 deletions(-) rename protobuf_to_pydantic/{parse_rule => field_info_rule}/__init__.py (100%) rename protobuf_to_pydantic/{field_param.py => field_info_rule/field_info_param.py} (52%) rename protobuf_to_pydantic/{parse_rule => field_info_rule}/protobuf_option_to_field_info/__init__.py (100%) rename protobuf_to_pydantic/{parse_rule => field_info_rule}/protobuf_option_to_field_info/base.py (97%) rename protobuf_to_pydantic/{parse_rule => field_info_rule}/protobuf_option_to_field_info/comment.py (96%) rename protobuf_to_pydantic/{parse_rule => field_info_rule}/protobuf_option_to_field_info/desc.py (96%) rename protobuf_to_pydantic/{parse_rule => field_info_rule}/protobuf_option_to_field_info/types.py (100%) rename protobuf_to_pydantic/{ => field_info_rule}/types.py (100%) diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py index 653b583..2d6c4f7 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py @@ -42,7 +42,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py index e18bb5d..e92a6aa 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py @@ -39,7 +39,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py index 95ce0a6..276f94c 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py @@ -45,7 +45,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 697d76d..86238c2 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -45,7 +45,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index db4a682..cfad85c 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index db4a682..cfad85c 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py index b0a11bb..41ce8bb 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py @@ -44,7 +44,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py index 47e2151..bfe0fa6 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py @@ -44,7 +44,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py index 677b474..c0c6ba0 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py index b79cbba..59b669c 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py @@ -49,7 +49,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 0b5aa68..08fecbd 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -49,7 +49,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index 4ad72b8..73839fe 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -43,7 +43,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index 4ad72b8..73839fe 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -43,7 +43,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py index e760b05..b608d2f 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py @@ -47,7 +47,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_pydanticv1/demo_gen_code_by_p2p.py b/example/proto_pydanticv1/demo_gen_code_by_p2p.py index 3ad1fbd..0417150 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_p2p.py +++ b/example/proto_pydanticv1/demo_gen_code_by_p2p.py @@ -42,7 +42,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_pydanticv1/demo_gen_code_by_pgv.py b/example/proto_pydanticv1/demo_gen_code_by_pgv.py index 079c753..7df5e70 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_pgv.py +++ b/example/proto_pydanticv1/demo_gen_code_by_pgv.py @@ -39,7 +39,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py index eca2965..26704cf 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py @@ -45,7 +45,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 4161ae1..531690f 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -45,7 +45,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index 0d0f812..f72fa3b 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index 0d0f812..f72fa3b 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, Field, root_validator, validator from pydantic.networks import AnyUrl, EmailStr, IPvAnyAddress diff --git a/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py index bea3cc9..e9014db 100644 --- a/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py @@ -44,7 +44,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_pydanticv2/demo_gen_code_by_p2p.py b/example/proto_pydanticv2/demo_gen_code_by_p2p.py index 7e974c6..9b3b76a 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_p2p.py +++ b/example/proto_pydanticv2/demo_gen_code_by_p2p.py @@ -44,7 +44,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_pydanticv2/demo_gen_code_by_pgv.py b/example/proto_pydanticv2/demo_gen_code_by_pgv.py index b51af1b..07d6fb2 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_pgv.py +++ b/example/proto_pydanticv2/demo_gen_code_by_pgv.py @@ -41,7 +41,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py index 28fc635..910090d 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py @@ -49,7 +49,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py index f78397e..3e954f6 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -49,7 +49,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index b8c61ba..3b91c10 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -43,7 +43,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index b8c61ba..3b91c10 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -43,7 +43,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic.functional_validators import BeforeValidator diff --git a/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py index e8cc533..f1460e7 100644 --- a/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py @@ -47,7 +47,7 @@ timestamp_lt_validator, timestamp_within_validator, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import HostNameStr, UriRefStr from protobuf_to_pydantic.util import Timedelta diff --git a/protobuf_to_pydantic/customer_validator/rule.py b/protobuf_to_pydantic/customer_validator/rule.py index 60d7779..6b69dd8 100644 --- a/protobuf_to_pydantic/customer_validator/rule.py +++ b/protobuf_to_pydantic/customer_validator/rule.py @@ -3,7 +3,7 @@ from protobuf_to_pydantic.grpc_types import AnyMessage -from protobuf_to_pydantic.types import OneOfTypedDict # isort:skip +from protobuf_to_pydantic.field_info_rule.types import OneOfTypedDict # isort:skip def to_datetime(value: Any) -> Any: diff --git a/protobuf_to_pydantic/customer_validator/v1.py b/protobuf_to_pydantic/customer_validator/v1.py index c327d50..fed8b4d 100644 --- a/protobuf_to_pydantic/customer_validator/v1.py +++ b/protobuf_to_pydantic/customer_validator/v1.py @@ -5,7 +5,7 @@ from . import rule -from protobuf_to_pydantic.types import OneOfTypedDict # isort:skip +from protobuf_to_pydantic.field_info_rule.types import OneOfTypedDict # isort:skip ################ diff --git a/protobuf_to_pydantic/customer_validator/v2.py b/protobuf_to_pydantic/customer_validator/v2.py index c4dece2..630445a 100644 --- a/protobuf_to_pydantic/customer_validator/v2.py +++ b/protobuf_to_pydantic/customer_validator/v2.py @@ -8,7 +8,7 @@ from . import rule -from protobuf_to_pydantic.types import OneOfTypedDict # isort:skip +from protobuf_to_pydantic.field_info_rule.types import OneOfTypedDict # isort:skip ################ diff --git a/protobuf_to_pydantic/parse_rule/__init__.py b/protobuf_to_pydantic/field_info_rule/__init__.py similarity index 100% rename from protobuf_to_pydantic/parse_rule/__init__.py rename to protobuf_to_pydantic/field_info_rule/__init__.py diff --git a/protobuf_to_pydantic/field_param.py b/protobuf_to_pydantic/field_info_rule/field_info_param.py similarity index 52% rename from protobuf_to_pydantic/field_param.py rename to protobuf_to_pydantic/field_info_rule/field_info_param.py index 3c977cd..1218c11 100644 --- a/protobuf_to_pydantic/field_param.py +++ b/protobuf_to_pydantic/field_info_rule/field_info_param.py @@ -2,49 +2,55 @@ import warnings from dataclasses import MISSING from types import ModuleType -from typing import Any, Callable, Dict, Optional, Set, Type, Union +from typing import Any, Callable, Dict, List, Optional, Set, Type, Union from pydantic import BaseModel, Field from pydantic.fields import FieldInfo from typing_extensions import Literal, get_args, get_origin from protobuf_to_pydantic import _pydantic_adapter, constant -from protobuf_to_pydantic.types import JsonAndDict +from protobuf_to_pydantic.field_info_rule.types import JsonAndDict from protobuf_to_pydantic.util import check_dict_one_of # You can decide whether to generate the 'Field' parameter by modifying the 'field_param_set' field_param_set = set(inspect.signature(Field).parameters.keys()) +support_con_type_module_name: List[str] = [ + "pydantic.types", + "protobuf_to_pydantic.customer_con_type.v1", + "protobuf_to_pydantic.customer_con_type.v2", +] -def field_param_dict_migration_v2_handler(field_param_dict: Dict[str, Any], is_warnings: bool = True) -> None: + +def field_info_param_dict_migration_v2_handler(field_info_param_dict: Dict[str, Any], is_warnings: bool = True) -> None: json_schema_extra = {} - for k in list(field_param_dict.keys()): + for k in list(field_info_param_dict.keys()): new_k = constant.pydantic_field_v1_migration_v2_dict.get(k, k) if new_k != k: if new_k is None: if is_warnings: warnings.warn( - f"field param `{k}` is deprecated, " + f"field info param `{k}` is deprecated, " f"https://docs.pydantic.dev/latest/migration/#changes-to-pydanticfield" ) - field_param_dict.pop(k) + field_info_param_dict.pop(k) else: if is_warnings: warnings.warn( - f"field param `{k}` is deprecated, please use `{new_k}` instead," + f"field info param `{k}` is deprecated, please use `{new_k}` instead," f" https://docs.pydantic.dev/latest/migration/#changes-to-pydanticfield" ) - value = field_param_dict.pop(k) + value = field_info_param_dict.pop(k) if value: - field_param_dict[new_k] = value + field_info_param_dict[new_k] = value else: if k not in field_param_set: - json_schema_extra[k] = field_param_dict.pop(k) - field_param_dict["json_schema_extra"] = json_schema_extra + json_schema_extra[k] = field_info_param_dict.pop(k) + field_info_param_dict["json_schema_extra"] = json_schema_extra -def field_param_dict_handle( - field_param_dict: dict, +def field_info_param_dict_handle( + field_info_param_dict: dict, default: Any, default_factory: Optional[_pydantic_adapter.NoArgAnyCallable], field_type: Optional[type] = None, @@ -53,7 +59,7 @@ def field_param_dict_handle( """ Convert the field_param_dict data to Pydantic Field parameters - :param field_param_dict: + :param field_info_param_dict: :param default: Pydantic Field default value :param default_factory: Pydantic Field default_factory value :param field_type: Pydantic Field type value @@ -65,72 +71,81 @@ def field_param_dict_handle( :return: """ # default_template support - if field_param_dict.get("default", None) is None and field_param_dict.get("default_template", None) is not None: - field_param_dict["default"] = field_param_dict["default_template"] - field_param_dict.pop("default_template", None) + if ( + field_info_param_dict.get("default", None) is None + and field_info_param_dict.get("default_template", None) is not None + ): + field_info_param_dict["default"] = field_info_param_dict["default_template"] + field_info_param_dict.pop("default_template", None) # Handle complex relationships with different defaults - check_dict_one_of(field_param_dict, ["required", "default", "default_factory"]) - if field_param_dict.get("default_factory", None) is not None: - field_param_dict.pop("default", "") - elif field_param_dict.get("default", None) is None: + check_dict_one_of(field_info_param_dict, ["required", "default", "default_factory"]) + if field_info_param_dict.get("default_factory", None) is not None: + field_info_param_dict.pop("default", "") + elif field_info_param_dict.get("default", None) is None: if default_factory: - field_param_dict["default_factory"] = default_factory - field_param_dict.pop("default", "") + field_info_param_dict["default_factory"] = default_factory + field_info_param_dict.pop("default", "") else: - field_param_dict["default"] = default - field_param_dict.pop("default_factory", None) - - if field_param_dict.get("required", None) is True: - field_param_dict.pop("default", "") - field_param_dict.pop("default_factory", "") - field_param_dict.pop("required", None) + field_info_param_dict["default"] = default + field_info_param_dict.pop("default_factory", None) - _const = field_param_dict.pop("const", MISSING) + # const handler + _const = field_info_param_dict.pop("const", MISSING) # PGV&P2P const handler if _const.__class__ != MISSING.__class__: if _pydantic_adapter.is_v1: - field_param_dict["default"] = _const - field_param_dict["const"] = True + field_info_param_dict["default"] = _const + field_info_param_dict["const"] = True else: # pydantic v2 not support const, only support Literal - field_param_dict["type_"] = Literal.__getitem__(_const) + field_info_param_dict["type_"] = Literal.__getitem__(_const) + + # required handler + if field_info_param_dict.get("required", None) is True: + _pop_default = field_info_param_dict.pop("default", "") + _pop_default_factory = field_info_param_dict.pop("default_factory", "") + if _pop_default or _pop_default_factory: + warnings.warn( + "if required is True," + " `default`, `default_factory`, `default_template` and `const` param values should not be set" + ) + + field_info_param_dict.pop("required", None) - if not _pydantic_adapter.is_v1 and field_param_dict.get("unique_items", None) is not None: + # unique handler + if not _pydantic_adapter.is_v1 and field_info_param_dict.get("unique_items", None) is not None: # In pydantic v2, not support unique_items # only use the Set type instead of this feature if not field_type or get_origin(field_type) != list: raise RuntimeError(f"unique_items only support type List (protobuf type: repeated) not {field_type}") - field_param_dict["type_"] = Set.__getitem__(get_args(field_type)) # type: ignore[index] - if field_param_dict["default_factory"]: - field_param_dict["default_factory"] = set - - # example handle - check_dict_one_of(field_param_dict, ["example", "example_factory"]) - if field_param_dict.get("example", MISSING).__class__ == MISSING.__class__: - field_param_dict.pop("example", None) - example_factory = field_param_dict.pop("example_factory", None) + # change type: list -> set + field_info_param_dict["type_"] = Set.__getitem__(get_args(field_type)) # type: ignore[misc] + if field_info_param_dict["default_factory"]: + field_info_param_dict["default_factory"] = set + + # example handler + check_dict_one_of(field_info_param_dict, ["example", "example_factory"]) + if field_info_param_dict.get("example", MISSING).__class__ == MISSING.__class__: + field_info_param_dict.pop("example", None) + example_factory = field_info_param_dict.pop("example_factory", None) if example_factory: - field_param_dict["example"] = example_factory + field_info_param_dict["example"] = example_factory - # extra handle - extra = field_param_dict.pop("extra", None) + # extra handler + extra = field_info_param_dict.pop("extra", None) if extra: - field_param_dict.update(extra) + field_info_param_dict.update(extra) - # type handle - field_type = field_param_dict.get("type_") - sub_field_param_dict: Optional[dict] = field_param_dict.pop("sub", None) + # type handler + field_type = field_info_param_dict.get("type_") + sub_field_param_dict: Optional[dict] = field_info_param_dict.pop("sub", None) field_type_model: Optional[ModuleType] = inspect.getmodule(field_type) - if not field_type: - return - if ( field_type and not inspect.isclass(field_type) and field_type_model - and field_type_model.__name__ - in ("pydantic.types", "protobuf_to_pydantic.customer_con_type.v1", "protobuf_to_pydantic.customer_con_type.v2") + and field_type_model.__name__ in support_con_type_module_name ): # support https://pydantic-docs.helpmanual.io/usage/types/#constrained-types # Parameters needed to extract `constrained-types` @@ -140,19 +155,19 @@ def field_param_dict_handle( # In pydantic v2 # If it is the first layer, all parameters need to be passed to Field, not Type for key in inspect.signature(field_type).parameters.keys(): - if key in field_param_dict: - type_param_dict[key] = field_param_dict.pop(key) + if key in field_info_param_dict: + type_param_dict[key] = field_info_param_dict.pop(key) if sub_field_param_dict and "type_" in sub_field_param_dict: # If a nested type is found, use the same treatment - field_param_dict_handle( + field_info_param_dict_handle( sub_field_param_dict, default, default_factory, nested_call_count=nested_call_count + 1 ) - field_param_dict["type_"] = field_type(sub_field_param_dict["type_"], **type_param_dict) + field_info_param_dict["type_"] = field_type(sub_field_param_dict["type_"], **type_param_dict) else: - field_param_dict["type_"] = field_type(**type_param_dict) + field_info_param_dict["type_"] = field_type(**type_param_dict) -class FieldParamModel(BaseModel): +class FieldInfoParamModel(BaseModel): field: Optional[Type[FieldInfo]] = Field(None) enable: bool = Field(True) required: bool = Field(False) @@ -179,5 +194,5 @@ class FieldParamModel(BaseModel): extra: JsonAndDict = Field(default_factory=dict) type_: Any = Field(None, alias="type") validator: Optional[Dict[str, Any]] = Field(None) - sub: Optional["FieldParamModel"] = Field(None) + sub: Optional["FieldInfoParamModel"] = Field(None) map_type: Optional[dict] = Field(None) diff --git a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/__init__.py b/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/__init__.py similarity index 100% rename from protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/__init__.py rename to protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/__init__.py diff --git a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/base.py b/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/base.py similarity index 97% rename from protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/base.py rename to protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/base.py index 4f2dde2..81e7591 100644 --- a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/base.py +++ b/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/base.py @@ -16,8 +16,8 @@ contimestamp, ) from protobuf_to_pydantic.customer_validator import validate_validator_dict -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.types import rule_name_pydantic_type_dict -from protobuf_to_pydantic.types import FieldInfoTypedDict +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.types import rule_name_pydantic_type_dict +from protobuf_to_pydantic.field_info_rule.types import FieldInfoTypedDict logger: logging.Logger = logging.getLogger(__name__) diff --git a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/comment.py b/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/comment.py similarity index 96% rename from protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/comment.py rename to protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/comment.py index 85b8a0f..01f0e3f 100644 --- a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/comment.py +++ b/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/comment.py @@ -1,12 +1,12 @@ from datetime import timedelta from typing import Any, Dict, Optional, Tuple, Union -from protobuf_to_pydantic.grpc_types import FieldDescriptor, FieldDescriptorProto, Timestamp -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.base import ( +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.base import ( BaseProtobufOptionToFieldInfo, special_type_rule_name_set, ) -from protobuf_to_pydantic.types import FieldInfoTypedDict +from protobuf_to_pydantic.field_info_rule.types import FieldInfoTypedDict +from protobuf_to_pydantic.grpc_types import FieldDescriptor, FieldDescriptorProto, Timestamp class ProtobufOptionToFieldInfoWithCommentDict(BaseProtobufOptionToFieldInfo): diff --git a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/desc.py b/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/desc.py similarity index 96% rename from protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/desc.py rename to protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/desc.py index e584306..30bd6c9 100644 --- a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/desc.py +++ b/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/desc.py @@ -5,8 +5,8 @@ from google.protobuf.descriptor_pb2 import FieldDescriptorProto from google.protobuf.message import Message -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.base import BaseProtobufOptionToFieldInfo -from protobuf_to_pydantic.types import FieldInfoTypedDict +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.base import BaseProtobufOptionToFieldInfo +from protobuf_to_pydantic.field_info_rule.types import FieldInfoTypedDict from protobuf_to_pydantic.util import replace_protobuf_type_to_python_type logger: logging.Logger = logging.getLogger(__name__) diff --git a/protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/types.py b/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/types.py similarity index 100% rename from protobuf_to_pydantic/parse_rule/protobuf_option_to_field_info/types.py rename to protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/types.py diff --git a/protobuf_to_pydantic/types.py b/protobuf_to_pydantic/field_info_rule/types.py similarity index 100% rename from protobuf_to_pydantic/types.py rename to protobuf_to_pydantic/field_info_rule/types.py diff --git a/protobuf_to_pydantic/gen_model.py b/protobuf_to_pydantic/gen_model.py index dedea41..9164cfa 100644 --- a/protobuf_to_pydantic/gen_model.py +++ b/protobuf_to_pydantic/gen_model.py @@ -15,10 +15,13 @@ from protobuf_to_pydantic.constant import protobuf_common_type_dict from protobuf_to_pydantic.customer_validator import check_one_of from protobuf_to_pydantic.exceptions import WaitingToCompleteException -from protobuf_to_pydantic.field_param import ( - FieldParamModel, - field_param_dict_handle, - field_param_dict_migration_v2_handler, +from protobuf_to_pydantic.field_info_rule.field_info_param import ( + FieldInfoParamModel, + field_info_param_dict_handle, + field_info_param_dict_migration_v2_handler, +) +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.comment import ( + gen_field_rule_info_dict_from_field_comment_dict, ) from protobuf_to_pydantic.get_message_option import ( get_message_option_dict_from_message_with_p2p, @@ -27,14 +30,11 @@ get_message_option_dict_from_pyi_file, ) from protobuf_to_pydantic.grpc_types import AnyMessage, Descriptor, FieldDescriptor, Message -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.comment import ( - gen_field_rule_info_dict_from_field_comment_dict, -) from protobuf_to_pydantic.template import CommentTemplate from protobuf_to_pydantic.util import create_pydantic_model if TYPE_CHECKING: - from protobuf_to_pydantic.types import FieldInfoTypedDict, MessageOptionTypedDict, UseOneOfTypedDict + from protobuf_to_pydantic.field_info_rule.types import FieldInfoTypedDict, MessageOptionTypedDict, UseOneOfTypedDict def replace_file_name_to_class_name(filename: str) -> str: @@ -448,7 +448,7 @@ def _gen_field_info(self, field_dataclass: FieldDataClass, skip_validate_rule: b type_name=field_dataclass.field_type_name, full_name=field_dataclass.protobuf_field.full_name, ) - field_param_dict: dict = FieldParamModel(**field_doc_dict).dict() # type: ignore + field_param_dict: dict = FieldInfoParamModel(**field_doc_dict).dict() # type: ignore # Nested types do not include the `enable`, `field` and `validator` attributes if not field_param_dict.pop("enable"): return None @@ -484,7 +484,7 @@ def _gen_field_info(self, field_dataclass: FieldDataClass, skip_validate_rule: b field_dataclass.validators.update(field_doc_dict["validator"]) # type: ignore[index] # Unified field parameter handling - field_param_dict_handle( + field_info_param_dict_handle( field_param_dict, field_dataclass.field_default, field_dataclass.field_default_factory, @@ -517,7 +517,7 @@ def _gen_field_info(self, field_dataclass: FieldDataClass, skip_validate_rule: b "default_factory": field_dataclass.field_default_factory, } if not _pydantic_adapter.is_v1: - field_param_dict_migration_v2_handler(field_param_dict) + field_info_param_dict_migration_v2_handler(field_param_dict) return field(**field_param_dict) # type: ignore def _parse_msg_to_pydantic_model( diff --git a/protobuf_to_pydantic/get_message_option/from_message_option/base.py b/protobuf_to_pydantic/get_message_option/from_message_option/base.py index 63253c5..2ecacfb 100644 --- a/protobuf_to_pydantic/get_message_option/from_message_option/base.py +++ b/protobuf_to_pydantic/get_message_option/from_message_option/base.py @@ -2,9 +2,9 @@ from typing import Dict, Set, Type from protobuf_to_pydantic.constant import protobuf_common_type_dict +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.desc import gen_field_info_dict_from_field_desc +from protobuf_to_pydantic.field_info_rule.types import MessageOptionTypedDict, OneOfTypedDict from protobuf_to_pydantic.grpc_types import Descriptor, FieldDescriptor, Message -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.desc import gen_field_info_dict_from_field_desc -from protobuf_to_pydantic.types import MessageOptionTypedDict, OneOfTypedDict _logger: logging.Logger = logging.getLogger(__name__) diff --git a/protobuf_to_pydantic/get_message_option/from_proto_file.py b/protobuf_to_pydantic/get_message_option/from_proto_file.py index 5889514..d04ab6f 100644 --- a/protobuf_to_pydantic/get_message_option/from_proto_file.py +++ b/protobuf_to_pydantic/get_message_option/from_proto_file.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from protobuf_to_pydantic.contrib.proto_parser import Message, ProtoFile - from protobuf_to_pydantic.types import MessageOptionTypedDict + from protobuf_to_pydantic.field_info_rule.types import MessageOptionTypedDict _filename_desc_dict: Dict[str, Dict[str, "MessageOptionTypedDict"]] = {} diff --git a/protobuf_to_pydantic/get_message_option/from_pyi_file.py b/protobuf_to_pydantic/get_message_option/from_pyi_file.py index ad20c32..968bd4f 100644 --- a/protobuf_to_pydantic/get_message_option/from_pyi_file.py +++ b/protobuf_to_pydantic/get_message_option/from_pyi_file.py @@ -6,7 +6,7 @@ from .utils import rule_dict_handler if TYPE_CHECKING: - from protobuf_to_pydantic.types import FieldInfoTypedDict, MessageOptionTypedDict + from protobuf_to_pydantic.field_info_rule.types import FieldInfoTypedDict, MessageOptionTypedDict _filename_message_option_dict: Dict[str, Dict[str, "MessageOptionTypedDict"]] = {} diff --git a/protobuf_to_pydantic/get_message_option/utils.py b/protobuf_to_pydantic/get_message_option/utils.py index de175b2..a369adf 100644 --- a/protobuf_to_pydantic/get_message_option/utils.py +++ b/protobuf_to_pydantic/get_message_option/utils.py @@ -2,7 +2,7 @@ from warnings import warn if TYPE_CHECKING: - from protobuf_to_pydantic.types import MessageOptionTypedDict + from protobuf_to_pydantic.field_info_rule.types import MessageOptionTypedDict def rule_dict_handler( diff --git a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py index 8142060..566ee78 100644 --- a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py +++ b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py @@ -17,11 +17,15 @@ python_type_default_value_dict, ) from protobuf_to_pydantic.exceptions import WaitingToCompleteException -from protobuf_to_pydantic.field_param import ( - FieldParamModel, - field_param_dict_handle, - field_param_dict_migration_v2_handler, +from protobuf_to_pydantic.field_info_rule.field_info_param import ( + FieldInfoParamModel, + field_info_param_dict_handle, + field_info_param_dict_migration_v2_handler, ) +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.comment import ( + gen_field_rule_info_dict_from_field_comment_dict, +) +from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.desc import gen_field_info_dict_from_field_desc from protobuf_to_pydantic.gen_code import BaseP2C from protobuf_to_pydantic.grpc_types import ( AnyMessage, @@ -30,10 +34,6 @@ FieldDescriptorProto, FileDescriptorProto, ) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.comment import ( - gen_field_rule_info_dict_from_field_comment_dict, -) -from protobuf_to_pydantic.parse_rule.protobuf_option_to_field_info.desc import gen_field_info_dict_from_field_desc from protobuf_to_pydantic.plugin.my_types import ProtobufTypeModel from protobuf_to_pydantic.template import CommentTemplate from protobuf_to_pydantic.util import camel_to_snake, get_dict_from_comment @@ -380,7 +380,7 @@ def _message_field_handle( if nested_message_name not in nested_message_config_dict: nested_message_config_dict[nested_message_name] = {} nested_message_config_dict[nested_message_name]["skip"] = skip - field_option_info_dict = FieldParamModel(**field_option_info_dict).dict() + field_option_info_dict = FieldInfoParamModel(**field_option_info_dict).dict() if not field_option_info_dict.pop("enable", False): return None try: @@ -389,7 +389,7 @@ def _message_field_handle( field_type = eval(type_str) except NameError: field_type = None - field_param_dict_handle( + field_info_param_dict_handle( field_option_info_dict, field_info_dict.get("default", _pydantic_adapter.PydanticUndefined), field_info_dict.get("default_factory", None), @@ -457,7 +457,7 @@ def _message_field_handle( if not _pydantic_adapter.is_v1: # pgv or p2p rule no warning required - field_param_dict_migration_v2_handler(field_info_dict, is_warnings=False) + field_info_param_dict_migration_v2_handler(field_info_dict, is_warnings=False) if optional_dict.get(field.name, {}).get("is_proto3_optional", False): self._add_import_code("typing") From f4954977fd14b20a479992a8fcbaf0c58d7c896c Mon Sep 17 00:00:00 2001 From: so1n Date: Sat, 3 Aug 2024 04:31:49 +0800 Subject: [PATCH 04/10] Refactor, gen model --- protobuf_to_pydantic/gen_model.py | 208 +++++++++++++++--------------- 1 file changed, 107 insertions(+), 101 deletions(-) diff --git a/protobuf_to_pydantic/gen_model.py b/protobuf_to_pydantic/gen_model.py index 9164cfa..e5b5233 100644 --- a/protobuf_to_pydantic/gen_model.py +++ b/protobuf_to_pydantic/gen_model.py @@ -36,6 +36,9 @@ if TYPE_CHECKING: from protobuf_to_pydantic.field_info_rule.types import FieldInfoTypedDict, MessageOptionTypedDict, UseOneOfTypedDict +SKIP_RULE_MESSAGE_SUFFIX = "WithSkipRule" +ALLOW_ARBITRARY_TYPE = (AnyMessage,) + def replace_file_name_to_class_name(filename: str) -> str: """Convert the protobuf file name to the class name(PEP-8)""" @@ -124,7 +127,7 @@ def __init__( create_model_cache: Optional[CREATE_MODEL_CACHE_T] = None, ): proto_file_name = msg.DESCRIPTOR.file.name # type: ignore - message_field_dict: Dict[str, "MessageOptionTypedDict"] = {} + global_message_option_dict: Dict[str, "MessageOptionTypedDict"] = {} if proto_file_name.endswith("empty.proto") or parse_msg_desc_method == "ignore": pass @@ -133,7 +136,9 @@ def __init__( file_str: str = parse_msg_desc_method if not file_str.endswith("/"): file_str += "/" - message_field_dict = get_message_option_dict_from_proto_file(file_str + proto_file_name, comment_prefix) + global_message_option_dict = get_message_option_dict_from_proto_file( + file_str + proto_file_name, comment_prefix + ) elif inspect.ismodule(parse_msg_desc_method): # get field dict from pyi file if getattr(parse_msg_desc_method, msg.__name__, None) is not msg: # type: ignore @@ -141,10 +146,10 @@ def __init__( pyi_file_name = parse_msg_desc_method.__file__ + "i" # type: ignore if not Path(pyi_file_name).exists(): raise RuntimeError(f"Can not found {msg} pyi file") - message_field_dict = get_message_option_dict_from_pyi_file(pyi_file_name, comment_prefix) + global_message_option_dict = get_message_option_dict_from_pyi_file(pyi_file_name, comment_prefix) elif parse_msg_desc_method == "PGV": # get field dict from pgv - message_field_dict = get_message_option_dict_from_message_with_pgv(message=msg) # type: ignore + global_message_option_dict = get_message_option_dict_from_message_with_pgv(message=msg) # type: ignore elif parse_msg_desc_method is not None: raise ValueError( f"parse_msg_desc_method param must be exist path, `ignore` or `PGV`," @@ -152,16 +157,16 @@ def __init__( ) else: # get field dict from p2p - message_field_dict = get_message_option_dict_from_message_with_p2p(message=msg) # type: ignore + global_message_option_dict = get_message_option_dict_from_message_with_p2p(message=msg) # type: ignore - self._parse_msg_desc_method: Optional[str] = parse_msg_desc_method - self._field_doc_dict: Dict[str, MessageOptionTypedDict] = message_field_dict + self._parse_msg_desc_method = parse_msg_desc_method + self._message_option_dict = global_message_option_dict self._default_field = default_field self._comment_prefix = comment_prefix self._creat_cache: CREATE_MODEL_CACHE_T = create_model_cache or _create_model_cache self._pydantic_base: Type["BaseModel"] = pydantic_base or BaseModel self._pydantic_module: str = pydantic_module or __name__ - self._desc_template: CommentTemplate = (desc_template or CommentTemplate)( + self._comment_template: CommentTemplate = (desc_template or CommentTemplate)( local_dict or {}, self._comment_prefix ) self._message_type_dict_by_type_name: Dict[str, Any] = ( @@ -198,29 +203,31 @@ def _get_field_info_dict_by_full_name(self, full_name: str) -> Optional["FieldIn else: # TODO Maybe fix the problem that multiple packages have the same message message_name, *key_list = split_full_name[1:] # ignore package name - if message_name not in self._field_doc_dict: + if message_name not in self._message_option_dict: return None - desc_dict: "MessageOptionTypedDict" = self._field_doc_dict[message_name] - if desc_dict["metadata"].get("ignore", False): + message_option_dict: "MessageOptionTypedDict" = self._message_option_dict[message_name] + if message_option_dict["metadata"].get("ignore", False): return None for key in key_list: - if key in desc_dict["message"]: - return desc_dict["message"][key] - elif key in desc_dict["nested"]: - desc_dict = desc_dict["nested"][key] - if desc_dict["metadata"].get("ignored", False): + if key in message_option_dict["message"]: + return message_option_dict["message"][key] + elif key in message_option_dict["nested"]: + message_option_dict = message_option_dict["nested"][key] + if message_option_dict["metadata"].get("ignored", False): return None else: return None return None def _one_of_handle(self, descriptor: Descriptor) -> Tuple[Dict[str, "UseOneOfTypedDict"], Dict[str, Any]]: - desc_dict: "MessageOptionTypedDict" = self._field_doc_dict.get(descriptor.name, {}) # type: ignore - ignore_parse_rule = desc_dict.get("metadata", {}).get("ignored", False) + message_option_dict: "MessageOptionTypedDict" = self._message_option_dict.get( + descriptor.name, {"message": {}, "one_of": {}, "nested": {}, "metadata": {}} + ) + ignore_parse_rule = message_option_dict.get("metadata", {}).get("ignored", False) one_of_desc_dict = {} if not ignore_parse_rule: - one_of_desc_dict = desc_dict.get("one_of", {}) + one_of_desc_dict = message_option_dict.get("one_of", {}) one_of_dict: Dict[str, "UseOneOfTypedDict"] = {} optional_dict: Dict[str, Any] = {} @@ -243,25 +250,25 @@ def _one_of_handle(self, descriptor: Descriptor) -> Tuple[Dict[str, "UseOneOfTyp optional_dict[field.full_name] = {"is_proto3_optional": True} for one_of in descriptor.oneofs: - column_name: str = one_of.full_name - if column_name in optional_id_set: + field_full_name: str = one_of.full_name + if field_full_name in optional_id_set: continue - if column_name not in one_of_dict: - one_of_dict[column_name] = {"required": False, "fields": set()} + if field_full_name not in one_of_dict: + one_of_dict[field_full_name] = {"required": False, "fields": set()} # pyi file not include pkg info - for found_column_name in [column_name, ".".join(column_name.split(".")[1:])]: + for found_column_name in [field_full_name, ".".join(field_full_name.split(".")[1:])]: if found_column_name not in one_of_desc_dict: continue # only PGV or P2P support - one_of_dict[column_name]["required"] = one_of_desc_dict[found_column_name].get("required", False) + one_of_dict[field_full_name]["required"] = one_of_desc_dict[found_column_name].get("required", False) optional_fields = one_of_desc_dict[found_column_name].get("optional_fields", set()) if optional_fields: for field_name in optional_fields: optional_dict[descriptor.full_name + "." + field_name] = {"is_proto3_optional": True} for _field in one_of.fields: - one_of_dict[column_name]["fields"].add(_field.name) + one_of_dict[field_full_name]["fields"].add(_field.name) return one_of_dict, optional_dict def _get_pydantic_base(self, config_dict: Dict[str, Any]) -> Type[BaseModel]: @@ -290,20 +297,12 @@ def get_nested_message_dict_by_message(self, descriptor: Descriptor) -> Dict[str continue nested_type: Any = self._parse_msg_to_pydantic_model(descriptor=message) nested_message_dict[message.full_name] = nested_type - # Facilitate the analysis of `gen code` - setattr(nested_type, "_is_nested", True) - # It is used to determine whether the field is used for these messages - setattr(nested_type, "_is_use", False) # enum support for enum_type in descriptor.enum_types: class_dict: dict = {v.name: v.number for v in enum_type.values} class_dict["__doc__"] = "" nested_type = IntEnum(enum_type.name, class_dict) # type: ignore nested_message_dict[enum_type.full_name] = nested_type - # Facilitate the analysis of `gen code` - setattr(nested_type, "_is_nested", True) - # It is used to determine whether the field is used for these messages - setattr(nested_type, "_is_use", False) return nested_message_dict #################### @@ -338,32 +337,31 @@ def _protobuf_field_type_is_type_message_handler(self, field_dataclass: FieldDat field_dataclass.field_default_factory = dict elif protobuf_field.message_type.file.name.startswith("google/protobuf/"): module_name = protobuf_field.message_type.file.name.split(".")[0].replace("/", ".") + "_pb2" - message_name = protobuf_field.message_type.name - type_factory = getattr(importlib.import_module(module_name), message_name) + type_factory = getattr(importlib.import_module(module_name), protobuf_field.message_type.name) field_dataclass.field_type = type_factory field_dataclass.field_default_factory = type_factory else: # support google.protobuf.Message - field_doc_dict: Union[FieldInfoTypedDict, dict] = ( + field_info_dict: Union[FieldInfoTypedDict, dict] = ( self._get_field_info_dict_by_full_name(field_dataclass.protobuf_field.full_name) or {} ) - skip_validate_rule = field_doc_dict.get("skip", False) + skip_validate_rule = field_info_dict.get("skip", False) full_name = protobuf_field.message_type.full_name - if protobuf_field.message_type.full_name in field_dataclass.nested_message_dict: - if skip_validate_rule: + if full_name in field_dataclass.nested_message_dict: + _cache_full_name = full_name + SKIP_RULE_MESSAGE_SUFFIX if skip_validate_rule else full_name + if _cache_full_name not in field_dataclass.nested_message_dict: # found and gen new message, finally, register to nested_message_dict nested_message = [i for i in field_dataclass.descriptor.nested_types if i.full_name == full_name][0] nested_type: Any = self._parse_msg_to_pydantic_model( descriptor=nested_message, - class_name=protobuf_field.message_type.name + "OnlyUseSkipRule", + class_name=protobuf_field.message_type.name + SKIP_RULE_MESSAGE_SUFFIX, skip_validate_rule=skip_validate_rule, ) - field_dataclass.nested_message_dict[full_name + "OnlyUseSkipRule"] = nested_type - setattr(nested_type, "_is_nested", True) - field_dataclass.field_type = nested_type - else: - field_dataclass.field_type = field_dataclass.nested_message_dict[full_name] - setattr(field_dataclass.field_type, "_is_use", True) + field_dataclass.nested_message_dict[_cache_full_name] = nested_type + + field_dataclass.field_type = field_dataclass.nested_message_dict[full_name] + # Facilitate the analysis of `gen code` + setattr(field_dataclass.field_type, "_is_nested", True) else: # Python Protobuf does not solve the namespace problem of modules, # so there is no uniform cross-module reference @@ -385,7 +383,6 @@ def _protobuf_field_type_is_type_message_handler(self, field_dataclass: FieldDat else: # if self-referencing, need use Python type hints postponed annotations field_dataclass.field_type = f'"{_class_name}"' - use_class_name = _class_name if not skip_validate_rule else _class_name + "OnlyUseSkipRule" if ( skip_validate_rule or protobuf_field.message_type.full_name != field_dataclass.descriptor.full_name @@ -393,7 +390,7 @@ def _protobuf_field_type_is_type_message_handler(self, field_dataclass: FieldDat try: field_dataclass.field_type = self._parse_msg_to_pydantic_model( descriptor=protobuf_field.message_type, - class_name=use_class_name, + class_name=_class_name if not skip_validate_rule else _class_name + "OnlyUseSkipRule", skip_validate_rule=skip_validate_rule, ) except WaitingToCompleteException: @@ -406,7 +403,8 @@ def _protobuf_field_type_is_type_enum_handler(self, field_dataclass: FieldDataCl field_dataclass.field_type_name = "enum" if protobuf_field.enum_type.full_name in field_dataclass.nested_message_dict: field_dataclass.field_type = field_dataclass.nested_message_dict[protobuf_field.enum_type.full_name] - setattr(field_dataclass.field_type, "_is_use", True) + # Facilitate the analysis of `gen code` + setattr(field_dataclass.field_type, "_is_nested", True) else: enum_class_dict = {v.name: v.number for v in protobuf_field.enum_type.values} _class_name = protobuf_field.enum_type.name @@ -432,76 +430,78 @@ def _protobuf_field_lable_is_label_repeated_handler(self, field_dataclass: Field if field_dataclass.field_default is not _pydantic_adapter.PydanticUndefined: field_dataclass.field_default = _pydantic_adapter.PydanticUndefined - def _gen_field_info(self, field_dataclass: FieldDataClass, skip_validate_rule: bool) -> Optional[FieldInfo]: - field = self._default_field - field_doc_dict = self._get_field_info_dict_by_full_name(field_dataclass.protobuf_field.full_name) + def _gen_field_info( + self, field_dataclass: FieldDataClass, skip_validate_rule: bool, is_proto3_optional: bool = False + ) -> Optional[FieldInfo]: + field_class = self._default_field + field_info_dict = self._get_field_info_dict_by_full_name(field_dataclass.protobuf_field.full_name) - if field_doc_dict is not None and not skip_validate_rule: + if field_info_dict is not None and not skip_validate_rule: if self._parse_msg_desc_method != "PGV": # pgv method not support template var - field_doc_dict = self._desc_template.handle_template_var(field_doc_dict) + field_info_dict = self._comment_template.handle_template_var(field_info_dict) if not (self._parse_msg_desc_method is None or self._parse_msg_desc_method == "PGV"): # comment rule need handler - field_doc_dict = gen_field_rule_info_dict_from_field_comment_dict( - field_doc_dict, # type:ignore[arg-type] + field_info_dict = gen_field_rule_info_dict_from_field_comment_dict( + field_info_dict, # type:ignore[arg-type] field=field_dataclass.protobuf_field, type_name=field_dataclass.field_type_name, full_name=field_dataclass.protobuf_field.full_name, ) - field_param_dict: dict = FieldInfoParamModel(**field_doc_dict).dict() # type: ignore + + raw_validator_dict = field_info_dict.get("validator", {}) + field_info_dict: FieldInfoTypedDict = FieldInfoParamModel(**field_info_dict).dict() # type: ignore # Nested types do not include the `enable`, `field` and `validator` attributes - if not field_param_dict.pop("enable"): + if not field_info_dict.pop("enable"): return None - _field = field_param_dict.pop("field") + _field = field_info_dict.pop("field") if _field: - field = _field - validator_dict = field_param_dict.pop("validator") + field_class = _field + validator_dict = field_info_dict.pop("validator") if validator_dict: - if _pydantic_adapter.is_v1: - field_dataclass.validators.update(validator_dict) - else: - # In Pydantic v2: - # field_doc_dict["validatos"] = { - # 'not_in_test_any_not_in_validator': PydanticDescriptorProxy( - # wrapped=, - # decorator_info=FieldValidatorDecoratorInfo(fields=('not_in_test',), - # mode='after', check_fields=None), - # shim=None - # ) - # } - # But validator_dict output: - # { - # 'not_in_test_any_not_in_validator': { - # 'wrapped': , - # 'decorator_info': { - # 'fields': ('not_in_test',), - # 'mode': 'after', - # 'check_fields': None - # }, - # 'shim': None - # } - # } - field_dataclass.validators.update(field_doc_dict["validator"]) # type: ignore[index] + # In Pydantic v2: + # field_doc_dict["validatos"] = { + # 'not_in_test_any_not_in_validator': PydanticDescriptorProxy( + # wrapped=, + # decorator_info=FieldValidatorDecoratorInfo(fields=('not_in_test',), + # mode='after', check_fields=None), + # shim=None + # ) + # } + # But validator_dict output: + # { + # 'not_in_test_any_not_in_validator': { + # 'wrapped': , + # 'decorator_info': { + # 'fields': ('not_in_test',), + # 'mode': 'after', + # 'check_fields': None + # }, + # 'shim': None + # } + # } + field_dataclass.validators.update(raw_validator_dict) # Unified field parameter handling field_info_param_dict_handle( - field_param_dict, + field_info_dict, field_dataclass.field_default, field_dataclass.field_default_factory, field_dataclass.field_type, ) # Type will change in the unified processing logic - field_type = field_param_dict.pop("type_", field_dataclass.field_type) - map_type_dict = field_param_dict.pop("map_type", {}) + field_type = field_info_dict.pop("type_", field_dataclass.field_type) + map_type_dict = field_info_dict.pop("map_type", {}) if field_type: field_dataclass.field_type = field_type elif map_type_dict and field_dataclass.field_type._name == "Dict": new_args_list: List = list(field_dataclass.field_type.__args__) for index, k_v_column in enumerate(["keys", "values"]): - raw_k_v_type = new_args_list[index] if k_v_column not in map_type_dict: continue + + raw_k_v_type = new_args_list[index] new_k_v_type = map_type_dict[k_v_column] if ( @@ -511,14 +511,21 @@ def _gen_field_info(self, field_dataclass: FieldDataClass, skip_validate_rule: b ): new_args_list[index] = new_k_v_type field_dataclass.field_type = Dict[tuple(new_args_list)] # type: ignore + if is_proto3_optional: + field_dataclass.field_type = Optional[field_dataclass.field_type] + if field_dataclass.field_default is _pydantic_adapter.PydanticUndefined and not field_info_dict.get( + "required", False + ): + field_dataclass.field_default = None else: - field_param_dict = { + field_info_dict = { "default": field_dataclass.field_default, "default_factory": field_dataclass.field_default_factory, + "extra": {}, } if not _pydantic_adapter.is_v1: - field_info_param_dict_migration_v2_handler(field_param_dict) - return field(**field_param_dict) # type: ignore + field_info_param_dict_migration_v2_handler(field_info_dict) + return field_class(**field_info_dict) # type: ignore def _parse_msg_to_pydantic_model( self, *, descriptor: Descriptor, class_name: str = "", skip_validate_rule: bool = False @@ -561,17 +568,16 @@ def _parse_msg_to_pydantic_model( # At this time, the field type may be modified by the above logic, so it needs to be handled separately if protobuf_field.label == FieldDescriptor.LABEL_REPEATED: self._protobuf_field_lable_is_label_repeated_handler(field_dataclass) - field_info = self._gen_field_info(field_dataclass, skip_validate_rule) + is_proto3_optional = optional_dict.get(protobuf_field.full_name, {}).get("is_proto3_optional", False) + field_info = self._gen_field_info( + field_dataclass, skip_validate_rule, is_proto3_optional=is_proto3_optional + ) if not field_info: continue - if optional_dict.get(protobuf_field.full_name, {}).get("is_proto3_optional", False): - field_dataclass.field_type = Optional[field_dataclass.field_type] - if field_dataclass.field_default is _pydantic_adapter.PydanticUndefined: - field_dataclass.field_default = None annotation_dict[field_dataclass.field_name] = (field_dataclass.field_type, field_info) - if field_dataclass.field_type in (AnyMessage,) and not _pydantic_adapter.get_model_config_value( + if field_dataclass.field_type in ALLOW_ARBITRARY_TYPE and not _pydantic_adapter.get_model_config_value( self._pydantic_base, "arbitrary_types_allowed" ): pydantic_model_config_dict["arbitrary_types_allowed"] = True @@ -607,7 +613,7 @@ def _parse_msg_to_pydantic_model( one_of_dict=one_of_dict, base_model=self._pydantic_base, # Facilitate the analysis of `gen code` - nested_message_dict={k: v for k, v in nested_message_dict.items() if getattr(v, "_is_use", False)}, + nested_message_dict={k: v for k, v in nested_message_dict.items() if getattr(v, "_is_nested", False)}, validators=validators, ) setattr(pydantic_model, "_one_of_dict", one_of_dict) From 9ff187e06d2b1cd897d971df2088f09d447c2bc1 Mon Sep 17 00:00:00 2001 From: so1n Date: Sun, 1 Sep 2024 21:39:14 +0800 Subject: [PATCH 05/10] Refactor, plugin --- .../demo_pb2_by_protobuf.py | 4 +- .../demo_pb2_by_pyi.py | 4 +- .../field_info_rule/field_info_param.py | 7 +- protobuf_to_pydantic/field_info_rule/types.py | 2 + protobuf_to_pydantic/gen_model.py | 20 +-- protobuf_to_pydantic/plugin/code_gen.py | 27 +-- .../plugin/field_desc_proto_to_code.py | 161 ++++++++---------- protobuf_to_pydantic/template/__init__.py | 8 +- 8 files changed, 111 insertions(+), 122 deletions(-) diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index 3b91c10..c8143a6 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -478,7 +478,7 @@ class UserPayMessage(BaseModel): timestamp_gt_now_validator ) - class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): + class NotEnableUserPayMessageWithSkipRule(BaseModel): bank_number: str = Field(default="") exp: datetime = Field(default_factory=datetime.now) uuid: str = Field(default="") @@ -486,7 +486,7 @@ class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): string_in_map_test: typing.Dict[str, StringTest] = Field(default_factory=dict) map_in_map_test: typing.Dict[str, MapTest] = Field(default_factory=dict) user_pay: UserPayMessage = Field() - not_enable_user_pay: NotEnableUserPayMessageOnlyUseSkipRule = Field() + not_enable_user_pay: NotEnableUserPayMessageWithSkipRule = Field() empty: typing.Any = Field() after_refer: AfterReferMessage = Field() diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index 3b91c10..c8143a6 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -478,7 +478,7 @@ class UserPayMessage(BaseModel): timestamp_gt_now_validator ) - class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): + class NotEnableUserPayMessageWithSkipRule(BaseModel): bank_number: str = Field(default="") exp: datetime = Field(default_factory=datetime.now) uuid: str = Field(default="") @@ -486,7 +486,7 @@ class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): string_in_map_test: typing.Dict[str, StringTest] = Field(default_factory=dict) map_in_map_test: typing.Dict[str, MapTest] = Field(default_factory=dict) user_pay: UserPayMessage = Field() - not_enable_user_pay: NotEnableUserPayMessageOnlyUseSkipRule = Field() + not_enable_user_pay: NotEnableUserPayMessageWithSkipRule = Field() empty: typing.Any = Field() after_refer: AfterReferMessage = Field() diff --git a/protobuf_to_pydantic/field_info_rule/field_info_param.py b/protobuf_to_pydantic/field_info_rule/field_info_param.py index 1218c11..5f2f4ce 100644 --- a/protobuf_to_pydantic/field_info_rule/field_info_param.py +++ b/protobuf_to_pydantic/field_info_rule/field_info_param.py @@ -9,7 +9,7 @@ from typing_extensions import Literal, get_args, get_origin from protobuf_to_pydantic import _pydantic_adapter, constant -from protobuf_to_pydantic.field_info_rule.types import JsonAndDict +from protobuf_to_pydantic.field_info_rule.types import FieldInfoTypedDict, JsonAndDict from protobuf_to_pydantic.util import check_dict_one_of # You can decide whether to generate the 'Field' parameter by modifying the 'field_param_set' @@ -192,7 +192,12 @@ class FieldInfoParamModel(BaseModel): multiple_of: Optional[int] = Field(None) regex: Optional[str] = Field(None) extra: JsonAndDict = Field(default_factory=dict) + json_schema_extra: JsonAndDict = Field(default_factory=dict) + skip: bool = Field(False) type_: Any = Field(None, alias="type") validator: Optional[Dict[str, Any]] = Field(None) sub: Optional["FieldInfoParamModel"] = Field(None) map_type: Optional[dict] = Field(None) + + def to_dict(self) -> FieldInfoTypedDict: + return self.dict() # type: ignore diff --git a/protobuf_to_pydantic/field_info_rule/types.py b/protobuf_to_pydantic/field_info_rule/types.py index 9ce305e..18a617a 100644 --- a/protobuf_to_pydantic/field_info_rule/types.py +++ b/protobuf_to_pydantic/field_info_rule/types.py @@ -22,6 +22,7 @@ class OneOfTypedDict(TypedDict): class FieldInfoTypedDict(TypedDict): extra: dict + json_schema_extra: NotRequired[dict] skip: NotRequired[bool] enable: NotRequired[bool] required: NotRequired[bool] @@ -50,6 +51,7 @@ class FieldInfoTypedDict(TypedDict): unique_items: NotRequired[Optional[bool]] multiple_of: NotRequired[Optional[int]] regex: NotRequired[Optional[str]] + type_: NotRequired[Any] class MessageOptionTypedDict(TypedDict): diff --git a/protobuf_to_pydantic/gen_model.py b/protobuf_to_pydantic/gen_model.py index e5b5233..5efe2be 100644 --- a/protobuf_to_pydantic/gen_model.py +++ b/protobuf_to_pydantic/gen_model.py @@ -358,8 +358,9 @@ def _protobuf_field_type_is_type_message_handler(self, field_dataclass: FieldDat skip_validate_rule=skip_validate_rule, ) field_dataclass.nested_message_dict[_cache_full_name] = nested_type - - field_dataclass.field_type = field_dataclass.nested_message_dict[full_name] + field_dataclass.field_type = field_dataclass.nested_message_dict[_cache_full_name] + else: + field_dataclass.field_type = field_dataclass.nested_message_dict[full_name] # Facilitate the analysis of `gen code` setattr(field_dataclass.field_type, "_is_nested", True) else: @@ -451,6 +452,7 @@ def _gen_field_info( raw_validator_dict = field_info_dict.get("validator", {}) field_info_dict: FieldInfoTypedDict = FieldInfoParamModel(**field_info_dict).dict() # type: ignore + field_info_dict.pop("skip") # Nested types do not include the `enable`, `field` and `validator` attributes if not field_info_dict.pop("enable"): return None @@ -484,7 +486,7 @@ def _gen_field_info( # Unified field parameter handling field_info_param_dict_handle( - field_info_dict, + field_info_dict, # type: ignore[arg-type] field_dataclass.field_default, field_dataclass.field_default_factory, field_dataclass.field_type, @@ -511,12 +513,6 @@ def _gen_field_info( ): new_args_list[index] = new_k_v_type field_dataclass.field_type = Dict[tuple(new_args_list)] # type: ignore - if is_proto3_optional: - field_dataclass.field_type = Optional[field_dataclass.field_type] - if field_dataclass.field_default is _pydantic_adapter.PydanticUndefined and not field_info_dict.get( - "required", False - ): - field_dataclass.field_default = None else: field_info_dict = { "default": field_dataclass.field_default, @@ -524,7 +520,7 @@ def _gen_field_info( "extra": {}, } if not _pydantic_adapter.is_v1: - field_info_param_dict_migration_v2_handler(field_info_dict) + field_info_param_dict_migration_v2_handler(field_info_dict) # type: ignore[arg-type] return field_class(**field_info_dict) # type: ignore def _parse_msg_to_pydantic_model( @@ -575,6 +571,10 @@ def _parse_msg_to_pydantic_model( if not field_info: continue + if is_proto3_optional: + field_dataclass.field_type = Optional[field_dataclass.field_type] + if field_info.default is _pydantic_adapter.PydanticUndefined: + field_info.default = None annotation_dict[field_dataclass.field_name] = (field_dataclass.field_type, field_info) if field_dataclass.field_type in ALLOW_ARBITRARY_TYPE and not _pydantic_adapter.get_model_config_value( diff --git a/protobuf_to_pydantic/plugin/code_gen.py b/protobuf_to_pydantic/plugin/code_gen.py index 5ee3c45..6ad7098 100644 --- a/protobuf_to_pydantic/plugin/code_gen.py +++ b/protobuf_to_pydantic/plugin/code_gen.py @@ -25,14 +25,14 @@ class CodeGen(Generic[ConfigT]): def __init__(self, config_class: Type[ConfigT]) -> None: self.config_class: Type[ConfigT] = config_class - self.param_dict: dict = {} with code_generation() as (request, response): - self.parse_param(request) + self.param_dict = self.gen_param_from_request(request) self.gen_config() self.generate_pydantic_model(Descriptors(request), response) - def parse_param(self, request: CodeGeneratorRequest) -> None: + @staticmethod + def gen_param_from_request(request: CodeGeneratorRequest) -> dict: """ Parse the parameter information passed by the user and store it in the 'param_dict'. @@ -42,16 +42,17 @@ def parse_param(self, request: CodeGeneratorRequest) -> None: param_dict result: {"bar1": "foo1", "bar2": "foo2"} """ - if not request.parameter: - return - try: - for one_param_str in request.parameter.split(","): - k, v = one_param_str.split("=") - self.param_dict[k] = v - except Exception as e: - logger.exception(e) - print(f"parse command-line error:{e}", file=sys.stderr) - print(f"Parse command-line arguments:{self.param_dict}", file=sys.stderr) + param_dict: dict = {} + if request.parameter: + try: + for one_param_str in request.parameter.split(","): + k, v = one_param_str.split("=") + param_dict[k] = v + except Exception as e: + logger.exception(e) + print(f"parse command-line error:{e}", file=sys.stderr) + print(f"Parse command-line arguments:{param_dict}", file=sys.stderr) + return param_dict def _get_config_by_path(self, key: str) -> None: """ diff --git a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py index 566ee78..d78ef18 100644 --- a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py +++ b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py @@ -3,12 +3,12 @@ import logging from datetime import datetime, timedelta from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Set, Tuple, Type from mypy_protobuf.main import PYTHON_RESERVED, Descriptors, SourceCodeLocation from pydantic import BaseModel from pydantic.fields import FieldInfo -from typing_extensions import NotRequired, TypedDict +from typing_extensions import TypedDict from protobuf_to_pydantic import _pydantic_adapter from protobuf_to_pydantic.constant import ( @@ -26,6 +26,7 @@ gen_field_rule_info_dict_from_field_comment_dict, ) from protobuf_to_pydantic.field_info_rule.protobuf_option_to_field_info.desc import gen_field_info_dict_from_field_desc +from protobuf_to_pydantic.field_info_rule.types import FieldInfoTypedDict, OneOfTypedDict from protobuf_to_pydantic.gen_code import BaseP2C from protobuf_to_pydantic.grpc_types import ( AnyMessage, @@ -35,7 +36,6 @@ FileDescriptorProto, ) from protobuf_to_pydantic.plugin.my_types import ProtobufTypeModel -from protobuf_to_pydantic.template import CommentTemplate from protobuf_to_pydantic.util import camel_to_snake, get_dict_from_comment if TYPE_CHECKING: @@ -52,11 +52,6 @@ class OptionTypedDict(TypedDict): is_proto3_optional: bool -class OneOfInfoTypedDict(TypedDict): - fields: Set[str] - required: NotRequired[bool] - - def remove_comment_last_n(content: str) -> str: return_content = "" if content.endswith("\n"): @@ -80,16 +75,16 @@ def __init__(self, fd: FileDescriptorProto, descriptors: Descriptors, config: "C pyproject_file_path=config.pyproject_file_path, ) self.config = config - self._fd: FileDescriptorProto = fd - self._descriptors: Descriptors = descriptors - self._desc_template: CommentTemplate = config.desc_template_instance + self._fd = fd + self._descriptors = descriptors + self._desc_template = config.desc_template_instance self.source_code_info_by_scl = {tuple(location.path): location for location in fd.source_code_info.location} if config.base_model_class is BaseModel: self._import_set.add("from pydantic import BaseModel") else: self._add_import_code(config.base_model_class.__module__, config.base_model_class.__name__) - self._parse_desc_name_dict: Dict[str, str] = {} + self._model_cache: Dict[str, str] = {} self._parse_field_descriptor() def _add_other_module_pkg(self, other_fd: FileDescriptorProto, type_str: str) -> None: @@ -234,7 +229,7 @@ def _message_field_handle( optional_dict: dict, scl_prefix: SourceCodeLocation, skip_validate_rule: bool = False, - ) -> Optional[Tuple[str, str]]: + ) -> Optional[Tuple[str, str, bool]]: """generate message's field to Pydantic.FieldInfo code :param desc: The message to which the field belongs is used to determine whether it is self-referencing @@ -249,10 +244,13 @@ def _message_field_handle( e.g: {"{field name}": {"is_proto3_optional": True}} :param skip_validate_rule: If the value is True, the validation information for the field will not be generated - :return: validator_handle_content, class_field_content + :return: validator_handle_content, class_field_content, use_custom_type """ - field_info_dict: dict = {} + field_info_default_value = _pydantic_adapter.PydanticUndefined + field_info_default_factory_value: Any = None + field_type = None nested_message_name: Optional[str] = None + use_custom_type = False raw_validator_dict = {} leading_comments = "" trailing_comments = "" @@ -275,18 +273,20 @@ def _message_field_handle( f"typing.Dict[{self._get_protobuf_type_model(key_msg).py_type_str}," f" {self._get_protobuf_type_model(value_msg).py_type_str}]" ) - field_info_dict["default_factory"] = dict + field_info_default_factory_value = dict rule_type_str = "map" elif field.type_name.startswith(".google.protobuf"): protobuf_type_model = self._get_protobuf_type_model(field) type_str = protobuf_type_model.py_type_str rule_type_str = protobuf_type_model.rule_type_str - field_info_dict["default_factory"] = protobuf_type_model.type_factory + field_info_default_factory_value = protobuf_type_model.type_factory + use_custom_type = protobuf_type_model.use_custom_type else: protobuf_type_model = self._get_protobuf_type_model(field) type_str = protobuf_type_model.py_type_str rule_type_str = protobuf_type_model.rule_type_str nested_message_name = type_str + use_custom_type = protobuf_type_model.use_custom_type message_fd: FileDescriptorProto = self._descriptors.message_to_fd[field.type_name] self._add_other_module_pkg(message_fd, type_str) @@ -310,7 +310,7 @@ def _message_field_handle( elif field.type == 14: # enum handle type_str = field.type_name.split(".")[-1] - field_info_dict["default"] = 0 + field_info_default_value = 0 rule_type_str = "enum" root_desc_enum_name = {i.name for i in root_desc.enum_type} if type_str in root_desc_enum_name: @@ -322,8 +322,9 @@ def _message_field_handle( logger.error(f"Not found {field.type} in type_dict") return None else: - field_info_dict["default"] = python_type_default_value_dict[protobuf_desc_python_type_dict[field.type]] + field_info_default_value = python_type_default_value_dict[protobuf_desc_python_type_dict[field.type]] protobuf_type_model = self._get_protobuf_type_model(field) + use_custom_type = protobuf_type_model.use_custom_type type_str = protobuf_type_model.py_type_str rule_type_str = protobuf_type_model.rule_type_str @@ -331,71 +332,59 @@ def _message_field_handle( # repeated support self._add_import_code("typing") type_str = f"typing.List[{type_str}]" - field_info_dict.pop("default", "") - field_info_dict["default_factory"] = list + field_info_default_value = None + field_info_default_factory_value = list rule_type_str = "repeated" - field_option_info_dict: dict = {} + field_info_dict: FieldInfoTypedDict = {} # type: ignore[typeddict-item] comment_field_info_dict, leading_comments, trailing_comments = self._comment_handler( leading_comments, trailing_comments, ) - # - # comment_field_info_dict: dict = {} - # if self.config.parse_comment: - # leading_comments_list: List[str] = [] - # trailing_comments_list: List[str] = [] - # for container, comments in ( - # (leading_comments_list, leading_comments), - # (trailing_comments_list, trailing_comments), - # ): - # for line in comments.split("\n"): - # field_dict = gen_dict_from_desc_str(self.config.comment_prefix, line) - # if not field_dict: - # container.append(line) - # else: - # comment_field_info_dict.update(field_dict) - # leading_comments = "\n".join(leading_comments_list) - # trailing_comments = "\n".join(trailing_comments_list) - if not skip_validate_rule: - field_option_info_dict.update(comment_field_info_dict) - # TODO support message option - # if list(desc.options.ListFields()): + field_info_dict.update(comment_field_info_dict) # type: ignore[typeddict-item] if len(field.options.ListFields()) != 0 and rule_type_str: # protobuf option support - field_option_info_dict.update(gen_field_info_dict_from_field_desc(rule_type_str, field.name, field)) # type: ignore - field_option_info_dict = self._desc_template.handle_template_var(field_option_info_dict) - elif field_option_info_dict: - field_option_info_dict = self._desc_template.handle_template_var(field_option_info_dict) - field_option_info_dict = gen_field_rule_info_dict_from_field_comment_dict( # type: ignore - field_option_info_dict, field, rule_type_str, field.name + field_info_dict.update(gen_field_info_dict_from_field_desc(rule_type_str, field.name, field)) + field_info_dict = self._desc_template.handle_template_var(field_info_dict) + elif field_info_dict: + field_info_dict = self._desc_template.handle_template_var(field_info_dict) + field_info_dict = gen_field_rule_info_dict_from_field_comment_dict( + field_info_dict, field, rule_type_str, field.name # type: ignore[arg-type] ) - if field_option_info_dict: - raw_validator_dict = field_option_info_dict.get("validator", {}) + if field_info_dict: + raw_validator_dict = field_info_dict.get("validator", {}) + + field_info_dict = FieldInfoParamModel(**field_info_dict).to_dict() # type: ignore - skip = field_option_info_dict.pop("skip", False) + skip = field_info_dict.pop("skip", False) if nested_message_name: if nested_message_name not in nested_message_config_dict: nested_message_config_dict[nested_message_name] = {} nested_message_config_dict[nested_message_name]["skip"] = skip - field_option_info_dict = FieldInfoParamModel(**field_option_info_dict).dict() - if not field_option_info_dict.pop("enable", False): + + if not field_info_dict.pop("enable", False): return None try: if type_str.startswith("typing"): import typing # isort:skip field_type = eval(type_str) except NameError: - field_type = None + pass + + if ( + field_info_dict + or field_info_default_value is not _pydantic_adapter.PydanticUndefined + or field_info_default_factory_value is not None + or field_type is not None + ): field_info_param_dict_handle( - field_option_info_dict, - field_info_dict.get("default", _pydantic_adapter.PydanticUndefined), - field_info_dict.get("default_factory", None), + field_info_dict, # type: ignore[arg-type] + field_info_default_value, + field_info_default_factory_value, field_type=field_type, ) - field_info_dict = field_option_info_dict validator_handle_content = "" field_info_dict.pop("validator", None) @@ -448,7 +437,7 @@ def _message_field_handle( type_str = f"typing.Dict[{key_type_str}, {value_type_str}]" # custom field support - field_class: Optional[FieldInfo] = field_info_dict.pop("field", None) + field_class: Optional[Type[FieldInfo]] = field_info_dict.pop("field", None) if field_class: field_name: str = self._get_value_code(field_class) else: @@ -457,7 +446,7 @@ def _message_field_handle( if not _pydantic_adapter.is_v1: # pgv or p2p rule no warning required - field_info_param_dict_migration_v2_handler(field_info_dict, is_warnings=False) + field_info_param_dict_migration_v2_handler(field_info_dict, is_warnings=False) # type: ignore[arg-type] if optional_dict.get(field.name, {}).get("is_proto3_optional", False): self._add_import_code("typing") @@ -471,13 +460,13 @@ def _message_field_handle( for key in FieldInfo.__slots__: value: Any = field_info_dict.get(key, None) if value is getattr(FieldInfo(), key): - field_info_dict.pop(key, None) + field_info_dict.pop(key, None) # type: ignore[misc] if isinstance(field_info_dict.get("json_schema_extra", None), dict): # After Pydantic version 2.1, json_schema_extra type may be callable for k in list(field_info_dict["json_schema_extra"].keys()): if k not in field_info_dict: - field_info_dict[k] = field_info_dict["json_schema_extra"].pop(k) + field_info_dict[k] = field_info_dict["json_schema_extra"].pop(k) # type: ignore[literal-required] if not field_info_dict.get("json_schema_extra", None): field_info_dict.pop("json_schema_extra") @@ -499,14 +488,13 @@ def _message_field_handle( if self.config.parse_comment and leading_comments: class_field_content = leading_comments + "\n" + class_field_content if self.config.parse_comment and trailing_comments: - class_field_content = class_field_content + trailing_comments + "\n" - else: - class_field_content = class_field_content + "\n" - return validator_handle_content, class_field_content + class_field_content = class_field_content + trailing_comments + class_field_content = class_field_content + "\n" + return validator_handle_content, class_field_content, use_custom_type def _gen_one_of_dict( self, desc: DescriptorProto, scl_prefix: SourceCodeLocation, skip_validate_rule: bool - ) -> Tuple[Dict[str, OneOfInfoTypedDict], Dict[str, OptionTypedDict]]: + ) -> Tuple[Dict[str, OneOfTypedDict], Dict[str, OptionTypedDict]]: """ protobuf content: message OneOfOptionalTest { @@ -562,7 +550,7 @@ def _gen_one_of_dict( {'name': {'is_proto3_optional': True}, 'age': {'is_proto3_optional': True}} """ - one_of_dict: Dict[str, OneOfInfoTypedDict] = {} + one_of_dict: Dict[str, OneOfTypedDict] = {} optional_dict: Dict[str, OptionTypedDict] = {} index_field_name_dict: Dict[int, Set[str]] = {} @@ -582,7 +570,7 @@ def _gen_one_of_dict( if one_of_item.name.startswith("_") and one_of_item.name[1:] in optional_dict: continue - option_dict: OneOfInfoTypedDict = {} # type: ignore[typeddict-item] + option_dict: OneOfTypedDict = {} # type: ignore[typeddict-item] for option_descriptor, option_value in one_of_item.options.ListFields(): full_name_list = option_descriptor.full_name.split(".") pkg, rule_name = full_name_list[-2], full_name_list[-1] @@ -628,15 +616,15 @@ def _message( indent: int = 0, skip_validate_rule: bool = False, ) -> str: - self._add_import_code("google.protobuf.message", "Message") class_name = desc.name if desc.name not in PYTHON_RESERVED else "_r_" + desc.name - if class_name in self._parse_desc_name_dict: - if not self._parse_desc_name_dict[class_name]: + if class_name in self._model_cache: + if not self._model_cache[class_name]: raise WaitingToCompleteException(f"The model:{class_name} is being generated") - return self._parse_desc_name_dict[class_name] + return self._model_cache[class_name] else: - self._parse_desc_name_dict[class_name] = "" + self._model_cache[class_name] = "" + self._add_import_code("google.protobuf.message", "Message") comment_info_dict, desc_content, comment_content = self.add_class_desc(scl_prefix, indent) class_name_content = " " * indent + f"class {class_name}({self.config.base_model_class.__name__}):" if comment_content: @@ -662,20 +650,12 @@ def _message( scl_prefix + [DescriptorProto.ONEOF_DECL_FIELD_NUMBER], skip_validate_rule=skip_validate_rule, ) - # for option_descriptor, option_value in field_list: - # # filter unwanted Option - # if protobuf_pkg: - # if not option_descriptor.full_name.endswith(f"{protobuf_pkg}.rules"): - # continue - # elif not option_descriptor.full_name.endswith("validate.rules"): - # continue + for idx, field in enumerate(desc.field): if field.name in PYTHON_RESERVED: continue - if field.type == 11 and self._get_protobuf_type_model(field).use_custom_type: - use_custom_type = True - _content_tuple: Optional[Tuple[str, str]] = self._message_field_handle( + _content_tuple = self._message_field_handle( desc, root_desc, field, @@ -688,6 +668,8 @@ def _message( if _content_tuple: class_validate_handler_content += _content_tuple[0] class_field_content += _content_tuple[1] + if _content_tuple[2]: + use_custom_type = True if desc.nested_type: class_sub_c_str_list.extend( self._message_nested_type_handle( @@ -753,7 +735,7 @@ def _message( ) if not any([class_head_content, class_field_content]): content += " " * (indent + self.code_indent) + "pass\n" - self._parse_desc_name_dict[class_name] = content + self._model_cache[class_name] = content return content def _get_protobuf_type_model(self, field: FieldDescriptorProto) -> ProtobufTypeModel: @@ -770,7 +752,6 @@ def _get_type_factory(module_name: str, message_name: str) -> Any: type_factory: Optional[Any] = None use_custom_type = False - # TODO use gen_model.py _message_default_factory_dict_by_type_name if field.type in protobuf_desc_python_type_dict: type_factory = protobuf_desc_python_type_dict[field.type] return ProtobufTypeModel( @@ -799,16 +780,14 @@ def _get_type_factory(module_name: str, message_name: str) -> Any: type_factory = datetime.now self._add_import_code("datetime", "datetime") elif _type_str == "Duration": + rule_type_str = "duration" + type_factory = timedelta if _pydantic_adapter.is_v1: py_type_str = "Timedelta" - rule_type_str = "duration" - type_factory = timedelta self._add_import_code("datetime", "timedelta") self._add_import_code("protobuf_to_pydantic.util", py_type_str) else: py_type_str = "Annotated[timedelta, BeforeValidator(Timedelta.validate)]" - rule_type_str = "duration" - type_factory = timedelta self._add_import_code("pydantic", "BeforeValidator") self._add_import_code("typing_extensions", "Annotated") diff --git a/protobuf_to_pydantic/template/__init__.py b/protobuf_to_pydantic/template/__init__.py index 57060a3..a342ac0 100644 --- a/protobuf_to_pydantic/template/__init__.py +++ b/protobuf_to_pydantic/template/__init__.py @@ -1,9 +1,11 @@ import json from importlib import import_module -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, TypeVar from protobuf_to_pydantic.grpc_types import RepeatedCompositeContainer, RepeatedScalarContainer +_T = TypeVar("_T") + class CommentTemplate(object): def __init__(self, local_dict: Dict[str, Any], comment_prefix: str, **kwargs: Any) -> None: @@ -50,9 +52,9 @@ def _template_str_handler(self, template_str: str) -> Any: except Exception as e: raise ValueError(f"parse {template_str} error: {e}") from e - def handle_template_var(self, container: Any) -> Any: + def handle_template_var(self, container: _T) -> _T: if isinstance(container, (list, RepeatedCompositeContainer, RepeatedScalarContainer)): - return [self.handle_template_var(i) for i in container] + return [self.handle_template_var(i) for i in container] # type: ignore[return-value] elif isinstance(container, dict): return {k: self.handle_template_var(v) for k, v in container.items()} elif isinstance(container, str) and container.startswith(f"{self._comment_prefix}@"): From 3edb8a8205dc615fd409a487132c8db2f81655aa Mon Sep 17 00:00:00 2001 From: so1n Date: Sun, 8 Sep 2024 00:11:04 +0800 Subject: [PATCH 06/10] Fix, fix pydantic v1 default value and config error --- .../proto_3_20_pydanticv1/demo_gen_code.py | 2 +- .../demo_gen_code_by_p2p.py | 2 +- .../demo_gen_code_by_pgv.py | 2 +- ...gen_code_by_text_comment_protobuf_field.py | 2 +- .../demo_gen_code_by_text_comment_pyi.py | 2 +- .../example_proto/common/single_p2p.py | 2 +- .../example/example_proto/demo/demo_p2p.py | 2 +- .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 6 ++-- .../demo_pb2_by_pyi.py | 6 ++-- .../example_proto/validate/demo_p2p.py | 2 +- .../proto_3_20_pydanticv2/demo_gen_code.py | 2 +- .../demo_gen_code_by_p2p.py | 2 +- .../demo_gen_code_by_pgv.py | 2 +- ...gen_code_by_text_comment_protobuf_field.py | 2 +- .../demo_gen_code_by_text_comment_pyi.py | 2 +- .../example_proto/common/single_p2p.py | 2 +- .../example/example_proto/demo/demo_p2p.py | 2 +- .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 4 +-- .../demo_pb2_by_protobuf.py | 8 ++--- .../demo_pb2_by_pyi.py | 8 ++--- .../example_proto/validate/demo_p2p.py | 2 +- example/proto_pydanticv1/demo_gen_code.py | 2 +- .../proto_pydanticv1/demo_gen_code_by_p2p.py | 2 +- .../proto_pydanticv1/demo_gen_code_by_pgv.py | 2 +- ...gen_code_by_text_comment_protobuf_field.py | 2 +- .../demo_gen_code_by_text_comment_pyi.py | 2 +- .../example_proto/common/single_p2p.py | 2 +- .../example/example_proto/demo/demo_p2p.py | 2 +- .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 6 ++-- .../demo_pb2_by_pyi.py | 6 ++-- .../example_proto/validate/demo_p2p.py | 2 +- .../proto_pydanticv2/demo_gen_code_by_p2p.py | 30 +++++++++---------- .../proto_pydanticv2/demo_gen_code_by_pgv.py | 30 +++++++++---------- .../example_proto/p2p_validate/demo_p2p.py | 30 +++++++++---------- .../p2p_validate_by_comment/demo_p2p.py | 30 +++++++++---------- .../demo_pb2_by_protobuf.py | 30 +++++++++---------- .../demo_pb2_by_pyi.py | 30 +++++++++---------- .../example_proto/validate/demo_p2p.py | 30 +++++++++---------- .../protobuf_option_to_field_info/base.py | 3 +- protobuf_to_pydantic/gen_model.py | 21 ++++++------- protobuf_to_pydantic/grpc_types.py | 1 + protobuf_to_pydantic/plugin/config.py | 2 +- .../test_demo_proto/test_simple.py | 2 +- 48 files changed, 172 insertions(+), 169 deletions(-) diff --git a/example/proto_3_20_pydanticv1/demo_gen_code.py b/example/proto_3_20_pydanticv1/demo_gen_code.py index d138d6c..51d0e50 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py index 2d6c4f7..9510a01 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py index e92a6aa..f9779fd 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py index ec70c6c..52d9c0e 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py index ec70c6c..52d9c0e 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/example/example_proto/common/single_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/common/single_p2p.py index c85d5c4..4b4763d 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/common/single_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/common/single_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 from enum import IntEnum diff --git a/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py index a3ba387..9bd2c35 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py index 276f94c..c7632e4 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 86238c2..c0492ab 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index cfad85c..0b56371 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing @@ -427,7 +427,7 @@ class UserPayMessage(BaseModel): exp_timestamp_gt_now_validator = validator("exp", allow_reuse=True)(timestamp_gt_now_validator) - class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): + class NotEnableUserPayMessageWithSkipRule(BaseModel): bank_number: str = Field(default="") exp: datetime = Field(default_factory=datetime.now) uuid: str = Field(default="") @@ -435,7 +435,7 @@ class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): string_in_map_test: typing.Dict[str, StringTest] = Field(default_factory=dict) map_in_map_test: typing.Dict[str, MapTest] = Field(default_factory=dict) user_pay: UserPayMessage = Field() - not_enable_user_pay: NotEnableUserPayMessageOnlyUseSkipRule = Field() + not_enable_user_pay: NotEnableUserPayMessageWithSkipRule = Field() empty: typing.Any = Field() after_refer: AfterReferMessage = Field() diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index cfad85c..0b56371 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing @@ -427,7 +427,7 @@ class UserPayMessage(BaseModel): exp_timestamp_gt_now_validator = validator("exp", allow_reuse=True)(timestamp_gt_now_validator) - class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): + class NotEnableUserPayMessageWithSkipRule(BaseModel): bank_number: str = Field(default="") exp: datetime = Field(default_factory=datetime.now) uuid: str = Field(default="") @@ -435,7 +435,7 @@ class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): string_in_map_test: typing.Dict[str, StringTest] = Field(default_factory=dict) map_in_map_test: typing.Dict[str, MapTest] = Field(default_factory=dict) user_pay: UserPayMessage = Field() - not_enable_user_pay: NotEnableUserPayMessageOnlyUseSkipRule = Field() + not_enable_user_pay: NotEnableUserPayMessageWithSkipRule = Field() empty: typing.Any = Field() after_refer: AfterReferMessage = Field() diff --git a/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py index 41ce8bb..c9dfd25 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv2/demo_gen_code.py b/example/proto_3_20_pydanticv2/demo_gen_code.py index 91ec87e..fc8e363 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py index bfe0fa6..a793ca6 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py index c0c6ba0..61f41f6 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py index dde1d4c..09bd136 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py index dde1d4c..09bd136 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/example/example_proto/common/single_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/common/single_p2p.py index fbaa791..fbf9564 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/common/single_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/common/single_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 from enum import IntEnum diff --git a/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py index 0dfff3b..51500dc 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py index 59b669c..f96fcd5 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 08fecbd..5114ff6 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing @@ -509,7 +509,7 @@ class RepeatedTest(BaseModel): items_string_test: typing.List[ typing_extensions.Annotated[str, MinLen(min_length=1), MaxLen(max_length=5)] ] = Field(default_factory=list, min_length=1, max_length=5) - items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1), Lt(lt=5)]] = Field( + items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1.0), Lt(lt=5.0)]] = Field( default_factory=list, min_length=1, max_length=5 ) items_int32_test: typing.List[typing_extensions.Annotated[int, Gt(gt=1), Lt(lt=5)]] = Field( diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index 73839fe..faa6c5d 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing @@ -478,7 +478,7 @@ class UserPayMessage(BaseModel): timestamp_gt_now_validator ) - class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): + class NotEnableUserPayMessageWithSkipRule(BaseModel): bank_number: str = Field(default="") exp: datetime = Field(default_factory=datetime.now) uuid: str = Field(default="") @@ -486,7 +486,7 @@ class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): string_in_map_test: typing.Dict[str, StringTest] = Field(default_factory=dict) map_in_map_test: typing.Dict[str, MapTest] = Field(default_factory=dict) user_pay: UserPayMessage = Field() - not_enable_user_pay: NotEnableUserPayMessageOnlyUseSkipRule = Field() + not_enable_user_pay: NotEnableUserPayMessageWithSkipRule = Field() empty: typing.Any = Field() after_refer: AfterReferMessage = Field() @@ -532,7 +532,7 @@ class RepeatedTest(BaseModel): items_string_test: typing.List[ typing_extensions.Annotated[str, MinLen(min_length=1), MaxLen(max_length=5)] ] = Field(default_factory=list, min_length=1, max_length=5) - items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1), Lt(lt=5)]] = Field( + items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1.0), Lt(lt=5.0)]] = Field( default_factory=list, min_length=1, max_length=5 ) items_int32_test: typing.List[typing_extensions.Annotated[int, Gt(gt=1), Lt(lt=5)]] = Field( diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index 73839fe..faa6c5d 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing @@ -478,7 +478,7 @@ class UserPayMessage(BaseModel): timestamp_gt_now_validator ) - class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): + class NotEnableUserPayMessageWithSkipRule(BaseModel): bank_number: str = Field(default="") exp: datetime = Field(default_factory=datetime.now) uuid: str = Field(default="") @@ -486,7 +486,7 @@ class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): string_in_map_test: typing.Dict[str, StringTest] = Field(default_factory=dict) map_in_map_test: typing.Dict[str, MapTest] = Field(default_factory=dict) user_pay: UserPayMessage = Field() - not_enable_user_pay: NotEnableUserPayMessageOnlyUseSkipRule = Field() + not_enable_user_pay: NotEnableUserPayMessageWithSkipRule = Field() empty: typing.Any = Field() after_refer: AfterReferMessage = Field() @@ -532,7 +532,7 @@ class RepeatedTest(BaseModel): items_string_test: typing.List[ typing_extensions.Annotated[str, MinLen(min_length=1), MaxLen(max_length=5)] ] = Field(default_factory=list, min_length=1, max_length=5) - items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1), Lt(lt=5)]] = Field( + items_double_test: typing.List[typing_extensions.Annotated[float, Gt(gt=1.0), Lt(lt=5.0)]] = Field( default_factory=list, min_length=1, max_length=5 ) items_int32_test: typing.List[typing_extensions.Annotated[int, Gt(gt=1), Lt(lt=5)]] = Field( diff --git a/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py index b608d2f..d8353a4 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv1/demo_gen_code.py b/example/proto_pydanticv1/demo_gen_code.py index 45307ba..e602cfd 100644 --- a/example/proto_pydanticv1/demo_gen_code.py +++ b/example/proto_pydanticv1/demo_gen_code.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/demo_gen_code_by_p2p.py b/example/proto_pydanticv1/demo_gen_code_by_p2p.py index 0417150..f3c8dbd 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_p2p.py +++ b/example/proto_pydanticv1/demo_gen_code_by_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/demo_gen_code_by_pgv.py b/example/proto_pydanticv1/demo_gen_code_by_pgv.py index 7df5e70..00ecd04 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_pgv.py +++ b/example/proto_pydanticv1/demo_gen_code_by_pgv.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py index 9754e5a..0e743a0 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py b/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py index 9754e5a..0e743a0 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/example/example_proto/common/single_p2p.py b/example/proto_pydanticv1/example/example_proto/common/single_p2p.py index 80cd407..618e1d5 100644 --- a/example/proto_pydanticv1/example/example_proto/common/single_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/common/single_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 from enum import IntEnum diff --git a/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py index 372cbfa..8e6cf79 100644 --- a/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py index 26704cf..055a4c6 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 531690f..596c902 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index f72fa3b..291720d 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing @@ -427,7 +427,7 @@ class UserPayMessage(BaseModel): exp_timestamp_gt_now_validator = validator("exp", allow_reuse=True)(timestamp_gt_now_validator) - class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): + class NotEnableUserPayMessageWithSkipRule(BaseModel): bank_number: str = Field(default="") exp: datetime = Field(default_factory=datetime.now) uuid: str = Field(default="") @@ -435,7 +435,7 @@ class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): string_in_map_test: typing.Dict[str, StringTest] = Field(default_factory=dict) map_in_map_test: typing.Dict[str, MapTest] = Field(default_factory=dict) user_pay: UserPayMessage = Field() - not_enable_user_pay: NotEnableUserPayMessageOnlyUseSkipRule = Field() + not_enable_user_pay: NotEnableUserPayMessageWithSkipRule = Field() empty: typing.Any = Field() after_refer: AfterReferMessage = Field() diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index f72fa3b..291720d 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing @@ -427,7 +427,7 @@ class UserPayMessage(BaseModel): exp_timestamp_gt_now_validator = validator("exp", allow_reuse=True)(timestamp_gt_now_validator) - class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): + class NotEnableUserPayMessageWithSkipRule(BaseModel): bank_number: str = Field(default="") exp: datetime = Field(default_factory=datetime.now) uuid: str = Field(default="") @@ -435,7 +435,7 @@ class NotEnableUserPayMessageOnlyUseSkipRule(BaseModel): string_in_map_test: typing.Dict[str, StringTest] = Field(default_factory=dict) map_in_map_test: typing.Dict[str, MapTest] = Field(default_factory=dict) user_pay: UserPayMessage = Field() - not_enable_user_pay: NotEnableUserPayMessageOnlyUseSkipRule = Field() + not_enable_user_pay: NotEnableUserPayMessageWithSkipRule = Field() empty: typing.Any = Field() after_refer: AfterReferMessage = Field() diff --git a/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py index e9014db..fa6f094 100644 --- a/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv2/demo_gen_code_by_p2p.py b/example/proto_pydanticv2/demo_gen_code_by_p2p.py index 9b3b76a..92b7fe0 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_p2p.py +++ b/example/proto_pydanticv2/demo_gen_code_by_p2p.py @@ -129,7 +129,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -154,7 +154,7 @@ class DoubleTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -252,7 +252,7 @@ class EnumTest(BaseModel): title_test: State = Field(default=0, title="title_test") extra_test: State = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -277,7 +277,7 @@ class Fixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -302,7 +302,7 @@ class Fixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -327,7 +327,7 @@ class FloatTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -352,7 +352,7 @@ class Int32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -377,7 +377,7 @@ class Int64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -465,7 +465,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -581,7 +581,7 @@ class Sfixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -606,7 +606,7 @@ class Sfixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -631,7 +631,7 @@ class Sint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -656,7 +656,7 @@ class Sint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -736,7 +736,7 @@ class Uint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -761,5 +761,5 @@ class Uint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/example/proto_pydanticv2/demo_gen_code_by_pgv.py b/example/proto_pydanticv2/demo_gen_code_by_pgv.py index 07d6fb2..64e5c68 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_pgv.py +++ b/example/proto_pydanticv2/demo_gen_code_by_pgv.py @@ -95,7 +95,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -107,7 +107,7 @@ class DoubleTest(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1.0, 2.0, 3.0]) ignore_test: float = Field(default=0.0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -168,7 +168,7 @@ class EnumTest(BaseModel): in_test: State = Field(default=0, in_=[0, 2]) not_in_test: State = Field(default=0, not_in=[0, 2]) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -180,7 +180,7 @@ class Fixed32Test(BaseModel): not_in_test: float = Field(default=0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -192,7 +192,7 @@ class Fixed64Test(BaseModel): not_in_test: float = Field(default=0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -204,7 +204,7 @@ class FloatTest(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1.0, 2.0, 3.0]) ignore_test: float = Field(default=0.0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -216,7 +216,7 @@ class Int32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -228,7 +228,7 @@ class Int64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -302,7 +302,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -381,7 +381,7 @@ class Sfixed32Test(BaseModel): not_in_test: float = Field(default=0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -393,7 +393,7 @@ class Sfixed64Test(BaseModel): not_in_test: float = Field(default=0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -405,7 +405,7 @@ class Sint32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -417,7 +417,7 @@ class Sint64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -473,7 +473,7 @@ class Uint32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -485,5 +485,5 @@ class Uint64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py index 910090d..df61b21 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py @@ -80,7 +80,7 @@ class FloatTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -105,7 +105,7 @@ class DoubleTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -130,7 +130,7 @@ class Int32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -155,7 +155,7 @@ class Int64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -180,7 +180,7 @@ class Uint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -205,7 +205,7 @@ class Sint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -230,7 +230,7 @@ class Uint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -255,7 +255,7 @@ class Sint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -280,7 +280,7 @@ class Fixed32Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -305,7 +305,7 @@ class Fixed64Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -330,7 +330,7 @@ class Sfixed32Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -355,7 +355,7 @@ class Sfixed64Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -416,7 +416,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -446,7 +446,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -464,7 +464,7 @@ class EnumTest(BaseModel): title_test: State = Field(default=0, title="title_test") extra_test: State = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 3e954f6..0732958 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -80,7 +80,7 @@ class FloatTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -105,7 +105,7 @@ class DoubleTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -130,7 +130,7 @@ class Int32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -155,7 +155,7 @@ class Int64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -180,7 +180,7 @@ class Uint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -205,7 +205,7 @@ class Sint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -230,7 +230,7 @@ class Uint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -255,7 +255,7 @@ class Sint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -280,7 +280,7 @@ class Fixed32Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -305,7 +305,7 @@ class Fixed64Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -330,7 +330,7 @@ class Sfixed32Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -355,7 +355,7 @@ class Sfixed64Test(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -416,7 +416,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -446,7 +446,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -464,7 +464,7 @@ class EnumTest(BaseModel): title_test: State = Field(default=0, title="title_test") extra_test: State = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index c8143a6..90abd92 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -128,7 +128,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -153,7 +153,7 @@ class DoubleTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -251,7 +251,7 @@ class EnumTest(BaseModel): title_test: State = Field(default=0, title="title_test") extra_test: State = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -276,7 +276,7 @@ class Fixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -301,7 +301,7 @@ class Fixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -326,7 +326,7 @@ class FloatTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -351,7 +351,7 @@ class Int32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -376,7 +376,7 @@ class Int64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -464,7 +464,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -580,7 +580,7 @@ class Sfixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -605,7 +605,7 @@ class Sfixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -630,7 +630,7 @@ class Sint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -655,7 +655,7 @@ class Sint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -735,7 +735,7 @@ class Uint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -760,5 +760,5 @@ class Uint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index c8143a6..90abd92 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -128,7 +128,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -153,7 +153,7 @@ class DoubleTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -251,7 +251,7 @@ class EnumTest(BaseModel): title_test: State = Field(default=0, title="title_test") extra_test: State = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -276,7 +276,7 @@ class Fixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -301,7 +301,7 @@ class Fixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -326,7 +326,7 @@ class FloatTest(BaseModel): title_test: float = Field(default=0.0, title="title_test") extra_test: float = Field(default=0.0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -351,7 +351,7 @@ class Int32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -376,7 +376,7 @@ class Int64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -464,7 +464,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -580,7 +580,7 @@ class Sfixed32Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -605,7 +605,7 @@ class Sfixed64Test(BaseModel): title_test: float = Field(default=0, title="title_test") extra_test: float = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -630,7 +630,7 @@ class Sint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -655,7 +655,7 @@ class Sint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -735,7 +735,7 @@ class Uint32Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -760,5 +760,5 @@ class Uint64Test(BaseModel): title_test: int = Field(default=0, title="title_test") extra_test: int = Field(default=0, customer_string="c1", customer_int=1) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py index f1460e7..f96f401 100644 --- a/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py @@ -65,7 +65,7 @@ class FloatTest(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1.0, 2.0, 3.0]) ignore_test: float = Field(default=0.0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -77,7 +77,7 @@ class DoubleTest(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1.0, 2.0, 3.0]) ignore_test: float = Field(default=0.0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -89,7 +89,7 @@ class Int32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -101,7 +101,7 @@ class Uint32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -113,7 +113,7 @@ class Sint32Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -125,7 +125,7 @@ class Int64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -137,7 +137,7 @@ class Uint64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -149,7 +149,7 @@ class Sint64Test(BaseModel): not_in_test: int = Field(default=0, not_in=[1, 2, 3]) ignore_test: int = Field(default=0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -161,7 +161,7 @@ class Fixed32Test(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0.0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -173,7 +173,7 @@ class Fixed64Test(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0.0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -185,7 +185,7 @@ class Sfixed32Test(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0.0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -197,7 +197,7 @@ class Sfixed64Test(BaseModel): not_in_test: float = Field(default=0.0, not_in=[1, 2, 3]) ignore_test: float = Field(default=0.0) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -238,7 +238,7 @@ class StringTest(BaseModel): not_contains_test_not_contains_validator = field_validator("not_contains_test", mode="after", check_fields=None)( not_contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -259,7 +259,7 @@ class BytesTest(BaseModel): contains_test_contains_validator = field_validator("contains_test", mode="after", check_fields=None)( contains_validator ) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) @@ -269,7 +269,7 @@ class EnumTest(BaseModel): in_test: State = Field(default=0, in_=[0, 2]) not_in_test: State = Field(default=0, not_in=[0, 2]) - in_test_in__validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) + in_test_in_validator = field_validator("in_test", mode="after", check_fields=None)(in_validator) not_in_test_not_in_validator = field_validator("not_in_test", mode="after", check_fields=None)(not_in_validator) diff --git a/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/base.py b/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/base.py index 81e7591..e0273ff 100644 --- a/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/base.py +++ b/protobuf_to_pydantic/field_info_rule/protobuf_option_to_field_info/base.py @@ -144,8 +144,9 @@ def _core_handler( # Compatible with PGV attributes that are not supported by pydantic if "validator" not in field_info_type_dict: field_info_type_dict["validator"] = {} + validator_name = f"{field_name}_{rule_name}_validator" + # dict key not use python keyword _rule_name = rule_name + "_" if rule_name in ("in",) else rule_name - validator_name = f"{field_name}_{_rule_name}_validator" field_info_type_dict["extra"][_rule_name] = self.rule_value_to_field_value_handler( type_name, rule_name, rule_value ) diff --git a/protobuf_to_pydantic/gen_model.py b/protobuf_to_pydantic/gen_model.py index 5efe2be..39bacfe 100644 --- a/protobuf_to_pydantic/gen_model.py +++ b/protobuf_to_pydantic/gen_model.py @@ -29,7 +29,7 @@ get_message_option_dict_from_proto_file, get_message_option_dict_from_pyi_file, ) -from protobuf_to_pydantic.grpc_types import AnyMessage, Descriptor, FieldDescriptor, Message +from protobuf_to_pydantic.grpc_types import AnyMessage, Descriptor, FieldDescriptor, FieldMask, Message from protobuf_to_pydantic.template import CommentTemplate from protobuf_to_pydantic.util import create_pydantic_model @@ -37,7 +37,7 @@ from protobuf_to_pydantic.field_info_rule.types import FieldInfoTypedDict, MessageOptionTypedDict, UseOneOfTypedDict SKIP_RULE_MESSAGE_SUFFIX = "WithSkipRule" -ALLOW_ARBITRARY_TYPE = (AnyMessage,) +ALLOW_ARBITRARY_TYPE = (AnyMessage, FieldMask) def replace_file_name_to_class_name(filename: str) -> str: @@ -431,9 +431,7 @@ def _protobuf_field_lable_is_label_repeated_handler(self, field_dataclass: Field if field_dataclass.field_default is not _pydantic_adapter.PydanticUndefined: field_dataclass.field_default = _pydantic_adapter.PydanticUndefined - def _gen_field_info( - self, field_dataclass: FieldDataClass, skip_validate_rule: bool, is_proto3_optional: bool = False - ) -> Optional[FieldInfo]: + def _gen_field_info(self, field_dataclass: FieldDataClass, skip_validate_rule: bool) -> Optional[FieldInfo]: field_class = self._default_field field_info_dict = self._get_field_info_dict_by_full_name(field_dataclass.protobuf_field.full_name) @@ -521,6 +519,11 @@ def _gen_field_info( } if not _pydantic_adapter.is_v1: field_info_param_dict_migration_v2_handler(field_info_dict) # type: ignore[arg-type] + + for remove_key in ("extra", "json_schema_extra"): + if not field_info_dict.get(remove_key, True): # type: ignore[misc] + field_info_dict.pop(remove_key) # type: ignore[misc] + return field_class(**field_info_dict) # type: ignore def _parse_msg_to_pydantic_model( @@ -564,16 +567,14 @@ def _parse_msg_to_pydantic_model( # At this time, the field type may be modified by the above logic, so it needs to be handled separately if protobuf_field.label == FieldDescriptor.LABEL_REPEATED: self._protobuf_field_lable_is_label_repeated_handler(field_dataclass) - is_proto3_optional = optional_dict.get(protobuf_field.full_name, {}).get("is_proto3_optional", False) - field_info = self._gen_field_info( - field_dataclass, skip_validate_rule, is_proto3_optional=is_proto3_optional - ) + field_info = self._gen_field_info(field_dataclass, skip_validate_rule) if not field_info: continue + is_proto3_optional = optional_dict.get(protobuf_field.full_name, {}).get("is_proto3_optional", False) if is_proto3_optional: field_dataclass.field_type = Optional[field_dataclass.field_type] - if field_info.default is _pydantic_adapter.PydanticUndefined: + if field_info.default is _pydantic_adapter.PydanticUndefined and field_info.default_factory is None: field_info.default = None annotation_dict[field_dataclass.field_name] = (field_dataclass.field_type, field_info) diff --git a/protobuf_to_pydantic/grpc_types.py b/protobuf_to_pydantic/grpc_types.py index a8d1b8e..c92c6f7 100644 --- a/protobuf_to_pydantic/grpc_types.py +++ b/protobuf_to_pydantic/grpc_types.py @@ -174,6 +174,7 @@ def __setitem__(self, *args, **kwargs): # real signature unknown "FieldDescriptor", "FieldDescriptorProto", "FileDescriptorProto", + "FieldMask", "Message", "Timestamp", "MessageToDict", diff --git a/protobuf_to_pydantic/plugin/config.py b/protobuf_to_pydantic/plugin/config.py index 5ba508f..1d3b698 100644 --- a/protobuf_to_pydantic/plugin/config.py +++ b/protobuf_to_pydantic/plugin/config.py @@ -91,7 +91,7 @@ class Config: def after_init(cls, values: Any) -> Any: if _pydantic_adapter.is_v1: # values: Dict[str, Any] - values["desc_template_instance"] = values["template"](values["local_dict"], values["comment_prefix"]) + values["desc_template_instance"] = values["desc_template"](values["local_dict"], values["comment_prefix"]) return values else: # values: "ConfigModel" diff --git a/tests/test_gen_code_output_by_cli/test_demo_proto/test_simple.py b/tests/test_gen_code_output_by_cli/test_demo_proto/test_simple.py index 2c1ff92..4db8d01 100644 --- a/tests/test_gen_code_output_by_cli/test_demo_proto/test_simple.py +++ b/tests/test_gen_code_output_by_cli/test_demo_proto/test_simple.py @@ -307,7 +307,7 @@ class OptionalMessage(BaseModel): y: int = Field(default=0) name: typing.Optional[str] = Field(default="") age: typing.Optional[int] = Field(default=0) - item: typing.Optional[InvoiceItem] = Field() + item: typing.Optional[InvoiceItem] = Field(default=None) str_list: typing.List[str] = Field(default_factory=list) int_map: typing.Dict[str, int] = Field(default_factory=dict) default_template_test: float = Field(default=0.0) From 96dab2228e3d2d26c5c877a06473148b3dadb9e8 Mon Sep 17 00:00:00 2001 From: so1n Date: Sun, 8 Sep 2024 16:00:18 +0800 Subject: [PATCH 07/10] Fix, fix plugin model message references --- example/example_proto/demo/demo.proto | 16 +++++++ example/proto_pydanticv2/demo_gen_code.py | 13 ++++++ ...gen_code_by_text_comment_protobuf_field.py | 13 ++++++ .../demo_gen_code_by_text_comment_pyi.py | 13 ++++++ .../example/example_proto/demo/demo_p2p.py | 18 ++++++++ .../example/example_proto/demo/demo_pb2.py | 12 +++-- .../example/example_proto/demo/demo_pb2.pyi | 45 +++++++++++++++++++ .../plugin/field_desc_proto_to_code.py | 7 ++- 8 files changed, 133 insertions(+), 4 deletions(-) diff --git a/example/example_proto/demo/demo.proto b/example/example_proto/demo/demo.proto index 61ac08b..b874849 100644 --- a/example/example_proto/demo/demo.proto +++ b/example/example_proto/demo/demo.proto @@ -125,3 +125,19 @@ message Invoice3 { int32 quantity = 3; repeated InvoiceItem2 items = 4; } + +// Test Message references +// from: https://github.com/so1n/protobuf_to_pydantic/issues/64 +message RootMessage { + string field1 = 1; + AnOtherMessage field2 = 2; +} + +message AnOtherMessage { + string field1 = 1; + SubMessage field2 = 2; + + message SubMessage { + string text = 1; + } +} diff --git a/example/proto_pydanticv2/demo_gen_code.py b/example/proto_pydanticv2/demo_gen_code.py index b856fa8..ed830df 100644 --- a/example/proto_pydanticv2/demo_gen_code.py +++ b/example/proto_pydanticv2/demo_gen_code.py @@ -17,6 +17,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -129,3 +137,8 @@ class OtherMessage(BaseModel): metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py index 45154b7..b13fe61 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py @@ -20,6 +20,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0, title="use age", example=18, ge=0) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -131,3 +139,8 @@ class OtherMessage(BaseModel): metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py b/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py index 45154b7..b13fe61 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py @@ -20,6 +20,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0, title="use age", example=18, ge=0) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -131,3 +139,8 @@ class OtherMessage(BaseModel): metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py index 26eb37f..74f4a4b 100644 --- a/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py @@ -140,3 +140,21 @@ class InvoiceItem2(BaseModel): quantity: int = Field(default=0) items: typing.List["InvoiceItem2"] = Field(default_factory=list) invoice: Invoice3 = Field() + + +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + +class RootMessage(BaseModel): + """ + Test Message references + from: https://github.com/so1n/protobuf_to_pydantic/issues/64 + """ + + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_pydanticv2/example/example_proto/demo/demo_pb2.py b/example/proto_pydanticv2/example/example_proto/demo/demo_pb2.py index 210242d..364406c 100644 --- a/example/proto_pydanticv2/example/example_proto/demo/demo_pb2.py +++ b/example/proto_pydanticv2/example/example_proto/demo/demo_pb2.py @@ -19,7 +19,7 @@ from example.proto_pydanticv2.example.example_proto.common import single_pb2 as example_dot_example__proto_dot_common_dot_single__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%example/example_proto/demo/demo.proto\x12\x04user\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a google/protobuf/field_mask.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a)example/example_proto/common/single.proto\"\xc3\x01\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x1a\n\x03sex\x18\x04 \x01(\x0e\x32\r.user.SexType\x12\x1e\n\x04\x64\x65mo\x18\x06 \x01(\x0e\x32\x10.single.DemoEnum\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\t\x12)\n\x0c\x64\x65mo_message\x18\t \x01(\x0b\x32\x13.single.DemoMessage\"\xb1\x01\n\x0cOtherMessage\x12)\n\x08metadata\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x32\n\x0c\x64ouble_value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x33\n\nfield_mask\x18\x64 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskH\x00\x88\x01\x01\x42\r\n\x0b_field_mask\"\xe4\x01\n\nMapMessage\x12/\n\x08user_map\x18\x01 \x03(\x0b\x32\x1d.user.MapMessage.UserMapEntry\x12\x31\n\tuser_flag\x18\x02 \x03(\x0b\x32\x1e.user.MapMessage.UserFlagEntry\x1a\x41\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.user.UserMessage:\x02\x38\x01\x1a/\n\rUserFlagEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"[\n\x0fRepeatedMessage\x12\x10\n\x08str_list\x18\x01 \x03(\t\x12\x10\n\x08int_list\x18\x02 \x03(\x05\x12$\n\tuser_list\x18\x03 \x03(\x0b\x32\x11.user.UserMessage\"\x99\x05\n\rNestedMessage\x12;\n\ruser_list_map\x18\x01 \x03(\x0b\x32$.user.NestedMessage.UserListMapEntry\x12\x32\n\x08user_map\x18\x02 \x03(\x0b\x32 .user.NestedMessage.UserMapEntry\x12\x34\n\x08user_pay\x18\x03 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12\x35\n\x0cinclude_enum\x18\x04 \x01(\x0e\x32\x1f.user.NestedMessage.IncludeEnum\x12?\n\x13not_enable_user_pay\x18\x05 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12%\n\x05\x65mpty\x18\x06 \x01(\x0b\x32\x16.google.protobuf.Empty\x12,\n\x0b\x61\x66ter_refer\x18\x07 \x01(\x0b\x32\x17.user.AfterReferMessage\x1a\\\n\x0eUserPayMessage\x12\x13\n\x0b\x62\x61nk_number\x18\x01 \x01(\t\x12\'\n\x03\x65xp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04uuid\x18\x03 \x01(\t\x1aI\n\x10UserListMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.user.RepeatedMessage:\x02\x38\x01\x1a@\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1f\n\x05value\x18\x02 \x01(\x0b\x32\x10.user.MapMessage:\x02\x38\x01\")\n\x0bIncludeEnum\x12\x08\n\x04zero\x10\x00\x12\x07\n\x03one\x10\x01\x12\x07\n\x03two\x10\x02\"-\n\x11\x41\x66terReferMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\"_\n\x0bInvoiceItem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12 \n\x05items\x18\x04 \x03(\x0b\x32\x11.user.InvoiceItem\"\x0e\n\x0c\x45mptyMessage\"\xa9\x02\n\x0fOptionalMessage\x12\x0b\n\x01x\x18\x01 \x01(\tH\x00\x12\x0b\n\x01y\x18\x02 \x01(\x05H\x00\x12\x11\n\x04name\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x10\n\x03\x61ge\x18\x04 \x01(\x05H\x02\x88\x01\x01\x12$\n\x04item\x18\x05 \x01(\x0b\x32\x11.user.InvoiceItemH\x03\x88\x01\x01\x12\x10\n\x08str_list\x18\x06 \x03(\t\x12\x32\n\x07int_map\x18\x07 \x03(\x0b\x32!.user.OptionalMessage.IntMapEntry\x12\x1d\n\x15\x64\x65\x66\x61ult_template_test\x18\x08 \x01(\x02\x1a-\n\x0bIntMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x42\x03\n\x01\x61\x42\x07\n\x05_nameB\x06\n\x04_ageB\x07\n\x05_item\"\x82\x01\n\x0cInvoiceItem2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\x12\x1f\n\x07invoice\x18\x05 \x01(\x0b\x32\x0e.user.Invoice3\"]\n\x08Invoice3\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2*\x1d\n\x07SexType\x12\x07\n\x03man\x10\x00\x12\t\n\x05women\x10\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%example/example_proto/demo/demo.proto\x12\x04user\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a google/protobuf/field_mask.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a)example/example_proto/common/single.proto\"\xc3\x01\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x1a\n\x03sex\x18\x04 \x01(\x0e\x32\r.user.SexType\x12\x1e\n\x04\x64\x65mo\x18\x06 \x01(\x0e\x32\x10.single.DemoEnum\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\t\x12)\n\x0c\x64\x65mo_message\x18\t \x01(\x0b\x32\x13.single.DemoMessage\"\xb1\x01\n\x0cOtherMessage\x12)\n\x08metadata\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x32\n\x0c\x64ouble_value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x33\n\nfield_mask\x18\x64 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskH\x00\x88\x01\x01\x42\r\n\x0b_field_mask\"\xe4\x01\n\nMapMessage\x12/\n\x08user_map\x18\x01 \x03(\x0b\x32\x1d.user.MapMessage.UserMapEntry\x12\x31\n\tuser_flag\x18\x02 \x03(\x0b\x32\x1e.user.MapMessage.UserFlagEntry\x1a\x41\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.user.UserMessage:\x02\x38\x01\x1a/\n\rUserFlagEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"[\n\x0fRepeatedMessage\x12\x10\n\x08str_list\x18\x01 \x03(\t\x12\x10\n\x08int_list\x18\x02 \x03(\x05\x12$\n\tuser_list\x18\x03 \x03(\x0b\x32\x11.user.UserMessage\"\x99\x05\n\rNestedMessage\x12;\n\ruser_list_map\x18\x01 \x03(\x0b\x32$.user.NestedMessage.UserListMapEntry\x12\x32\n\x08user_map\x18\x02 \x03(\x0b\x32 .user.NestedMessage.UserMapEntry\x12\x34\n\x08user_pay\x18\x03 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12\x35\n\x0cinclude_enum\x18\x04 \x01(\x0e\x32\x1f.user.NestedMessage.IncludeEnum\x12?\n\x13not_enable_user_pay\x18\x05 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12%\n\x05\x65mpty\x18\x06 \x01(\x0b\x32\x16.google.protobuf.Empty\x12,\n\x0b\x61\x66ter_refer\x18\x07 \x01(\x0b\x32\x17.user.AfterReferMessage\x1a\\\n\x0eUserPayMessage\x12\x13\n\x0b\x62\x61nk_number\x18\x01 \x01(\t\x12\'\n\x03\x65xp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04uuid\x18\x03 \x01(\t\x1aI\n\x10UserListMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.user.RepeatedMessage:\x02\x38\x01\x1a@\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1f\n\x05value\x18\x02 \x01(\x0b\x32\x10.user.MapMessage:\x02\x38\x01\")\n\x0bIncludeEnum\x12\x08\n\x04zero\x10\x00\x12\x07\n\x03one\x10\x01\x12\x07\n\x03two\x10\x02\"-\n\x11\x41\x66terReferMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\"_\n\x0bInvoiceItem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12 \n\x05items\x18\x04 \x03(\x0b\x32\x11.user.InvoiceItem\"\x0e\n\x0c\x45mptyMessage\"\xa9\x02\n\x0fOptionalMessage\x12\x0b\n\x01x\x18\x01 \x01(\tH\x00\x12\x0b\n\x01y\x18\x02 \x01(\x05H\x00\x12\x11\n\x04name\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x10\n\x03\x61ge\x18\x04 \x01(\x05H\x02\x88\x01\x01\x12$\n\x04item\x18\x05 \x01(\x0b\x32\x11.user.InvoiceItemH\x03\x88\x01\x01\x12\x10\n\x08str_list\x18\x06 \x03(\t\x12\x32\n\x07int_map\x18\x07 \x03(\x0b\x32!.user.OptionalMessage.IntMapEntry\x12\x1d\n\x15\x64\x65\x66\x61ult_template_test\x18\x08 \x01(\x02\x1a-\n\x0bIntMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x42\x03\n\x01\x61\x42\x07\n\x05_nameB\x06\n\x04_ageB\x07\n\x05_item\"\x82\x01\n\x0cInvoiceItem2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\x12\x1f\n\x07invoice\x18\x05 \x01(\x0b\x32\x0e.user.Invoice3\"]\n\x08Invoice3\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\"C\n\x0bRootMessage\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12$\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x14.user.AnOtherMessage\"m\n\x0e\x41nOtherMessage\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12/\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x1f.user.AnOtherMessage.SubMessage\x1a\x1a\n\nSubMessage\x12\x0c\n\x04text\x18\x01 \x01(\t*\x1d\n\x07SexType\x12\x07\n\x03man\x10\x00\x12\t\n\x05women\x10\x01\x62\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'example.example_proto.demo.demo_pb2', globals()) @@ -36,8 +36,8 @@ _NESTEDMESSAGE_USERMAPENTRY._serialized_options = b'8\001' _OPTIONALMESSAGE_INTMAPENTRY._options = None _OPTIONALMESSAGE_INTMAPENTRY._serialized_options = b'8\001' - _SEXTYPE._serialized_start=2306 - _SEXTYPE._serialized_end=2335 + _SEXTYPE._serialized_start=2486 + _SEXTYPE._serialized_end=2515 _USERMESSAGE._serialized_start=249 _USERMESSAGE._serialized_end=444 _OTHERMESSAGE._serialized_start=447 @@ -74,4 +74,10 @@ _INVOICEITEM2._serialized_end=2209 _INVOICE3._serialized_start=2211 _INVOICE3._serialized_end=2304 + _ROOTMESSAGE._serialized_start=2306 + _ROOTMESSAGE._serialized_end=2373 + _ANOTHERMESSAGE._serialized_start=2375 + _ANOTHERMESSAGE._serialized_end=2484 + _ANOTHERMESSAGE_SUBMESSAGE._serialized_start=2458 + _ANOTHERMESSAGE_SUBMESSAGE._serialized_end=2484 # @@protoc_insertion_point(module_scope) diff --git a/example/proto_pydanticv2/example/example_proto/demo/demo_pb2.pyi b/example/proto_pydanticv2/example/example_proto/demo/demo_pb2.pyi index 1cb718c..6efd2fa 100644 --- a/example/proto_pydanticv2/example/example_proto/demo/demo_pb2.pyi +++ b/example/proto_pydanticv2/example/example_proto/demo/demo_pb2.pyi @@ -438,3 +438,48 @@ class Invoice3(google.protobuf.message.Message): ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["amount",b"amount","items",b"items","name",b"name","quantity",b"quantity"]) -> None: ... global___Invoice3 = Invoice3 + +class RootMessage(google.protobuf.message.Message): + """Test Message references + from: https://github.com/so1n/protobuf_to_pydantic/issues/64 + """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor + FIELD1_FIELD_NUMBER: builtins.int + FIELD2_FIELD_NUMBER: builtins.int + field1: typing.Text + @property + def field2(self) -> global___AnOtherMessage: ... + def __init__(self, + *, + field1: typing.Text = ..., + field2: typing.Optional[global___AnOtherMessage] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["field2",b"field2"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["field1",b"field1","field2",b"field2"]) -> None: ... +global___RootMessage = RootMessage + +class AnOtherMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + class SubMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + TEXT_FIELD_NUMBER: builtins.int + text: typing.Text + def __init__(self, + *, + text: typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["text",b"text"]) -> None: ... + + FIELD1_FIELD_NUMBER: builtins.int + FIELD2_FIELD_NUMBER: builtins.int + field1: typing.Text + @property + def field2(self) -> global___AnOtherMessage.SubMessage: ... + def __init__(self, + *, + field1: typing.Text = ..., + field2: typing.Optional[global___AnOtherMessage.SubMessage] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["field2",b"field2"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["field1",b"field1","field2",b"field2"]) -> None: ... +global___AnOtherMessage = AnOtherMessage diff --git a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py index bf621ac..e8f6e2c 100644 --- a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py +++ b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py @@ -288,10 +288,15 @@ def _message_field_handle( message_fd: FileDescriptorProto = self._descriptors.message_to_fd[field.type_name] self._add_other_module_pkg(message_fd, type_str) root_desc_nested_type_name = {i.name for i in root_desc.nested_type} + desc_nested_type_name = {i.name for i in desc.nested_type} if message == desc: # if self-referencing, need use Python type hints postponed annotations type_str = f'"{type_str}"' - elif message_fd.name == self._fd.name and message.name not in root_desc_nested_type_name: + elif ( + message_fd.name == self._fd.name + and message.name not in root_desc_nested_type_name + and message.name not in desc_nested_type_name + ): # If the referenced Message is generated later, it needs to be generated in advance scl_prefix = [FileDescriptorProto.MESSAGE_TYPE_FIELD_NUMBER] for index, desc in enumerate(self._fd.message_type): From 46b7445cd58329c5cbae2c46d380db242d04e662 Mon Sep 17 00:00:00 2001 From: so1n Date: Tue, 17 Sep 2024 02:27:41 +0800 Subject: [PATCH 08/10] Feat, support mutli config --- .../example_proto/demo/single_config.proto | 17 ++++ example/plugin_config.py | 6 +- .../proto_3_20_pydanticv1/demo_gen_code.py | 2 +- .../demo_gen_code_by_p2p.py | 2 +- .../demo_gen_code_by_pgv.py | 2 +- ...gen_code_by_text_comment_protobuf_field.py | 2 +- .../demo_gen_code_by_text_comment_pyi.py | 2 +- .../example_proto/common/single_p2p.py | 2 +- .../example/example_proto/demo/demo_p2p.py | 2 +- .../example_proto/demo/single_config_p2p.py | 18 ++++ .../example_proto/demo/single_config_pb2.py | 98 +++++++++++++++++++ .../example_proto/demo/single_config_pb2.pyi | 45 +++++++++ .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 2 +- .../demo_pb2_by_pyi.py | 2 +- .../example_proto/validate/demo_p2p.py | 2 +- .../proto_3_20_pydanticv2/demo_gen_code.py | 2 +- .../demo_gen_code_by_p2p.py | 2 +- .../demo_gen_code_by_pgv.py | 2 +- ...gen_code_by_text_comment_protobuf_field.py | 2 +- .../demo_gen_code_by_text_comment_pyi.py | 2 +- .../example_proto/common/single_p2p.py | 2 +- .../example/example_proto/demo/demo_p2p.py | 2 +- .../example_proto/demo/single_config_p2p.py | 18 ++++ .../example_proto/demo/single_config_pb2.py | 98 +++++++++++++++++++ .../example_proto/demo/single_config_pb2.pyi | 45 +++++++++ .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 2 +- .../demo_pb2_by_pyi.py | 2 +- .../example_proto/validate/demo_p2p.py | 2 +- example/proto_pydanticv1/demo_gen_code.py | 2 +- .../proto_pydanticv1/demo_gen_code_by_p2p.py | 2 +- .../proto_pydanticv1/demo_gen_code_by_pgv.py | 2 +- ...gen_code_by_text_comment_protobuf_field.py | 2 +- .../demo_gen_code_by_text_comment_pyi.py | 2 +- .../example_proto/common/single_p2p.py | 2 +- .../example/example_proto/demo/demo_p2p.py | 2 +- .../example_proto/demo/single_config_p2p.py | 18 ++++ .../example_proto/demo/single_config_pb2.py | 25 +++++ .../example_proto/demo/single_config_pb2.pyi | 45 +++++++++ .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 2 +- .../demo_pb2_by_pyi.py | 2 +- .../example_proto/validate/demo_p2p.py | 2 +- example/proto_pydanticv2/demo_gen_code.py | 2 +- .../proto_pydanticv2/demo_gen_code_by_p2p.py | 2 +- .../proto_pydanticv2/demo_gen_code_by_pgv.py | 2 +- ...gen_code_by_text_comment_protobuf_field.py | 2 +- .../demo_gen_code_by_text_comment_pyi.py | 2 +- .../example_proto/common/single_p2p.py | 2 +- .../example/example_proto/demo/demo_p2p.py | 2 +- .../example_proto/demo/single_config_p2p.py | 18 ++++ .../example_proto/demo/single_config_pb2.py | 25 +++++ .../example_proto/demo/single_config_pb2.pyi | 45 +++++++++ .../example_proto/p2p_validate/demo_p2p.py | 2 +- .../p2p_validate_by_comment/demo_p2p.py | 2 +- .../demo_pb2_by_protobuf.py | 2 +- .../demo_pb2_by_pyi.py | 2 +- .../example_proto/validate/demo_p2p.py | 2 +- example/single_config_pkg_plugin_config.py | 1 + protobuf_to_pydantic/plugin/code_gen.py | 10 +- protobuf_to_pydantic/plugin/config.py | 19 ++++ 65 files changed, 592 insertions(+), 55 deletions(-) create mode 100644 example/example_proto/demo/single_config.proto create mode 100644 example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_p2p.py create mode 100644 example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_pb2.py create mode 100644 example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_pb2.pyi create mode 100644 example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_p2p.py create mode 100644 example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_pb2.py create mode 100644 example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_pb2.pyi create mode 100644 example/proto_pydanticv1/example/example_proto/demo/single_config_p2p.py create mode 100644 example/proto_pydanticv1/example/example_proto/demo/single_config_pb2.py create mode 100644 example/proto_pydanticv1/example/example_proto/demo/single_config_pb2.pyi create mode 100644 example/proto_pydanticv2/example/example_proto/demo/single_config_p2p.py create mode 100644 example/proto_pydanticv2/example/example_proto/demo/single_config_pb2.py create mode 100644 example/proto_pydanticv2/example/example_proto/demo/single_config_pb2.pyi create mode 100644 example/single_config_pkg_plugin_config.py diff --git a/example/example_proto/demo/single_config.proto b/example/example_proto/demo/single_config.proto new file mode 100644 index 0000000..bc53d70 --- /dev/null +++ b/example/example_proto/demo/single_config.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; +package single_config; + + +// user info +message UserMessage { + // aha: {"required": true, "example": "10086", "title": "UID", "description": "user union id"} + string uid=1; + // aha: {"example": 18, "title": "use age", "ge": 0} + int32 age=2; + // aha: {"ge": 0, "le": 2.5} + float height=3; + bool is_adult=7; + // aha: {"description": "user name"} + // aha: {"default": "", "min_length": 1, "max_length": "10", "example": "so1n"} + string user_name=8; +} diff --git a/example/plugin_config.py b/example/plugin_config.py index 95340e7..1e9ca20 100644 --- a/example/plugin_config.py +++ b/example/plugin_config.py @@ -1,6 +1,6 @@ import logging import time -from typing import List, Type +from typing import Dict, List, Type from uuid import uuid4 from google.protobuf.any_pb2 import Any # type: ignore @@ -8,6 +8,9 @@ from pydantic.fields import FieldInfo from protobuf_to_pydantic.desc_template import DescTemplate +from protobuf_to_pydantic.plugin.config import SubConfigModel + +from . import single_config_pkg_plugin_config logging.basicConfig(format="[%(asctime)s %(levelname)s] %(message)s", datefmt="%y-%m-%d %H:%M:%S", level=logging.INFO) @@ -46,3 +49,4 @@ def exp_time() -> float: comment_prefix = "p2p" desc_template: Type[DescTemplate] = CustomDescTemplate ignore_pkg_list: List[str] = ["validate", "p2p_validate"] +pkg_config: Dict[str, SubConfigModel] = {"single_config": SubConfigModel(module=single_config_pkg_plugin_config)} diff --git a/example/proto_3_20_pydanticv1/demo_gen_code.py b/example/proto_3_20_pydanticv1/demo_gen_code.py index d138d6c..51d0e50 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py index f3976a7..f2ad403 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py index 436deff..df32389 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_pgv.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py index ec70c6c..52d9c0e 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py index ec70c6c..52d9c0e 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/example/example_proto/common/single_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/common/single_p2p.py index c85d5c4..4b4763d 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/common/single_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/common/single_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 from enum import IntEnum diff --git a/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py index a3ba387..9bd2c35 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_p2p.py new file mode 100644 index 0000000..9856c98 --- /dev/null +++ b/example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_p2p.py @@ -0,0 +1,18 @@ +# This is an automatically generated file, please do not change +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) +# Protobuf Version: 3.20.3 +# Pydantic Version: 1.10.7 +from google.protobuf.message import Message # type: ignore +from pydantic import BaseModel, Field + + +class UserMessage(BaseModel): + """ + user info + """ + + uid: str = Field(example="10086", title="UID", description="user union id") + age: int = Field(default=0, example=18, title="use age", ge=0.0) + height: float = Field(default=0.0, ge=0.0, le=2.5) + is_adult: bool = Field(default=False) + user_name: str = Field(default="", example="so1n", description="user name", min_length=1, max_length=10) diff --git a/example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_pb2.py b/example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_pb2.py new file mode 100644 index 0000000..4a6707e --- /dev/null +++ b/example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_pb2.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: example/example_proto/demo/single_config.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='example/example_proto/demo/single_config.proto', + package='single_config', + syntax='proto3', + serialized_options=None, + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n.example/example_proto/demo/single_config.proto\x12\rsingle_config\"\\\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\tb\x06proto3' +) + + + + +_USERMESSAGE = _descriptor.Descriptor( + name='UserMessage', + full_name='single_config.UserMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='uid', full_name='single_config.UserMessage.uid', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='age', full_name='single_config.UserMessage.age', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='height', full_name='single_config.UserMessage.height', index=2, + number=3, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='is_adult', full_name='single_config.UserMessage.is_adult', index=3, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='user_name', full_name='single_config.UserMessage.user_name', index=4, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=65, + serialized_end=157, +) + +DESCRIPTOR.message_types_by_name['UserMessage'] = _USERMESSAGE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +UserMessage = _reflection.GeneratedProtocolMessageType('UserMessage', (_message.Message,), { + 'DESCRIPTOR' : _USERMESSAGE, + '__module__' : 'example.example_proto.demo.single_config_pb2' + # @@protoc_insertion_point(class_scope:single_config.UserMessage) + }) +_sym_db.RegisterMessage(UserMessage) + + +# @@protoc_insertion_point(module_scope) diff --git a/example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_pb2.pyi b/example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_pb2.pyi new file mode 100644 index 0000000..72cf391 --- /dev/null +++ b/example/proto_3_20_pydanticv1/example/example_proto/demo/single_config_pb2.pyi @@ -0,0 +1,45 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.message +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class UserMessage(google.protobuf.message.Message): + """user info""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor + UID_FIELD_NUMBER: builtins.int + AGE_FIELD_NUMBER: builtins.int + HEIGHT_FIELD_NUMBER: builtins.int + IS_ADULT_FIELD_NUMBER: builtins.int + USER_NAME_FIELD_NUMBER: builtins.int + uid: typing.Text + """aha: {"required": true, "example": "10086", "title": "UID", "description": "user union id"}""" + + age: builtins.int + """aha: {"example": 18, "title": "use age", "ge": 0}""" + + height: builtins.float + """aha: {"ge": 0, "le": 2.5}""" + + is_adult: builtins.bool + user_name: typing.Text + """aha: {"description": "user name"} + aha: {"default": "", "min_length": 1, "max_length": "10", "example": "so1n"} + """ + + def __init__(self, + *, + uid: typing.Text = ..., + age: builtins.int = ..., + height: builtins.float = ..., + is_adult: builtins.bool = ..., + user_name: typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["age",b"age","height",b"height","is_adult",b"is_adult","uid",b"uid","user_name",b"user_name"]) -> None: ... +global___UserMessage = UserMessage diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py index d7bc702..d83fb64 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 42d32e6..e8e7296 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index a8b2dcd..642c4bb 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index a8b2dcd..642c4bb 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py index 9133b94..f36ac3c 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_3_20_pydanticv2/demo_gen_code.py b/example/proto_3_20_pydanticv2/demo_gen_code.py index 91ec87e..fc8e363 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py index 78c1670..8274ba6 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py index ee1fd2a..93ea3a5 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_pgv.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py index dde1d4c..09bd136 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py index dde1d4c..09bd136 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/example/example_proto/common/single_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/common/single_p2p.py index fbaa791..fbf9564 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/common/single_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/common/single_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 from enum import IntEnum diff --git a/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py index 0dfff3b..51500dc 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_p2p.py new file mode 100644 index 0000000..6b56931 --- /dev/null +++ b/example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_p2p.py @@ -0,0 +1,18 @@ +# This is an automatically generated file, please do not change +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) +# Protobuf Version: 3.20.3 +# Pydantic Version: 2.5.3 +from google.protobuf.message import Message # type: ignore +from pydantic import BaseModel, Field + + +class UserMessage(BaseModel): + """ + user info + """ + + uid: str = Field(title="UID", description="user union id", example="10086") + age: int = Field(default=0, title="use age", ge=0, example=18) + height: float = Field(default=0.0, ge=0.0, le=2.5) + is_adult: bool = Field(default=False) + user_name: str = Field(default="", description="user name", min_length=1, max_length=10, example="so1n") diff --git a/example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_pb2.py b/example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_pb2.py new file mode 100644 index 0000000..4a6707e --- /dev/null +++ b/example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_pb2.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: example/example_proto/demo/single_config.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='example/example_proto/demo/single_config.proto', + package='single_config', + syntax='proto3', + serialized_options=None, + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n.example/example_proto/demo/single_config.proto\x12\rsingle_config\"\\\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\tb\x06proto3' +) + + + + +_USERMESSAGE = _descriptor.Descriptor( + name='UserMessage', + full_name='single_config.UserMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='uid', full_name='single_config.UserMessage.uid', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='age', full_name='single_config.UserMessage.age', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='height', full_name='single_config.UserMessage.height', index=2, + number=3, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='is_adult', full_name='single_config.UserMessage.is_adult', index=3, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='user_name', full_name='single_config.UserMessage.user_name', index=4, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=65, + serialized_end=157, +) + +DESCRIPTOR.message_types_by_name['UserMessage'] = _USERMESSAGE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +UserMessage = _reflection.GeneratedProtocolMessageType('UserMessage', (_message.Message,), { + 'DESCRIPTOR' : _USERMESSAGE, + '__module__' : 'example.example_proto.demo.single_config_pb2' + # @@protoc_insertion_point(class_scope:single_config.UserMessage) + }) +_sym_db.RegisterMessage(UserMessage) + + +# @@protoc_insertion_point(module_scope) diff --git a/example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_pb2.pyi b/example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_pb2.pyi new file mode 100644 index 0000000..72cf391 --- /dev/null +++ b/example/proto_3_20_pydanticv2/example/example_proto/demo/single_config_pb2.pyi @@ -0,0 +1,45 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.message +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class UserMessage(google.protobuf.message.Message): + """user info""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor + UID_FIELD_NUMBER: builtins.int + AGE_FIELD_NUMBER: builtins.int + HEIGHT_FIELD_NUMBER: builtins.int + IS_ADULT_FIELD_NUMBER: builtins.int + USER_NAME_FIELD_NUMBER: builtins.int + uid: typing.Text + """aha: {"required": true, "example": "10086", "title": "UID", "description": "user union id"}""" + + age: builtins.int + """aha: {"example": 18, "title": "use age", "ge": 0}""" + + height: builtins.float + """aha: {"ge": 0, "le": 2.5}""" + + is_adult: builtins.bool + user_name: typing.Text + """aha: {"description": "user name"} + aha: {"default": "", "min_length": 1, "max_length": "10", "example": "so1n"} + """ + + def __init__(self, + *, + uid: typing.Text = ..., + age: builtins.int = ..., + height: builtins.float = ..., + is_adult: builtins.bool = ..., + user_name: typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["age",b"age","height",b"height","is_adult",b"is_adult","uid",b"uid","user_name",b"user_name"]) -> None: ... +global___UserMessage = UserMessage diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py index f0a8e0c..87d8dc2 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 68796df..fc07b61 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index a297a48..f36721f 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index a297a48..f36721f 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py index c1adc2c..531b797 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 3.20.3 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv1/demo_gen_code.py b/example/proto_pydanticv1/demo_gen_code.py index 45307ba..e602cfd 100644 --- a/example/proto_pydanticv1/demo_gen_code.py +++ b/example/proto_pydanticv1/demo_gen_code.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/demo_gen_code_by_p2p.py b/example/proto_pydanticv1/demo_gen_code_by_p2p.py index 42d2c89..b9804dd 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_p2p.py +++ b/example/proto_pydanticv1/demo_gen_code_by_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/demo_gen_code_by_pgv.py b/example/proto_pydanticv1/demo_gen_code_by_pgv.py index 1b19d40..e6fa5b6 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_pgv.py +++ b/example/proto_pydanticv1/demo_gen_code_by_pgv.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py index 9754e5a..0e743a0 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py b/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py index 9754e5a..0e743a0 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/example/example_proto/common/single_p2p.py b/example/proto_pydanticv1/example/example_proto/common/single_p2p.py index 80cd407..618e1d5 100644 --- a/example/proto_pydanticv1/example/example_proto/common/single_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/common/single_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 from enum import IntEnum diff --git a/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py index 372cbfa..8e6cf79 100644 --- a/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/example/example_proto/demo/single_config_p2p.py b/example/proto_pydanticv1/example/example_proto/demo/single_config_p2p.py new file mode 100644 index 0000000..8c6891b --- /dev/null +++ b/example/proto_pydanticv1/example/example_proto/demo/single_config_p2p.py @@ -0,0 +1,18 @@ +# This is an automatically generated file, please do not change +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) +# Protobuf Version: 4.24.4 +# Pydantic Version: 1.10.7 +from google.protobuf.message import Message # type: ignore +from pydantic import BaseModel, Field + + +class UserMessage(BaseModel): + """ + user info + """ + + uid: str = Field(example="10086", title="UID", description="user union id") + age: int = Field(default=0, example=18, title="use age", ge=0.0) + height: float = Field(default=0.0, ge=0.0, le=2.5) + is_adult: bool = Field(default=False) + user_name: str = Field(default="", example="so1n", description="user name", min_length=1, max_length=10) diff --git a/example/proto_pydanticv1/example/example_proto/demo/single_config_pb2.py b/example/proto_pydanticv1/example/example_proto/demo/single_config_pb2.py new file mode 100644 index 0000000..2df52f3 --- /dev/null +++ b/example/proto_pydanticv1/example/example_proto/demo/single_config_pb2.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: example/example_proto/demo/single_config.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.example/example_proto/demo/single_config.proto\x12\rsingle_config\"\\\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\tb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'example.example_proto.demo.single_config_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _USERMESSAGE._serialized_start=65 + _USERMESSAGE._serialized_end=157 +# @@protoc_insertion_point(module_scope) diff --git a/example/proto_pydanticv1/example/example_proto/demo/single_config_pb2.pyi b/example/proto_pydanticv1/example/example_proto/demo/single_config_pb2.pyi new file mode 100644 index 0000000..72cf391 --- /dev/null +++ b/example/proto_pydanticv1/example/example_proto/demo/single_config_pb2.pyi @@ -0,0 +1,45 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.message +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class UserMessage(google.protobuf.message.Message): + """user info""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor + UID_FIELD_NUMBER: builtins.int + AGE_FIELD_NUMBER: builtins.int + HEIGHT_FIELD_NUMBER: builtins.int + IS_ADULT_FIELD_NUMBER: builtins.int + USER_NAME_FIELD_NUMBER: builtins.int + uid: typing.Text + """aha: {"required": true, "example": "10086", "title": "UID", "description": "user union id"}""" + + age: builtins.int + """aha: {"example": 18, "title": "use age", "ge": 0}""" + + height: builtins.float + """aha: {"ge": 0, "le": 2.5}""" + + is_adult: builtins.bool + user_name: typing.Text + """aha: {"description": "user name"} + aha: {"default": "", "min_length": 1, "max_length": "10", "example": "so1n"} + """ + + def __init__(self, + *, + uid: typing.Text = ..., + age: builtins.int = ..., + height: builtins.float = ..., + is_adult: builtins.bool = ..., + user_name: typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["age",b"age","height",b"height","is_adult",b"is_adult","uid",b"uid","user_name",b"user_name"]) -> None: ... +global___UserMessage = UserMessage diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py index 5d1a765..80cecc8 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py index 0b0f968..a979194 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index 16981f6..e62169a 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index 16981f6..e62169a 100644 --- a/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py index 0758e69..f8caef4 100644 --- a/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 1.10.7 import typing diff --git a/example/proto_pydanticv2/demo_gen_code.py b/example/proto_pydanticv2/demo_gen_code.py index b856fa8..a654b9b 100644 --- a/example/proto_pydanticv2/demo_gen_code.py +++ b/example/proto_pydanticv2/demo_gen_code.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/demo_gen_code_by_p2p.py b/example/proto_pydanticv2/demo_gen_code_by_p2p.py index e311906..af6f8b2 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_p2p.py +++ b/example/proto_pydanticv2/demo_gen_code_by_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/demo_gen_code_by_pgv.py b/example/proto_pydanticv2/demo_gen_code_by_pgv.py index 48d2568..d63c55b 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_pgv.py +++ b/example/proto_pydanticv2/demo_gen_code_by_pgv.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py index 45154b7..5046229 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py b/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py index 45154b7..5046229 100644 --- a/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_pydanticv2/demo_gen_code_by_text_comment_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/example/example_proto/common/single_p2p.py b/example/proto_pydanticv2/example/example_proto/common/single_p2p.py index 02ec71c..32ba63e 100644 --- a/example/proto_pydanticv2/example/example_proto/common/single_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/common/single_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 from enum import IntEnum diff --git a/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py index 26eb37f..68e414d 100644 --- a/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/demo/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/example/example_proto/demo/single_config_p2p.py b/example/proto_pydanticv2/example/example_proto/demo/single_config_p2p.py new file mode 100644 index 0000000..01458c3 --- /dev/null +++ b/example/proto_pydanticv2/example/example_proto/demo/single_config_p2p.py @@ -0,0 +1,18 @@ +# This is an automatically generated file, please do not change +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) +# Protobuf Version: 4.24.4 +# Pydantic Version: 2.5.3 +from google.protobuf.message import Message # type: ignore +from pydantic import BaseModel, Field + + +class UserMessage(BaseModel): + """ + user info + """ + + uid: str = Field(title="UID", description="user union id", example="10086") + age: int = Field(default=0, title="use age", ge=0, example=18) + height: float = Field(default=0.0, ge=0.0, le=2.5) + is_adult: bool = Field(default=False) + user_name: str = Field(default="", description="user name", min_length=1, max_length=10, example="so1n") diff --git a/example/proto_pydanticv2/example/example_proto/demo/single_config_pb2.py b/example/proto_pydanticv2/example/example_proto/demo/single_config_pb2.py new file mode 100644 index 0000000..2df52f3 --- /dev/null +++ b/example/proto_pydanticv2/example/example_proto/demo/single_config_pb2.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: example/example_proto/demo/single_config.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.example/example_proto/demo/single_config.proto\x12\rsingle_config\"\\\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\tb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'example.example_proto.demo.single_config_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _USERMESSAGE._serialized_start=65 + _USERMESSAGE._serialized_end=157 +# @@protoc_insertion_point(module_scope) diff --git a/example/proto_pydanticv2/example/example_proto/demo/single_config_pb2.pyi b/example/proto_pydanticv2/example/example_proto/demo/single_config_pb2.pyi new file mode 100644 index 0000000..72cf391 --- /dev/null +++ b/example/proto_pydanticv2/example/example_proto/demo/single_config_pb2.pyi @@ -0,0 +1,45 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.message +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class UserMessage(google.protobuf.message.Message): + """user info""" + DESCRIPTOR: google.protobuf.descriptor.Descriptor + UID_FIELD_NUMBER: builtins.int + AGE_FIELD_NUMBER: builtins.int + HEIGHT_FIELD_NUMBER: builtins.int + IS_ADULT_FIELD_NUMBER: builtins.int + USER_NAME_FIELD_NUMBER: builtins.int + uid: typing.Text + """aha: {"required": true, "example": "10086", "title": "UID", "description": "user union id"}""" + + age: builtins.int + """aha: {"example": 18, "title": "use age", "ge": 0}""" + + height: builtins.float + """aha: {"ge": 0, "le": 2.5}""" + + is_adult: builtins.bool + user_name: typing.Text + """aha: {"description": "user name"} + aha: {"default": "", "min_length": 1, "max_length": "10", "example": "so1n"} + """ + + def __init__(self, + *, + uid: typing.Text = ..., + age: builtins.int = ..., + height: builtins.float = ..., + is_adult: builtins.bool = ..., + user_name: typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["age",b"age","height",b"height","is_adult",b"is_adult","uid",b"uid","user_name",b"user_name"]) -> None: ... +global___UserMessage = UserMessage diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py index e74cebd..ef0550e 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py index e082279..b54ac54 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py index 3137a28..9f5bd82 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_protobuf.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py index 3137a28..9f5bd82 100644 --- a/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py +++ b/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment/demo_pb2_by_pyi.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py b/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py index da9c02f..5b42135 100644 --- a/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py +++ b/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py @@ -1,5 +1,5 @@ # This is an automatically generated file, please do not change -# gen by protobuf_to_pydantic[v0.2.6](https://github.com/so1n/protobuf_to_pydantic) +# gen by protobuf_to_pydantic[v0.2.7](https://github.com/so1n/protobuf_to_pydantic) # Protobuf Version: 4.24.4 # Pydantic Version: 2.5.3 import typing diff --git a/example/single_config_pkg_plugin_config.py b/example/single_config_pkg_plugin_config.py new file mode 100644 index 0000000..b3fce06 --- /dev/null +++ b/example/single_config_pkg_plugin_config.py @@ -0,0 +1 @@ +comment_prefix = "aha" diff --git a/protobuf_to_pydantic/plugin/code_gen.py b/protobuf_to_pydantic/plugin/code_gen.py index 5ee3c45..51afe79 100644 --- a/protobuf_to_pydantic/plugin/code_gen.py +++ b/protobuf_to_pydantic/plugin/code_gen.py @@ -147,11 +147,9 @@ def gen_config(self) -> None: def generate_pydantic_model(self, descriptors: Descriptors, response: CodeGeneratorResponse) -> None: for name, fd in descriptors.to_generate.items(): - if fd.package in self.config.ignore_pkg_list: + config = self.config.pkg_config.get(fd.package, self.config) + if fd.package in config.ignore_pkg_list: continue file = response.file.add() - file.name = fd.name[:-6].replace("-", "_").replace(".", "/") + f"{self.config.file_name_suffix}.py" - file.content = self.config.file_descriptor_proto_to_code( - fd=fd, descriptors=descriptors, config=self.config - ).content - print(f"Writing protobuf-to-pydantic code to {file.name}", file=sys.stderr) + file.name = fd.name[:-6].replace("-", "_").replace(".", "/") + f"{config.file_name_suffix}.py" + file.content = config.file_descriptor_proto_to_code(fd=fd, descriptors=descriptors, config=config).content diff --git a/protobuf_to_pydantic/plugin/config.py b/protobuf_to_pydantic/plugin/config.py index 86bd369..6135c58 100644 --- a/protobuf_to_pydantic/plugin/config.py +++ b/protobuf_to_pydantic/plugin/config.py @@ -19,6 +19,10 @@ class ProtobufTypeConfigModel(BaseModel): ) +class SubConfigModel(BaseModel): + module: Any + + class ConfigModel(BaseModel): local_dict: dict = Field(default_factory=dict, description="Dict for local variables") desc_template: Type[DescTemplate] = Field( @@ -83,6 +87,9 @@ class ConfigModel(BaseModel): default_factory=lambda: DescTemplate({}, ""), description="This variable does not support configuration and will be overwritten even if configured", ) + pkg_config: Dict[str, "ConfigModel"] = Field( + default_factory=dict, description="Customize the configuration of different pkgs" + ) class Config: arbitrary_types_allowed = True @@ -96,6 +103,18 @@ def after_init(cls, values: Any) -> Any: else: # values: "ConfigModel" values.desc_template_instance = values.desc_template(values.local_dict, values.comment_prefix) + return values + + @_pydantic_adapter.model_validator(mode="before") + def before_init(cls, values: Any) -> Any: + def _validator(_values: Any) -> dict: + if not isinstance(_values, SubConfigModel): + raise ValueError("values must be a SubConfigModel") + return get_config_by_module(_values.module, ConfigModel).dict() + + if "pkg_config" in values: + values["pkg_config"] = {k: _validator(v) for k, v in values.get("pkg_config", {}).items()} + return values def get_config_by_module(module: Any, config_class: Type[ConfigT]) -> ConfigT: From 0124c35cdd6f27a77e1d9e114c8c130fc8d6f944 Mon Sep 17 00:00:00 2001 From: so1n Date: Sun, 15 Sep 2024 12:59:20 +0800 Subject: [PATCH 09/10] Docs, change buf plugin docs --- buf-plugin/README.md | 2 ++ buf-plugin/README_ZH.md | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/buf-plugin/README.md b/buf-plugin/README.md index ab4cdb2..25e6f37 100644 --- a/buf-plugin/README.md +++ b/buf-plugin/README.md @@ -145,6 +145,8 @@ Regardless of which method is used to generate `Pydantic` code, the final projec └── pyproject.toml ``` ### 1.4.Generating Pydantic Code with Remote Plugins +> Note: Since the BUF plugin created by the developer cannot be used publicly, the remote plug-in cannot be used normally(See: [issue](https://github.com/bufbuild/plugins/issues/589#issuecomment-1799085322))。If necessary, please refer to the custom plugins in the next section to create your own plugins。 + In addition to executing local plugin to generate `Pydantic` code, can also use remote plugin. First, need to change the `buf.gen.yaml` file to the following content: diff --git a/buf-plugin/README_ZH.md b/buf-plugin/README_ZH.md index cc495b9..29c1fa9 100644 --- a/buf-plugin/README_ZH.md +++ b/buf-plugin/README_ZH.md @@ -141,6 +141,8 @@ plugins: └── pyproject.toml ``` ### 1.4.使用远程插件生成Pydantic代码 +> Note: 由于开发者自己创建的buf插件是无法公开使用的,所以远程插件无法正常使用(具体见:[issue](https://github.com/bufbuild/plugins/issues/589#issuecomment-1799085322))。如有需要,请参考下一节的自定义插件自己创建插件。 + 除了执行本地插件生成Pydantic代码外,还可以使用远程插件,首先需要更改`buf.gen.yaml`文件为如下内容: ```yaml version: v1 @@ -166,7 +168,7 @@ plugins: -## 2.Custom Plugins +## 2.自定义插件 > Note: 请确保你已经阅读了 [buf custom plugins dev doc](https://buf.build/docs/bsr/remote-plugins/custom-plugins) `protobuf-to-pydantic`的标准`buf-plugin`可以满足大多数功能。如果您有自定义需求,您应该创建属于自己的自定义插件。 From 4fd542478804116964eb81711e95ba28cb90eb66 Mon Sep 17 00:00:00 2001 From: so1n Date: Mon, 14 Oct 2024 23:19:20 +0800 Subject: [PATCH 10/10] Feat, add func doc and change var name --- README.md | 266 ++++++++++-------- README_ZH.md | 257 +++++++++-------- example/gen_p2p_code.py | 4 +- example/gen_text_comment_code.py | 12 +- example/p2p_validate_by_comment_gen_code.py | 12 +- example/plugin_config.py | 6 +- .../proto_3_20_pydanticv1/demo_gen_code.py | 13 + ...gen_code_by_text_comment_protobuf_field.py | 13 + .../demo_gen_code_by_text_comment_pyi.py | 13 + .../example/example_proto/demo/demo_p2p.py | 18 ++ .../example/example_proto/demo/demo_pb2.py | 142 +++++++++- .../example/example_proto/demo/demo_pb2.pyi | 45 +++ .../proto_3_20_pydanticv2/demo_gen_code.py | 13 + ...gen_code_by_text_comment_protobuf_field.py | 13 + .../demo_gen_code_by_text_comment_pyi.py | 13 + .../example/example_proto/demo/demo_p2p.py | 18 ++ .../example/example_proto/demo/demo_pb2.py | 142 +++++++++- .../example/example_proto/demo/demo_pb2.pyi | 45 +++ example/proto_pydanticv1/demo_gen_code.py | 13 + ...gen_code_by_text_comment_protobuf_field.py | 13 + .../demo_gen_code_by_text_comment_pyi.py | 13 + .../example/example_proto/demo/demo_p2p.py | 18 ++ .../example/example_proto/demo/demo_pb2.py | 12 +- .../example/example_proto/demo/demo_pb2.pyi | 45 +++ images/protobuf-to-pydantic_index.drawio | 135 ++++----- images/protobuf-to-pydantic_index.png | Bin 374250 -> 372230 bytes protobuf_to_pydantic/gen_model.py | 14 +- protobuf_to_pydantic/plugin/config.py | 54 ++-- .../plugin/field_desc_proto_to_code.py | 2 +- protobuf_to_pydantic/template/__init__.py | 2 +- tests/base/base_p2p_validate.py | 14 +- .../test_p2p_validate.py | 2 +- 32 files changed, 1010 insertions(+), 372 deletions(-) diff --git a/README.md b/README.md index 8c9d1f8..c0c49d1 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ pip install protobuf_to_pydantic[all] # Usage ## 1.code generation `protobuf-to-pydantic` currently has two methods to generate `Pydantic Model` objects based on Protobuf files.: -- 1: Use the `Protoc` plug-in to generate the corresponding `Python` code file through the Protobuf file。 -- 2: Generate the corresponding `Pydantic Model` object through the `Message` object in `Python` runtime。 +- 1: Plugin Mode: Use the `Protoc` plug-in to generate the corresponding `Python` code file through the Protobuf file。 +- 2: Runtime Mode: Generate the corresponding `Pydantic Model` object through the `Message` object in `Python` runtime。 -### 1.1.Directly generate `Pydantic Model` code files through plug-ins +### 1.1.Plugin Mode #### 1.1.0.Install dependencies The `protobuf-to-pydantic` plug-in depends on `mypy-protobuf`, need to install `mypy-protobuf` through the following command first: ```bash @@ -71,9 +71,9 @@ After running the command, the `protobuf-to-pydantic` plugin writes the generate #### 1.1.2.Plug-in configuration The `protobuf-to-pydantic` plugin supports loading configuration by reading a `Python` file。 -> In order to ensure that the variables of the configuration file can be introduced normally, the configuration file must be stored in the current path of the running command.。 +> In order to ensure that the variables of the configuration file can be introduced normally, the configuration file should be stored in the current path where the command is run. -An example configuration that can be read by `protobuf-to-pydantic` is as follows: +An example configuration that can be read by 'protobuf_to_pydantic' would look like: ```Python import logging @@ -83,10 +83,14 @@ from google.protobuf.any_pb2 import Any # type: ignore from pydantic import confloat, conint from pydantic.fields import FieldInfo -from protobuf_to_pydantic.template import CommentTemplate +from protobuf_to_pydantic.template import Template # Configure the log output format and log level of the plugin, which is very useful when debugging -logging.basicConfig(format="[%(asctime)s %(levelname)s] %(message)s", datefmt="%y-%m-%d %H:%M:%S", level=logging.DEBUG) +logging.basicConfig( + format="[%(asctime)s %(levelname)s] %(message)s", + datefmt="%y-%m-%d %H:%M:%S", + level=logging.DEBUG +) class CustomerField(FieldInfo): @@ -107,7 +111,7 @@ local_dict = { # Specifies the start of key comments comment_prefix = "p2p" # Specify the class of the template, can extend the template by inheriting this class, see the chapter on custom templates for details -desc_template: Type[CommentTemplate] = CommentTemplate +template: Type[Template] = Template # Specify the protobuf files of which packages to ignore, and the messages of the ignored packages will not be parsed ignore_pkg_list: List[str] = ["validate", "p2p_validate"] # Specifies the generated file name suffix (without .py) @@ -127,27 +131,32 @@ In addition to the configuration options in the example configuration file, the `protobuf-to-pydantic` plug-in also supports other configuration options. The specific configuration instructions are as follows: -| Configuration name | Functional module | Type | Hidden meaning | -|-------------------------------|----------------------------------------|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| -| local_dict | Template | dict | Holds variables for the `local` template | -| desc_template | Template | protobuf_to_pydantic.desc_template.DescTemplate | Implementation of the template class | -| comment_prefix | Template | str | Comment prefix.Only strings with a fixed prefix will be used by the template | -| customer_import_set | Code generation | `Set[str]` | A collection of custom import statements, such as `from typing import Set`or `import typing`, that will write data in order to the source code file | -| customer_deque | Code generation | `deque[str]` | Custom source file content, used to add custom content | -| module_path | str | str | Used to define the root path of the project or module, which helps `protobuf-to-pydantic`to better automatically generate module import statements | -| pyproject_file_path | Code generation | str | Define the pyproject file path, which defaults to the current project path | -| code_indent | Code generation | int | Defines the number of indentation Spaces in the code; the default is 4 | -| ignore_pkg_list | Code generation(Limit plug-ins only) | `list[str]` | Definition ignores parsing of the specified package file | -| base_model_class | Model Code generation, Code generation | `Type[BaseModel]` | Define the parent class of the generated Model | -| file_name_suffix | Code generation | str | Define the generated file suffix, default `_p2p.py` | -| file_descriptor_proto_to_code | Code generation(Limit plug-ins only) | `Type[FileDescriptorProtoToCode]` | Define the `FileDescriptorProtoToCode` to use | - +| Configuration name | Functional module | Type | Hidden meaning | +|-------------------------------|----------------------------------------|-------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| local_dict | Template | dict | Holds variables for the `local` template | +| template | Template | protobuf_to_pydantic.template.Template | Implementation of the template class | +| comment_prefix | Template | str | Comment prefix.Only strings with a fixed prefix will be used by the template | +| parse_comment | comment(plugin only) | bool | If true, the annotation rule is compatible | +| customer_import_set | Code generation | `Set[str]` | A collection of custom import statements, such as `from typing import Set`or `import typing`, that will write data in order to the source code file | +| customer_deque | Code generation | `deque[str]` | Custom source file content, used to add custom content | +| module_path | str | str | Used to define the root path of the project or module, which helps `protobuf-to-pydantic`to better automatically generate module import statements | +| pyproject_file_path | Code generation | str | Define the pyproject file path, which defaults to the current project path | +| code_indent | Code generation | int | Defines the number of indentation Spaces in the code; the default is 4 | +| ignore_pkg_list | Code generation(plugin only) | `list[str]` | Definition ignores parsing of the specified package file | +| base_model_class | Model Code generation, Code generation | `Type[BaseModel]` | Define the parent class of the generated Model | +| file_name_suffix | Code generation | str | Define the generated file suffix, default `_p2p.py` | +| file_descriptor_proto_to_code | Code generation(plugin only) | `Type[FileDescriptorProtoToCode]` | Define the `FileDescriptorProtoToCode` to use | +| protobuf_type_config | Code generation(plugin only) | `Dict[str, ProtobufTypeConfigModel]` | Compatible with non-standard ones Message, See[ConfigModel note](https://github.com/so1n/protobuf_to_pydantic/blob/master/protobuf_to_pydantic/plugin/config.py) | +| pkg_config |Code generation(plugin only)| `Dict[str, "ConfigModel"]` | Adapt the corresponding configuration for each PKG | +> Note: +> - 1:For more information, see the configuration instructions[/protobuf_to_pydantic/plugin/config.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/protobuf_to_pydantic/plugin/config.py) +> - 2:See for directions of use[/example/plugin_config.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/plugin_config.py) #### 1.1.3.buf-cli If you are using `buf-cli` to manage Protobuf files, then you can also use `protobuf-to-pydantic` in `buf-cli`, See [How to use `protobuf-to-pydantic` in `buf-cli`](https://github.com/so1n/protobuf_to_pydantic/blob/master/buf-plugin/README.md) -### 1.2.Generate a `Pydantic Model` object in Python runtime +### 1.2.Runtime Mode `protobuf_to_pydantic` can generate the corresponding `PydanticModel` object based on the `Message` object at runtime。 For example, the `UserMessage` in the following Protobuf file named `demo.proto`: @@ -212,10 +221,88 @@ The `msg_to_pydantic_model` func is customizable just like plugins, with the fol | local_dict | Variables used by the `local` template | | pydantic_base | Generates the parent class of the `Pydantic Model` object | | pydantic_module | Generate the `Module` of the `Pydantic Model` object | -| desc_template | Template class to use | +| template | Template class to use | | message_type_dict_by_type_name | Protobuf type mapping to `Python` type | | message_default_factory_dict_by_type_name | Protobuf type mapping to the Python type factory | +Among them, `parse_msg_desc_method` defines the rule information where `protobuf_to_pydantic` obtains the Message object. + +### 1.2.1.parse_msg_desc_method +By default, the value of `parse_msg_desc_method` is empty. In this case, `protobuf_to_pydantic` obtains the parameter validation rules through the Option of the Message object. + +If the parameter validation rules are declared through comments, then `protobuf_to_pydantic` can only obtain the parameter validation rules through the other. + +- 1:The value of `parse_msg_desc_method` is the `Python` module corresponding to `Message` + + In this case, `protobuf-to-pydantic` can obtain additional information about each field in the Message object through the comments in the `.pyi` file corresponding to the `Python` module during the running process. + For example, in the above sample code, the `Python` module corresponding to `demo_pb2.UserMessage` is `demo_pb2`. + + > Note: This feature requires the use of the [mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf) plug-in when generating the corresponding `Python` code from the Protobuf file, and the specified pyi file output path must be the same as the generated `Python` code path to take effect. + > Before execution, please install `protobuf-to-pydantic` using the `python -m pip install protobuf-to-pydantic[mypy-protobuf]` command + +- 2:The value of `parse_msg_desc_method` is the path to the Protobuf file + + In addition to obtaining comments through `.pyi` files, `protobuf-to-pydantic` also supports obtaining information about each field through comments in the Protobuf file to which the Message object belongs. + Using this feat is very simple. Just set the value of `parse_msg_desc_method` to the root directory path specified when the Message object is generated. + + > When using this method, make sure to install `protobuf-to-pydantic` via `python -m pip install protobuf-to-pydantic[lark]` and that the Protobuf files are present in your project. + + For example, the project structure of the `protobuf-to-pydantic` sample code is as follows: + ```bash + ./protobuf_to_pydantic/ + ├── example/ + │ ├── python_example_proto_code/ + │ └── example_proto/ + ├── protobuf_to_pydantic/ + └── / + ``` + The Protobuf file is stored in the `example/example_proto` folder, and then run the following command in the `example` directory to generate the `Python` code file corresponding to Protobuf: + ```bash + cd example + + python -m grpc_tools.protoc + --python_out=./python_example_proto_code \ + --grpc_python_out=./python_example_proto_code \ + -I. \ + # or + protoc + --python_out=./python_example_proto_code \ + --grpc_python_out=./python_example_proto_code \ + -I. \ + ``` + Then the path that needs to be filled in for `parse_msg_desc_method` is `./protobuf_to_pydantic/example`. + For example, the following sample code: + ```python + # pydantic Version v1 + from typing import Type + from protobuf_to_pydantic import msg_to_pydantic_model + from pydantic import BaseModel + + # import protobuf gen python obj + from example.proto_3_20_pydanticv1.example.example_proto.demo import demo_pb2 + + UserModel: Type[BaseModel] = msg_to_pydantic_model( + demo_pb2.UserMessage, parse_msg_desc_method="./protobuf_to_pydantic/example" + ) + print( + { + k: v.field_info + for k, v in UserModel.__fields__.items() + } + ) + # output + # { + # 'uid': FieldInfo(default=PydanticUndefined, title='UID', description='user union id', extra={'example': '10086'}), + # 'age': FieldInfo(default=0, title='use age', ge=0, extra={'example': 18}), + # 'height': FieldInfo(default=0.0, ge=0, le=2, extra={}), + # 'sex': FieldInfo(default=0, extra={}), + # 'is_adult': FieldInfo(default=False, extra={}), + # 'user_name': FieldInfo(default='', description='user name', min_length=1, max_length=10, extra={'example': 'so1n'}) + # } + ``` + As you can see, the only difference in this code is the value of `parse_msg_desc_method`, but the output is the same. + +### 1.3.Generate files In addition to generating the corresponding `Pydantic Model` object at runtime, `protobuf-to-pydantic` also supports converting `Pydantic Model` objects to Python code text at runtime (only compatible with `Pydantic Model` objects generated by `protobuf-to-pydantic`). @@ -250,7 +337,8 @@ Among them, text annotations and P2P rules are consistent, they both support mos > NOTE: > - 1.Text annotation rules are not the focus of subsequent functional iterative development, and it is recommended to use P2P verification rules. -> - 2.`Protoc Plug-in` only support `PGV` and `P2P` rule. +> - 2.In plugin mode, annotation rules are written slightly differently, See[demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/p2p_validate_by_comment/demo.proto) +> - 3.The plugin mode automatically selects the most suitable parameter verification rule. ### 2.1.Text annotations @@ -293,7 +381,6 @@ In this example, each annotation that can be used by `protobuf_to_pydantic` star > Note: > - 1.Currently only single-line comments are supported and comments must be a complete Json data (no line breaks). -> - 2.multi line comments are not supported。 When these annotations are written, `protobuf_to_pydantic` will bring the corresponding information for each field when converting the Message into the corresponding `Pydantic.BaseModel` object, as follows: @@ -325,73 +412,6 @@ print( ``` It can be seen that the output fields carry the corresponding information, which is consistent with the comments of the Protobuf file. -In addition, this code differs from the previous section in that the `msg_to_pydantic_model` function has a keyword argument named `parse_msg_desc_method` and its value is the 'demo_pb2' module. -This parameter enables `protobuf-to-pydantic` to obtain additional information about each field in the Message object through comments in the `.pyi` file of the `demo_pb2` module. - -> Note: This function needs to use the [mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf) plugin when generating the corresponding `Python code through the Protobuf file, and the specified pyi file output path is the same as the generated `Python` code path to take effect. -> And need to install `protobuf-to-pydantic` via the `python -m pip install protobuf-to-pydantic[mypy-protobuf]` command - -In addition to getting comments from the `.pyi` file, `protobuf-to-pydantic` also supports getting comment information for each field through comments on the Protobuf file to which the Message object belongs. -Using this feature is as simple as setting the value of `parse_msg_desc_method` to the root directory path specified when the Message object was generated. - -> When using this method, make sure to install `protobuf-to-pydantic` via `python -m pip install protobuf-to-pydantic[lark]`, and also make sure that the Protobuf file exists in the project. - -For example, the project structure of the `protobuf-to-pydantic` sample code is as follows: -```bash -./protobuf_to_pydantic/ -├── example/ -│ ├── python_example_proto_code/ -│ └── example_proto/ -├── protobuf_to_pydantic/ -└── / -``` - -The Protobuf file is stored in the `example/example_proto` folder, and then run the following command in the `example` directory to generate the `Python` code file corresponding to Protobuf: -```bash -cd example - -python -m grpc_tools.protoc - --python_out=./python_example_proto_code \ - --grpc_python_out=./python_example_proto_code \ - -I. \ -# or -protoc - --python_out=./python_example_proto_code \ - --grpc_python_out=./python_example_proto_code \ - -I. \ -``` -Then the path that needs to be filled in for `parse_msg_desc_method` at this time is `./protobuf_to_pydantic/example`. -The following sample code: -```python -# pydantic Version v1 -from typing import Type -from protobuf_to_pydantic import msg_to_pydantic_model -from pydantic import BaseModel - -# import protobuf gen python obj -from example.proto_3_20_pydanticv1.example.example_proto.demo import demo_pb2 - -UserModel: Type[BaseModel] = msg_to_pydantic_model( - demo_pb2.UserMessage, parse_msg_desc_method="./protobuf_to_pydantic/example" -) -print( - { - k: v.field_info - for k, v in UserModel.__fields__.items() - } -) -# output -# { -# `uid`: FieldInfo(default=PydanticUndefined, title=`UID`, description=`user union id`, extra={`example`: `10086`}), -# `age`: FieldInfo(default=0, title=`use age`, ge=0, extra={`example`: 18}), -# `height`: FieldInfo(default=0.0, ge=0, le=2, extra={}), -# `sex`: FieldInfo(default=0, extra={}), -# `is_adult`: FieldInfo(default=False, extra={}), -# `user_name`: FieldInfo(default=``, description=`user name`, min_length=1, max_length=10, extra={`example`: `so1n`}) -# } -``` -As you can see, the only difference in this code is the value of the `parse_msg_desc_method`, but through the output result, you can see that the field carries the same information as the result obtained through the module. - ### 2.2.PGV(protoc-gen-validate) At present, the commonly used parameter verification project in the Protobuf ecosystem is [protoc-gen-validate](https://github.com/envoyproxy/protoc-gen-validate), It has become a common standard in Protobuf because it supports multiple languages and requires only one writing of `PGV` rules to make the generated `Message` object support the corresponding validation rules. @@ -432,7 +452,10 @@ print( > Note: > - 1.For the usage of `PGV`, see: [protoc-gen-validate doc](https://github.com/bufbuild/protoc-gen-validate/blob/v0.10.2-SNAPSHOT.17/README.md) -> - 2.Need to install `PGV` through `pip install protoc_gen_validate` or download [validate.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/common/validate.proto) to the protobuf directory in the project to write pgv rules in the Protobuf file. +> - 2.There are three ways to introduce validate +> - 2.1.Install `PGV` through `pip install protoc_gen_validate` +> - 2.2.Download [validate.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/common/validate.proto)to the protobuf directory in the project。 +> - 2.3.Install `PGV `through [buf-cli](https://github.com/so1n/protobuf_to_pydantic/blob/master/buf-plugin/README_ZH.md) ### 2.3.P2P @@ -527,7 +550,7 @@ print( ### 2.4.`P2P` and text annotation rule other parameter support The `protobuf-to-pydantic` text annotation rules and the `P2P` rules support most of the parameters in `FieldInfo`, as described in the [Pydantic Field doc](https://docs.pydantic.dev/latest/usage/fields/)。 -> The new parameters added to `Pydantic V2` will be supported in version 2.1, for now `P2P` rule naming is still written on the basis of `Pydantic V1`, but automatic mapping to `Pydantic V2` naming is supported. +> The new parameters added to `Pydantic V2` will be supported in next version , for now `P2P` rule naming is still written on the basis of `Pydantic V1`, but automatic mapping to `Pydantic V2` naming is supported. Other partial changes in meaning and new parameters are described as follows: @@ -737,7 +760,7 @@ class UserPayMessage(BaseModel): exp: float = FieldInfo() ``` #### 2.5.5.Customized templates -Currently `protobuf-to-pydantic` only supports a few simple templates, if have more template needs, can extend the templates by inheriting the `DescTemplate` class. +Currently `protobuf-to-pydantic` only supports a few simple templates, if have more template needs, can extend the templates by inheriting the `Template` class. For example, there is an odd feature that requires the default value of a field to be the timestamp of the time when the `Pydantic Model` object was generated, but the timestamps used are available in lengths of 10 and 13, so the following Protobuf file needs to be written to support defining the length of the timestamps: ```protobuf @@ -755,33 +778,33 @@ As you can see, the Protobuf file customizes the syntax of `p2p@timestamp|{x}`, ```python import time -from protobuf_to_pydantic.gen_model import CommentTemplate +from protobuf_to_pydantic.gen_model import Template -class CustomDescTemplate(CommentTemplate): - def template_timestamp(self, length_str: str) -> int: - timestamp: float = time.time() - if length_str == "10": - return int(timestamp) - elif length_str == "13": - return int(timestamp * 100) - else: - raise KeyError(f"timestamp template not support value:{length_str}") +class CustomTemplate(Template): + def template_timestamp(self, length_str: str) -> int: + timestamp: float = time.time() + if length_str == "10": + return int(timestamp) + elif length_str == "13": + return int(timestamp * 100) + else: + raise KeyError(f"timestamp template not support value:{length_str}") from .demo_pb2 import TimestampTest # fake code from protobuf_to_pydantic import msg_to_pydantic_model msg_to_pydantic_model( - TimestampTest, - desc_template=CustomDescTemplate # <-- Use a custom template class + TimestampTest, + template=CustomTemplate # <-- Use a custom template class ) ``` -This code first creates a class `CustomDescTemplate` that inherits from `DescTemplate`. -`DescTemplate` will forwards to the corresponding `template_{template name}` method based on the naming of the template, so this class needs to define the `template_timestamp` method to implement the `p2p@timestamp` template functionality. +This code first creates a class `CustomTemplate` that inherits from `Template`. +During the execution process, it is found that when the parameter verification rule starts with `p2p@`, the parameter will be sent to the `template_{template name}` method corresponding to the `Template` class, so `CustomTemplate` defines the `template_timestamp` method to implement the `p2p@timestamp` template function. In addition, the `length_str` variable received in this method is either 10 in `p2p@timestamp|10` or 13 in `p2p@timestamp|13`. -Then load the `CustomDescTemplate` through the `msg_to_pydantic_model` function, then the following code will be generated (assuming that the code is generated at a timestamp of 1600000000): +Then load the `CustomTemplate` through the `msg_to_pydantic_model` function, then the following code will be generated (assuming that the code is generated at a timestamp of 1600000000): ```python from pydantic import BaseModel from pydantic.fields import FieldInfo @@ -790,6 +813,9 @@ class TimestampTest(BaseModel): timestamp_10: int = FieldInfo(default=1600000000) timestamp_13: int = FieldInfo(default=1600000000000) ``` + +> Note: In plugin mode, you can declare a template class to be loaded through a configuration file. + ## 3.Code format The code generated directly through `protobuf-to-pydantic` is not perfect, but it is possible to indirectly generate code that conforms to the `Python` specification through different formatting tools. Currently, `protobuf-to-pydantic` supports formatting tools such as `autoflake`, `black` and `isort`. If the corresponding formatting tool is installed in the current `Python` environment, then `protobuf-to-pydantic` will call the tool to format the generated code before outputting it to a file. @@ -855,9 +881,11 @@ Generate `Pydantic Model`(Pydantic V1): [proto_pydanticv1/demo_gen_code_by_p2p.p Generate `Pydantic Model`(Pydantic V2): [proto_pydanticv2/demo_gen_code_by_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv2/demo_gen_code_by_p2p.py) ### 4.5.Protoc Plugin-in -Protobuf field: [demo/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/demo/demo.proto),[validate/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/validate/demo.proto),[p2p_validate/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/p2p_validate/demo.proto) - -> Note: The Protoc plugin only supports P2P and PGV rules +Protobuf field: + - [demo/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/demo/demo.proto) + - [validate/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/validate/demo.proto) + - [p2p_validate/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/p2p_validate/demo.proto) + - [p2p_validate_by_comment/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/p2p_validate_by_comment/demo.proto) `Pydantic Model` generated via `demo/demo.proto`(Pydantic V1):[example_proto/demo/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py) @@ -865,8 +893,12 @@ Protobuf field: [demo/demo.proto](https://github.com/so1n/protobuf_to_pydantic/b `Pydantic Model` generated via `validate/demo.proto`(Pydantic V1):[example_proto/validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py) -`Pydantic Model` generated via `validate/demo.proto`(Pydantic V1):[example_proto/validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py) +`Pydantic Model` generated via `validate/demo.proto`(Pydantic V2):[example_proto/validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py) `Pydantic Model` generated via `p2p_validate/demo.proto`(Pydantic V1):[example_proto/p2p_validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py) -`Pydantic Model` generated via `p2p_validate/demo.proto`(Pydantic V1):[example_proto/p2p_validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py) +`Pydantic Model` generated via `p2p_validate/demo.proto`(Pydantic V2):[example_proto/p2p_validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py) + +`Pydantic Model` generated via `p2p_validate_by_comment/demo.proto`(Pydantic V1):[example/example_proto/p2p_validate_by_comment](https://github.com/so1n/protobuf_to_pydantic/tree/master/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment) + +`Pydantic Model` generated via `p2p_validate_by_comment/demo.proto`(Pydantic V2):[example/example_proto/p2p_validate_by_comment](https://github.com/so1n/protobuf_to_pydantic/tree/master/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment) diff --git a/README_ZH.md b/README_ZH.md index 20a3f1f..7c7a404 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -31,10 +31,10 @@ pip install protobuf_to_pydantic[all] # 使用 ## 1.代码生成 `protobuf-to-pydantic`目前拥有两种方法基于Protobuf文件生成`Pydantic Model`对象: -- 1: 以`Protoc`插件的方式通过Protobuf文件生成对应的`Python`代码文件。 -- 2: 在`Python`运行时根据`Message`对象生成对应的`Pydantic Model`对象。 +- 1: 插件模式:以`Protoc`插件的方式通过Protobuf文件生成对应的`Python`代码文件。 +- 2: 运行时模式:在`Python`运行时根据`Message`对象生成对应的`Pydantic Model`对象。 -### 1.1.通过插件直接生成`Pydantic Model`代码文件 +### 1.1.插件模式 #### 1.1.0.安装依赖 `protobuf-to-pydantic`插件依赖`mypy-protobuf`,需要先通过如下命令安装`mypy-protobuf`: ```bash @@ -66,9 +66,9 @@ protoc -I. --protobuf-to-pydantic_out=. example.proto #### 1.1.2.插件的配置 `protobuf-to-pydantic`插件支持通过读取一个`Python`文件来加载配置。 -> 为了保证能够正常的引入配置文件的变量,配置文件必须存放在运行命令的当前路径下。 +> 为了保证能够正常的引入配置文件的变量,配置文件应尽量存放在运行命令的当前路径下。 -一个可以被`protobuf-to-pydantic`读取的示例配置内容如下: +一个可以被`protobuf-to-pydantic`读取的配置内容大概如下: ```Python import logging @@ -78,10 +78,14 @@ from google.protobuf.any_pb2 import Any # type: ignore from pydantic import confloat, conint from pydantic.fields import FieldInfo -from protobuf_to_pydantic.template import CommentTemplate +from protobuf_to_pydantic.template import Template # 配置插件的日志输出格式,和日志等级,在DEBUG的时候非常有用 -logging.basicConfig(format="[%(asctime)s %(levelname)s] %(message)s", datefmt="%y-%m-%d %H:%M:%S", level=logging.DEBUG) +logging.basicConfig( + format="[%(asctime)s %(levelname)s] %(message)s", + datefmt="%y-%m-%d %H:%M:%S", + level=logging.DEBUG +) class CustomerField(FieldInfo): @@ -102,7 +106,7 @@ local_dict = { # 指定关键注释的开头 comment_prefix = "p2p" # 指定模板的类,可以通过继承该类拓展模板,详见自定义模板章节 -desc_template: Type[CommentTemplate] = CommentTemplate +template: Type[Template] = Template # 指定要忽略的哪些package的protobuf文件,被忽略的package的message不会被解析 ignore_pkg_list: List[str] = ["validate", "p2p_validate"] # 指定生成的文件名后缀(不包含.py) @@ -121,28 +125,34 @@ protoc -I. --protobuf-to-pydantic_out=config_path=plugin_config.py:. example.pro 除了示例的配置文件中配置选项外,`protobuf-to-pydantic`插件还支持其他的配置选项,具体的配置说明如下: -|配置名|所属功能|类型|含义| -|---|---|---|---| -|local_dict|模板|dict|存放供`local`模板使用的变量| -|desc_template|模板|protobuf_to_pydantic.desc_template.DescTemplate|模板类的实现| -|comment_prefix|模板|str|注释前缀,只有固定前缀的字符串才会被模板使用| -|customer_import_set|代码生成|`Set[str]`|自定义import语句的集合,会写入到源码文件中,如`from typing import Set`或者是`import typing`| -|customer_deque|代码生成|`deque[str]`|自定义源码文件内容,用于增加自定义内容| -|module_path|代码生成|str|用于定义项目/模块的根路径,辅助`protobuf-to-pydantic`能更好的自动生成模块的引入语句| -|pyproject_file_path|代码生成|str|定义pyproject文件路径,默认为当前项目的路径| -|code_indent|代码生成|int|定义代码的缩进空格数量,默认为4| -|ignore_pkg_list|代码生成(只限插件)|`list[str]`|定义忽略指定package文件的解析| -|base_model_class|Model生成,代码生成|`Type[BaseModel]`|定义生成的Model的父类| -|file_name_suffix|代码生成|str|定义生成的文件后缀,默认为`_p2p.py`| -|file_descriptor_proto_to_code|代码生成(只限Protoc插件)|`Type[FileDescriptorProtoToCode]`|定义使用的FileDescriptorProtoToCode| +| 配置名 | 所属功能 | 类型 | 含义 | +|-------------------------------|------------------|-------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| local_dict | 模板 | dict | 存放供`local`模板使用的变量 | +| template | 模板 | protobuf_to_pydantic.template.Template | 模板类的实现 | +| comment_prefix | 模板 | str | 注释前缀,只有固定前缀的字符串才会被模板使用 | +| parse_comment | 注释(只限Protoc插件) | bool | 如果为True,则会兼容注释形式的参数校验规则 | +| customer_import_set | 代码生成 | `Set[str]` | 自定义import语句的集合,会写入到源码文件中,如`from typing import Set`或者是`import typing` | +| customer_deque | 代码生成 | `deque[str]` | 自定义源码文件内容,用于增加自定义内容 | +| module_path | 代码生成 | str | 用于定义项目/模块的根路径,辅助`protobuf-to-pydantic`能更好的自动生成模块的引入语句 | +| pyproject_file_path | 代码生成 | str | 定义pyproject文件路径,默认为当前项目的路径 | +| code_indent | 代码生成 | int | 定义代码的缩进空格数量,默认为4 | +| ignore_pkg_list | 代码生成(只限插件) | `list[str]` | 定义忽略指定package文件的解析 | +| base_model_class | Model生成,代码生成 | `Type[BaseModel]` | 定义生成的Model的父类 | +| file_name_suffix | 代码生成 | str | 定义生成的文件后缀,默认为`_p2p.py` | +| file_descriptor_proto_to_code | 代码生成(只限Protoc插件) | `Type[FileDescriptorProtoToCode]` | 定义使用的FileDescriptorProtoToCode | +| protobuf_type_config | 代码生成(只限Protoc插件) | `Dict[str, ProtobufTypeConfigModel]` | 兼容不规范的Message,具体见[ConfigModel说明](https://github.com/so1n/protobuf_to_pydantic/blob/master/protobuf_to_pydantic/plugin/config.py) | +| pkg_config |代码生成(只限Protoc插件)| `Dict[str, "ConfigModel"]` | 为每一个pkg适配对应的配置 | +> Note: +> - 1:配置的具体说明见[/protobuf_to_pydantic/plugin/config.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/protobuf_to_pydantic/plugin/config.py) +> - 2:使用方法见[/example/plugin_config.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/plugin_config.py) #### 1.1.3.buf-cli 如果你是使用`buf-cli`来管理Protobuf文件,那么也可以在`buf-cli`中使用`protobuf-to-pydantic`,具体请访问[如何在`buf-cli`中使用使用`protobuf-to-pydantic`](https://github.com/so1n/protobuf_to_pydantic/blob/master/buf-plugin/README_ZH.md)了解详情。 -### 1.2.在Python运行时生成`Pydantic Model`对象 -`protobuf-to-pydantic`可以在运行时根据`Message`对象生成对应的 `Pydantic Model`对象。 +### 1.2.运行时模式 +`protobuf-to-pydantic`可以在运行时根据`Message`对象的信息生成对应的 `Pydantic Model`对象。 例如下面一个名为`demo.proto`的Protobuf文件中的`UserMessage`: ```protobuf @@ -206,12 +216,88 @@ print( |local_dict| `local`模板使用的变量 | |pydantic_base| 生成`Pydantic Model`对象的父类 | |pydantic_module| 生成`Pydantic Model`对象的`Module` | -|desc_template| 使用的模板类 | +|template| 使用的模板类 | |message_type_dict_by_type_name| Protobuf类型与`Python`类型的映射 | |message_default_factory_dict_by_type_name| Protobuf类型与`Python`类型工厂的映射 | +其中,`parse_msg_desc_method`是定义`protobuf_to_pydantic`从哪里获取到Message对象的规则信息。 + +### 1.2.1.parse_msg_desc_method +默认情况下,`parse_msg_desc_method`的值为空,此时`protobuf_to_pydantic`会通过Message对象的Option获取参数校验规则。 + +如果参数校验的规则是通过注释声明的,那么`protobuf_to_pydantic`只能通过另外两种形式来获取参数校验规则。 + +- 1:`parse_msg_desc_method`的值为`Message`对应的`Python`模块 + + 在这种情况下,`protobuf-to-pydantic`在运行的过程能够通过`Python`模块对应的`.pyi`文件中的的注释来获取Message对象中每个字段的附加信息。 + 比如上述示例代码中`demo_pb2.UserMessage`对应的`Python`模块为`demo_pb2`。 + + > 注:该功能需要在通过Protobuf文件生成对应的`Python`代码时使用[mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf)插件,且指定的pyi文件输出路径与生成的`Python`代码路径相同时才能生效。 + > 在执行前请通过`python -m pip install protobuf-to-pydantic[mypy-protobuf]`命令安装`protobuf-to-pydantic` +- 2:`parse_msg_desc_method`的值为Protobuf文件的路径 + + 除了通过`.pyi`文件获取注释外,`protobuf-to-pydantic`还支持通过Message对象所属的Protobuf文件的注释来获取每个字段的注释信息。 +使用这个功能很简单,只需要把`parse_msg_desc_method`的值设置为Message对象生成时指定的根目录路径即可。 + + > 在使用该方法时,请确保通过`python -m pip install protobuf-to-pydantic[lark]`安装`protobuf-to-pydantic`,同时也要确保Protobuf文件存在于项目中。 + + 比如`protobuf-to-pydantic`示例代码的项目结构如下: + ```bash + ./protobuf_to_pydantic/ + ├── example/ + │ ├── python_example_proto_code/ + │ └── example_proto/ + ├── protobuf_to_pydantic/ + └── / + ``` + 其中Protobuf文件存放在`example/example_proto`文件夹中,然后在`example`目录下运行如下命令生成Protobuf对应的`Python`代码文件: + ```bash + cd example + + python -m grpc_tools.protoc + --python_out=./python_example_proto_code \ + --grpc_python_out=./python_example_proto_code \ + -I. \ + # or + protoc + --python_out=./python_example_proto_code \ + --grpc_python_out=./python_example_proto_code \ + -I. \ + ``` + 那么此时`parse_msg_desc_method`需要填写的路径是`./protobuf_to_pydantic/example`。 + 比如下面的示例代码: + ```python + # pydantic Version v1 + from typing import Type + from protobuf_to_pydantic import msg_to_pydantic_model + from pydantic import BaseModel + + # import protobuf gen python obj + from example.proto_3_20_pydanticv1.example.example_proto.demo import demo_pb2 + + UserModel: Type[BaseModel] = msg_to_pydantic_model( + demo_pb2.UserMessage, parse_msg_desc_method="./protobuf_to_pydantic/example" + ) + print( + { + k: v.field_info + for k, v in UserModel.__fields__.items() + } + ) + # output + # { + # 'uid': FieldInfo(default=PydanticUndefined, title='UID', description='user union id', extra={'example': '10086'}), + # 'age': FieldInfo(default=0, title='use age', ge=0, extra={'example': 18}), + # 'height': FieldInfo(default=0.0, ge=0, le=2, extra={}), + # 'sex': FieldInfo(default=0, extra={}), + # 'is_adult': FieldInfo(default=False, extra={}), + # 'user_name': FieldInfo(default='', description='user name', min_length=1, max_length=10, extra={'example': 'so1n'}) + # } + ``` + 可以看到,这份代码的唯一区别就是`parse_msg_desc_method`的值不同,但是输出结果中每个字段携带的信息与通过模块获取的结果是一样的。 +### 1.3.直接生成文件 除了在运行时生成对应的`Pydantic Model`对象外,`protobuf-to-pydantic`还支持在运行时将`Pydantic Model`对象转为对应的`Python`代码文本(仅兼容`protobuf-to-pydantic`生成的`Pydantic Model`对象)。 其中,`pydantic_model_to_py_code`用于生成代码源码,`pydantic_model_to_py_file`用于生成代码文件,`pydantic_model_to_py_file`函数的示例代码如下: ```Python @@ -228,20 +314,20 @@ pydantic_model_to_py_file( 代码运行的时候,会先把`demo_pb2.NestedMessage`转换为`Pydantic Model`对象,接着传入到`pydantic_model_to_py_file`函数中,由`pydantic_model_to_py_file`生成对应的源码内容再写入到`demo_gen_code.py`文件中。 ## 2.参数校验 -在上一节中,Protobuf文件生成的`Pydantic Model`对象非常简单,这是因为Protobuf文件没有足够的参数验证相关信息。 -为了使生成的`Pydantic Model`对象中的每个字段都拥有参数校验功能,需要在Protobuf文件中完善字段对应的参数校验规则。 - -目前`protobuf-to-pydantic`支持的校验规则有三种: +在上一节中,Protobuf文件生成的`Pydantic Model`对象非常简单,这是因为Protobuf文件没有足够的参数验证信息。 +为了使生成的`Pydantic Model`对象中的每个字段都拥有参数校验功能,需要完善Protobuf文件中每个Message的字段的参数校验规则。 +目前`protobuf-to-pydantic`支持下面三种参数校验规则: - 1.文本注释 - 2.PGV(protoc-geb-validate) - 3.P2P -通过这些规则,`protobuf-to-pydantic`生成的`Pydantic Model`对象将拥有参数校验功能。 +通过这些规则,可以让`protobuf-to-pydantic`生成的`Pydantic Model`对象拥有参数校验功能。 其中文本注释和P2P的规则是一致的,它们都支持`Pydantic Field`中的大多数参数,部分有变化和新增的参数见[2.4.`P2P`与文本注释的其它参数支持](#24p2p与文本注释的其它参数支持) > NOTE: > - 1.文本注释规则不是后续功能迭代开发的重点,推荐使用P2P校验规则。 -> - 2.插件生成代码只支持PGV和P2P校验规则。 +> - 2.插件模式下,文本注释的编写方法略有变化,详情请参考:[example/example_proto/p2p_validate_by_comment/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/p2p_validate_by_comment/demo.proto) +> - 3.插件模式会自动选择最适配的参数校验规则。 ### 2.1.文本注释 在Protobuf文件中可以为每个字段编写符合`protobuf-to-pydantic`要求的注释,以便`protobuf-to-pydantic`在解析Protobuf文件时能够获得到参数的校验信息,比如下面这个例子 @@ -283,7 +369,6 @@ message UserMessage { > Note: > - 1.目前只支持单行注释且注释必须是一个完整的Json数据(不能换行)。 -> - 2.不支持多行注释。 这样一来,`protobuf-to-pydantic`通过Message生成的`Pydantic Model`对象中的每个字段都拥有对应的信息,如下代码: ```python @@ -312,75 +397,11 @@ print( # 'user_name': FieldInfo(default='', description='user name', min_length=1, max_length=10, extra={'example': 'so1n'}) # } ``` -可以看出,输出的字段中都携带着对应的信息,这些数据与Protobuf文件的注释是一致的。 -除此之外,这段代码与上一节不同的是`msg_to_pydantic_model`函数多了一个名为`parse_msg_desc_method`的关键字参数且它的值为`demo_pb2`模块, -该参数会使`protobuf-to-pydantic`能够通过`demo_pb2`模块的`.pyi`文件中的的注释来获取Message对象中每个字段的附加信息。 - -> 注:该功能需要在通过Protobuf文件生成对应的`Python`代码时使用[mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf)插件,且指定的pyi文件输出路径与生成的`Python`代码路径相同时才能生效。 -> 在执行前请通过`python -m pip install protobuf-to-pydantic[mypy-protobuf]`命令安装`protobuf-to-pydantic` - -除了通过`.pyi`文件获取注释外,`protobuf-to-pydantic`还支持通过Message对象所属的Protobuf文件的注释来获取每个字段的注释信息。 -使用这个功能很简单,只需要把`parse_msg_desc_method`的值设置为Message对象生成时指定的根目录路径即可。 - -> 在使用该方法时,请确保通过`python -m pip install protobuf-to-pydantic[lark]`安装`protobuf-to-pydantic`,同时也要确保Protobuf文件存在于项目中。 - -比如`protobuf-to-pydantic`示例代码的项目结构如下: -```bash -./protobuf_to_pydantic/ -├── example/ -│ ├── python_example_proto_code/ -│ └── example_proto/ -├── protobuf_to_pydantic/ -└── / -``` -其中Protobuf文件存放在`example/example_proto`文件夹中,然后在`example`目录下运行如下命令生成Protobuf对应的`Python`代码文件: -```bash -cd example - -python -m grpc_tools.protoc - --python_out=./python_example_proto_code \ - --grpc_python_out=./python_example_proto_code \ - -I. \ -# or -protoc - --python_out=./python_example_proto_code \ - --grpc_python_out=./python_example_proto_code \ - -I. \ -``` -那么此时`parse_msg_desc_method`需要填写的路径是`./protobuf_to_pydantic/example`。 -比如下面的示例代码: -```python -# pydantic Version v1 -from typing import Type -from protobuf_to_pydantic import msg_to_pydantic_model -from pydantic import BaseModel +在执行代码后通过输出结果可以看到输出的字段中都携带着对应的信息,这些数据与Protobuf文件的注释是一致的。 -# import protobuf gen python obj -from example.proto_3_20_pydanticv1.example.example_proto.demo import demo_pb2 - -UserModel: Type[BaseModel] = msg_to_pydantic_model( - demo_pb2.UserMessage, parse_msg_desc_method="./protobuf_to_pydantic/example" -) -print( - { - k: v.field_info - for k, v in UserModel.__fields__.items() - } -) -# output -# { -# 'uid': FieldInfo(default=PydanticUndefined, title='UID', description='user union id', extra={'example': '10086'}), -# 'age': FieldInfo(default=0, title='use age', ge=0, extra={'example': 18}), -# 'height': FieldInfo(default=0.0, ge=0, le=2, extra={}), -# 'sex': FieldInfo(default=0, extra={}), -# 'is_adult': FieldInfo(default=False, extra={}), -# 'user_name': FieldInfo(default='', description='user name', min_length=1, max_length=10, extra={'example': 'so1n'}) -# } -``` -可以看到,这份代码的唯一区别就是`parse_msg_desc_method`的值不同,但是通过输出结果可以看出字段携带的信息与通过模块获取的结果一样。 ### 2.2.PGV(protoc-gen-validate) 目前Protobuf生态中常用的参数校验项目是[protoc-gen-validate](https://github.com/envoyproxy/protoc-gen-validate), -它由于支持多种语言且只要编写一次`PGV`规则就能使生成的`Message`对象虽然编程语言不同的,但都支持相同的校验规则,已然成为Protobuf中的通用标准。 +在使用的过程中,只要编写一次`PGV`规则就能生成不同编程语言,但拥有相同校验规则的`Message`对象,已然成为Protobuf中的通用标准。 > 目前`protobuf-to-pydantic`只支持[protoc-gen-validate](https://github.com/envoyproxy/protoc-gen-validate)小于1.0.0版本的规则 @@ -417,7 +438,10 @@ print( > Note: > - 1.`PGV`的使用方法见:[protoc-gen-validate doc](https://github.com/bufbuild/protoc-gen-validate/blob/v0.10.2-SNAPSHOT.17/README.md) -> - 2.使用前请通过`pip install protoc_gen_validate`安装`PGV`或者把[validate.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/common/validate.proto)下载到项目的Protobuf目录中,才能在Protobuf文件中编写pgv规则。 +> - 2.可以通过以下三种方法引入validate +> - 2.1.使用前请通过`pip install protoc_gen_validate`安装`PGV` +> - 2.2.把[validate.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/common/validate.proto)下载到项目的Protobuf目录中。 +> - 2.3.通过[buf-cli](https://github.com/so1n/protobuf_to_pydantic/blob/master/buf-plugin/README_ZH.md)安装validate ### 2.3.P2P @@ -510,7 +534,7 @@ print( ### 2.4.`P2P`与文本注释的其它参数支持 `protobuf-to-pydantic`的文本注释规则和`P2P`规则支持`FieldInfo`中的大部分参数,具体见[Pydantic Field文档](https://docs.pydantic.dev/latest/usage/fields/)。 -> `Pydantic V2`新增的参数将会在2.1版本中支持,目前`P2P`的规则命名仍是基于`Pydantic V1`编写的,但是支持自动映射为`Pydantic V2`的命名。 +> `Pydantic V2`新增的参数将会在下一个版本提供支持,目前`P2P`的规则命名仍是基于`Pydantic V1`编写的,但是支持自动映射为`Pydantic V2`的命名。 其它部分含义有变动和新增的参数说明如下: @@ -725,7 +749,7 @@ class UserPayMessage(BaseModel): exp: float = FieldInfo() ``` #### 2.5.5.自定义模板 -目前`protobuf-to-pydantic`只支持几种简单模板,如果有更多的模板需求,可以通过继承`DescTemplate`类来对模板进行拓展。 +目前`protobuf-to-pydantic`只支持几种简单模板,如果有更多的模板需求,可以通过继承`Template`类来对模板进行拓展。 比如有一个奇葩的需求,要求字段的默认值为Message对象生成`Pydantic Model`对象时的时间戳,不过使用的时间戳有长度为10位和13位两个版本, 于是需要编写如下Protobuf文件来支持定义时间戳的长度: ```protobuf @@ -743,10 +767,10 @@ message TimestampTest{ ```python import time -from protobuf_to_pydantic.gen_model import CommentTemplate +from protobuf_to_pydantic.gen_model import Template -class CustomDescTemplate(CommentTemplate): +class CustomTemplate(Template): def template_timestamp(self, length_str: str) -> int: timestamp: float = time.time() if length_str == "10": @@ -762,14 +786,14 @@ from protobuf_to_pydantic import msg_to_pydantic_model msg_to_pydantic_model( TimestampTest, - desc_template=CustomDescTemplate # <-- 使用自定义的模板类 + template=CustomTemplate # <-- 使用自定义的模板类 ) ``` -这段代码先是创建了一个继承于`DescTemplate`的`CustomDescTemplate`的类, -由于`DescTemplate`会根据模板的命名转发到对应的`template_{template name}`方法,所以这个类定义了`template_timestamp`的方法来实现`p2p@timestamp`模板功能。 +这段代码先是创建了一个继承于`Template`的`CustomTemplate`的类, +在执行的过程中,发现参数校验规则是以`p2p@`开头时就会把参数发到`Template`类对应的`template_{template name}`方法,所以`CustomTemplate`定义了`template_timestamp`的方法来实现`p2p@timestamp`模板功能。 此外,在这个方法中接收的`length_str`变量则是`p2p@timestamp|10`中的10或者是`p2p@timestamp|13`中的13。 -接着通过`msg_to_pydantic_model`函数加载`CustomDescTemplate`,那么会生成如下代码(假设在时间戳为1600000000时生成的代码): +在创建完`CustomTemplate`后,通过`msg_to_pydantic_model`函数加载`CustomTemplate`,那么会生成如下代码(假设在时间戳为1600000000时生成的代码): ```python from pydantic import BaseModel from pydantic.fields import FieldInfo @@ -778,6 +802,9 @@ class TimestampTest(BaseModel): timestamp_10: int = FieldInfo(default=1600000000) timestamp_13: int = FieldInfo(default=1600000000000) ``` + +> Note: 插件模式下可以通过配置文件声明要加载的模板类。 + ## 3.代码格式化 通过`protobuf-to-pydantic`直接生成的代码不是完美的,但是可以通过不同的格式化工具来间接的生成符合`Python`规范的代码。 目前, `protobuf-to-pydantic`支持`autoflake`, `black`和`isort`等格式化工具。如果在当前的`Python`环境中安装了对应的格式化工具,那么`protobuf-to-pydantic`会调用工具对生成的代码进行格式化再输出到文件中。 @@ -844,9 +871,11 @@ protobuf文件: [p2p_validate/demo.proto](https://github.com/so1n/protobuf_to_py 生成的`Pydantic Model`(Pydantic V2): [proto_pydanticv2/demo_gen_code_by_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv2/demo_gen_code_by_p2p.py) ### 4.5.Protoc插件 -protobuf文件: [demo/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/demo/demo.proto),[validate/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/validate/demo.proto),[p2p_validate/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/p2p_validate/demo.proto) - -> Note: Protoc插件只支持P2P和PGV校验规则 +protobuf文件: + - [demo/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/demo/demo.proto) + - [validate/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/validate/demo.proto) + - [p2p_validate/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/p2p_validate/demo.proto) + - [p2p_validate_by_comment/demo.proto](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/example_proto/p2p_validate_by_comment/demo.proto) 通过`demo/demo.proto`生成的`Pydantic Model`(Pydantic V1):[example_proto/demo/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py) @@ -854,8 +883,12 @@ protobuf文件: [demo/demo.proto](https://github.com/so1n/protobuf_to_pydantic/b 通过`validate/demo.proto`生成的`Pydantic Model`(Pydantic V1):[example_proto/validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv1/example/example_proto/validate/demo_p2p.py) -通过`validate/demo.proto`生成的`Pydantic Model`(Pydantic V1):[example_proto/validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py) +通过`validate/demo.proto`生成的`Pydantic Model`(Pydantic V2):[example_proto/validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv2/example/example_proto/validate/demo_p2p.py) 通过`p2p_validate/demo.proto`生成的`Pydantic Model`(Pydantic V1):[example_proto/p2p_validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv1/example/example_proto/p2p_validate/demo_p2p.py) -通过`p2p_validate/demo.proto`生成的`Pydantic Model`(Pydantic V1):[example_proto/p2p_validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py) +通过`p2p_validate/demo.proto`生成的`Pydantic Model`(Pydantic V2):[example_proto/p2p_validate/demo_p2p.py](https://github.com/so1n/protobuf_to_pydantic/blob/master/example/proto_pydanticv2/example/example_proto/p2p_validate/demo_p2p.py) + +通过`p2p_validate_by_comment/demo.proto`生成的`Pydantic Model`(Pydantic V1):[example/example_proto/p2p_validate_by_comment](https://github.com/so1n/protobuf_to_pydantic/tree/master/example/proto_pydanticv1/example/example_proto/p2p_validate_by_comment) + +通过`p2p_validate_by_comment/demo.proto`生成的`Pydantic Model`(Pydantic V2):[example/example_proto/p2p_validate_by_comment](https://github.com/so1n/protobuf_to_pydantic/tree/master/example/proto_pydanticv2/example/example_proto/p2p_validate_by_comment) diff --git a/example/gen_p2p_code.py b/example/gen_p2p_code.py index 5a8a850..e9dab8a 100644 --- a/example/gen_p2p_code.py +++ b/example/gen_p2p_code.py @@ -42,7 +42,7 @@ def customer_any() -> Any: now_path: pathlib.Path = pathlib.Path(__file__).absolute() -class CustomCommentTemplate(template.CommentTemplate): +class CustomCommentTemplate(template.Template): def template_timestamp(self, length_str: str) -> int: timestamp: float = 1600000000 if length_str == "10": @@ -65,7 +65,7 @@ def gen_code() -> None: "conint": conint, "customer_any": customer_any, }, - desc_template=CustomCommentTemplate, + template=CustomCommentTemplate, ) for model in message_class_list ], diff --git a/example/gen_text_comment_code.py b/example/gen_text_comment_code.py index 039900d..c647e53 100644 --- a/example/gen_text_comment_code.py +++ b/example/gen_text_comment_code.py @@ -9,10 +9,10 @@ from google.protobuf.message import Message from protobuf_to_pydantic import _pydantic_adapter, msg_to_pydantic_model, pydantic_model_to_py_file -from protobuf_to_pydantic.template import CommentTemplate +from protobuf_to_pydantic.template import Template -class CustomCommentTemplate(CommentTemplate): +class CustomCommentTemplate(Template): def template_timestamp(self, length_str: str) -> int: timestamp: float = 1600000000 if length_str == "10": @@ -59,7 +59,7 @@ def gen_code() -> None: model, parse_msg_desc_method=module, local_dict=local_dict, - desc_template=CustomCommentTemplate, + template=CustomCommentTemplate, ) for model in message_class_list ], @@ -72,7 +72,7 @@ def gen_code() -> None: model, parse_msg_desc_method=str(now_path.parent.parent), local_dict=local_dict, - desc_template=CustomCommentTemplate, + template=CustomCommentTemplate, ) for model in message_class_list ], @@ -85,7 +85,7 @@ def gen_code() -> None: model, parse_msg_desc_method=module, local_dict=local_dict, - desc_template=CustomCommentTemplate, + template=CustomCommentTemplate, ) for model in message_class_list ], @@ -97,7 +97,7 @@ def gen_code() -> None: model, parse_msg_desc_method=str(now_path.parent.parent), local_dict=local_dict, - desc_template=CustomCommentTemplate, + template=CustomCommentTemplate, ) for model in message_class_list ], diff --git a/example/p2p_validate_by_comment_gen_code.py b/example/p2p_validate_by_comment_gen_code.py index 38519e3..c33dd98 100644 --- a/example/p2p_validate_by_comment_gen_code.py +++ b/example/p2p_validate_by_comment_gen_code.py @@ -12,10 +12,10 @@ from pydantic.fields import FieldInfo from protobuf_to_pydantic import _pydantic_adapter, msg_to_pydantic_model, pydantic_model_to_py_file -from protobuf_to_pydantic.template import CommentTemplate +from protobuf_to_pydantic.template import Template -class CustomCommentTemplate(CommentTemplate): +class CustomCommentTemplate(Template): def template_timestamp(self, length_str: str) -> int: timestamp: float = 1600000000 if length_str == "10": @@ -77,7 +77,7 @@ def gen_code() -> None: model, parse_msg_desc_method=module, local_dict=local_dict, - desc_template=CustomCommentTemplate, + template=CustomCommentTemplate, ) for model in message_class_list ], @@ -92,7 +92,7 @@ def gen_code() -> None: model, parse_msg_desc_method=str(now_path.parent.parent), local_dict=local_dict, - desc_template=CustomCommentTemplate, + template=CustomCommentTemplate, ) for model in message_class_list ], @@ -105,7 +105,7 @@ def gen_code() -> None: model, parse_msg_desc_method=module, local_dict=local_dict, - desc_template=CustomCommentTemplate, + template=CustomCommentTemplate, ) for model in message_class_list ], @@ -119,7 +119,7 @@ def gen_code() -> None: model, parse_msg_desc_method=str(now_path.parent.parent), local_dict=local_dict, - desc_template=CustomCommentTemplate, + template=CustomCommentTemplate, ) for model in message_class_list ], diff --git a/example/plugin_config.py b/example/plugin_config.py index 134b07a..05bc375 100644 --- a/example/plugin_config.py +++ b/example/plugin_config.py @@ -8,9 +8,9 @@ from pydantic.fields import FieldInfo from protobuf_to_pydantic.plugin.config import SubConfigModel +from protobuf_to_pydantic.template import Template from . import single_config_pkg_plugin_config -from protobuf_to_pydantic.template import CommentTemplate logging.basicConfig(format="[%(asctime)s %(levelname)s] %(message)s", datefmt="%y-%m-%d %H:%M:%S", level=logging.INFO) @@ -23,7 +23,7 @@ def customer_any() -> Any: return Any # type: ignore -class CustomCommentTemplate(CommentTemplate): +class CustomCommentTemplate(Template): def template_timestamp(self, length_str: str) -> int: timestamp: float = 1600000000 if length_str == "10": @@ -47,6 +47,6 @@ def exp_time() -> float: "uuid4": uuid4, } comment_prefix = "p2p" -desc_template: Type[CommentTemplate] = CustomCommentTemplate +template: Type[Template] = CustomCommentTemplate ignore_pkg_list: List[str] = ["validate", "p2p_validate"] pkg_config: Dict[str, SubConfigModel] = {"single_config": SubConfigModel(module=single_config_pkg_plugin_config)} diff --git a/example/proto_3_20_pydanticv1/demo_gen_code.py b/example/proto_3_20_pydanticv1/demo_gen_code.py index 51d0e50..3993aed 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code.py @@ -17,6 +17,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -130,3 +138,8 @@ class Config: metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py index 52d9c0e..1255a3e 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py @@ -20,6 +20,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0, title="use age", ge=0.0, example=18) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -132,3 +140,8 @@ class Config: metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py index 52d9c0e..1255a3e 100644 --- a/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_3_20_pydanticv1/demo_gen_code_by_text_comment_pyi.py @@ -20,6 +20,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0, title="use age", ge=0.0, example=18) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -132,3 +140,8 @@ class Config: metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py b/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py index 9bd2c35..ddee6e4 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_p2p.py @@ -142,3 +142,21 @@ class InvoiceItem2(BaseModel): quantity: int = Field(default=0) items: typing.List["InvoiceItem2"] = Field(default_factory=list) invoice: Invoice3 = Field() + + +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + +class RootMessage(BaseModel): + """ + Test Message references + from: https://github.com/so1n/protobuf_to_pydantic/issues/64 + """ + + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_pb2.py b/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_pb2.py index 348bcde..9a0b2e3 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_pb2.py +++ b/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_pb2.py @@ -26,7 +26,7 @@ syntax='proto3', serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_pb=b'\n%example/example_proto/demo/demo.proto\x12\x04user\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a google/protobuf/field_mask.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a)example/example_proto/common/single.proto\"\xc3\x01\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x1a\n\x03sex\x18\x04 \x01(\x0e\x32\r.user.SexType\x12\x1e\n\x04\x64\x65mo\x18\x06 \x01(\x0e\x32\x10.single.DemoEnum\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\t\x12)\n\x0c\x64\x65mo_message\x18\t \x01(\x0b\x32\x13.single.DemoMessage\"\xb1\x01\n\x0cOtherMessage\x12)\n\x08metadata\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x32\n\x0c\x64ouble_value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x33\n\nfield_mask\x18\x64 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskH\x00\x88\x01\x01\x42\r\n\x0b_field_mask\"\xe4\x01\n\nMapMessage\x12/\n\x08user_map\x18\x01 \x03(\x0b\x32\x1d.user.MapMessage.UserMapEntry\x12\x31\n\tuser_flag\x18\x02 \x03(\x0b\x32\x1e.user.MapMessage.UserFlagEntry\x1a\x41\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.user.UserMessage:\x02\x38\x01\x1a/\n\rUserFlagEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"[\n\x0fRepeatedMessage\x12\x10\n\x08str_list\x18\x01 \x03(\t\x12\x10\n\x08int_list\x18\x02 \x03(\x05\x12$\n\tuser_list\x18\x03 \x03(\x0b\x32\x11.user.UserMessage\"\x99\x05\n\rNestedMessage\x12;\n\ruser_list_map\x18\x01 \x03(\x0b\x32$.user.NestedMessage.UserListMapEntry\x12\x32\n\x08user_map\x18\x02 \x03(\x0b\x32 .user.NestedMessage.UserMapEntry\x12\x34\n\x08user_pay\x18\x03 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12\x35\n\x0cinclude_enum\x18\x04 \x01(\x0e\x32\x1f.user.NestedMessage.IncludeEnum\x12?\n\x13not_enable_user_pay\x18\x05 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12%\n\x05\x65mpty\x18\x06 \x01(\x0b\x32\x16.google.protobuf.Empty\x12,\n\x0b\x61\x66ter_refer\x18\x07 \x01(\x0b\x32\x17.user.AfterReferMessage\x1a\\\n\x0eUserPayMessage\x12\x13\n\x0b\x62\x61nk_number\x18\x01 \x01(\t\x12\'\n\x03\x65xp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04uuid\x18\x03 \x01(\t\x1aI\n\x10UserListMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.user.RepeatedMessage:\x02\x38\x01\x1a@\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1f\n\x05value\x18\x02 \x01(\x0b\x32\x10.user.MapMessage:\x02\x38\x01\")\n\x0bIncludeEnum\x12\x08\n\x04zero\x10\x00\x12\x07\n\x03one\x10\x01\x12\x07\n\x03two\x10\x02\"-\n\x11\x41\x66terReferMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\"_\n\x0bInvoiceItem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12 \n\x05items\x18\x04 \x03(\x0b\x32\x11.user.InvoiceItem\"\x0e\n\x0c\x45mptyMessage\"\xa9\x02\n\x0fOptionalMessage\x12\x0b\n\x01x\x18\x01 \x01(\tH\x00\x12\x0b\n\x01y\x18\x02 \x01(\x05H\x00\x12\x11\n\x04name\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x10\n\x03\x61ge\x18\x04 \x01(\x05H\x02\x88\x01\x01\x12$\n\x04item\x18\x05 \x01(\x0b\x32\x11.user.InvoiceItemH\x03\x88\x01\x01\x12\x10\n\x08str_list\x18\x06 \x03(\t\x12\x32\n\x07int_map\x18\x07 \x03(\x0b\x32!.user.OptionalMessage.IntMapEntry\x12\x1d\n\x15\x64\x65\x66\x61ult_template_test\x18\x08 \x01(\x02\x1a-\n\x0bIntMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x42\x03\n\x01\x61\x42\x07\n\x05_nameB\x06\n\x04_ageB\x07\n\x05_item\"\x82\x01\n\x0cInvoiceItem2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\x12\x1f\n\x07invoice\x18\x05 \x01(\x0b\x32\x0e.user.Invoice3\"]\n\x08Invoice3\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2*\x1d\n\x07SexType\x12\x07\n\x03man\x10\x00\x12\t\n\x05women\x10\x01\x62\x06proto3' + serialized_pb=b'\n%example/example_proto/demo/demo.proto\x12\x04user\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a google/protobuf/field_mask.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a)example/example_proto/common/single.proto\"\xc3\x01\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x1a\n\x03sex\x18\x04 \x01(\x0e\x32\r.user.SexType\x12\x1e\n\x04\x64\x65mo\x18\x06 \x01(\x0e\x32\x10.single.DemoEnum\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\t\x12)\n\x0c\x64\x65mo_message\x18\t \x01(\x0b\x32\x13.single.DemoMessage\"\xb1\x01\n\x0cOtherMessage\x12)\n\x08metadata\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x32\n\x0c\x64ouble_value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x33\n\nfield_mask\x18\x64 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskH\x00\x88\x01\x01\x42\r\n\x0b_field_mask\"\xe4\x01\n\nMapMessage\x12/\n\x08user_map\x18\x01 \x03(\x0b\x32\x1d.user.MapMessage.UserMapEntry\x12\x31\n\tuser_flag\x18\x02 \x03(\x0b\x32\x1e.user.MapMessage.UserFlagEntry\x1a\x41\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.user.UserMessage:\x02\x38\x01\x1a/\n\rUserFlagEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"[\n\x0fRepeatedMessage\x12\x10\n\x08str_list\x18\x01 \x03(\t\x12\x10\n\x08int_list\x18\x02 \x03(\x05\x12$\n\tuser_list\x18\x03 \x03(\x0b\x32\x11.user.UserMessage\"\x99\x05\n\rNestedMessage\x12;\n\ruser_list_map\x18\x01 \x03(\x0b\x32$.user.NestedMessage.UserListMapEntry\x12\x32\n\x08user_map\x18\x02 \x03(\x0b\x32 .user.NestedMessage.UserMapEntry\x12\x34\n\x08user_pay\x18\x03 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12\x35\n\x0cinclude_enum\x18\x04 \x01(\x0e\x32\x1f.user.NestedMessage.IncludeEnum\x12?\n\x13not_enable_user_pay\x18\x05 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12%\n\x05\x65mpty\x18\x06 \x01(\x0b\x32\x16.google.protobuf.Empty\x12,\n\x0b\x61\x66ter_refer\x18\x07 \x01(\x0b\x32\x17.user.AfterReferMessage\x1a\\\n\x0eUserPayMessage\x12\x13\n\x0b\x62\x61nk_number\x18\x01 \x01(\t\x12\'\n\x03\x65xp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04uuid\x18\x03 \x01(\t\x1aI\n\x10UserListMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.user.RepeatedMessage:\x02\x38\x01\x1a@\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1f\n\x05value\x18\x02 \x01(\x0b\x32\x10.user.MapMessage:\x02\x38\x01\")\n\x0bIncludeEnum\x12\x08\n\x04zero\x10\x00\x12\x07\n\x03one\x10\x01\x12\x07\n\x03two\x10\x02\"-\n\x11\x41\x66terReferMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\"_\n\x0bInvoiceItem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12 \n\x05items\x18\x04 \x03(\x0b\x32\x11.user.InvoiceItem\"\x0e\n\x0c\x45mptyMessage\"\xa9\x02\n\x0fOptionalMessage\x12\x0b\n\x01x\x18\x01 \x01(\tH\x00\x12\x0b\n\x01y\x18\x02 \x01(\x05H\x00\x12\x11\n\x04name\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x10\n\x03\x61ge\x18\x04 \x01(\x05H\x02\x88\x01\x01\x12$\n\x04item\x18\x05 \x01(\x0b\x32\x11.user.InvoiceItemH\x03\x88\x01\x01\x12\x10\n\x08str_list\x18\x06 \x03(\t\x12\x32\n\x07int_map\x18\x07 \x03(\x0b\x32!.user.OptionalMessage.IntMapEntry\x12\x1d\n\x15\x64\x65\x66\x61ult_template_test\x18\x08 \x01(\x02\x1a-\n\x0bIntMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x42\x03\n\x01\x61\x42\x07\n\x05_nameB\x06\n\x04_ageB\x07\n\x05_item\"\x82\x01\n\x0cInvoiceItem2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\x12\x1f\n\x07invoice\x18\x05 \x01(\x0b\x32\x0e.user.Invoice3\"]\n\x08Invoice3\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\"C\n\x0bRootMessage\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12$\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x14.user.AnOtherMessage\"m\n\x0e\x41nOtherMessage\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12/\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x1f.user.AnOtherMessage.SubMessage\x1a\x1a\n\nSubMessage\x12\x0c\n\x04text\x18\x01 \x01(\t*\x1d\n\x07SexType\x12\x07\n\x03man\x10\x00\x12\t\n\x05women\x10\x01\x62\x06proto3' , dependencies=[google_dot_protobuf_dot_empty__pb2.DESCRIPTOR,google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,google_dot_protobuf_dot_field__mask__pb2.DESCRIPTOR,google_dot_protobuf_dot_wrappers__pb2.DESCRIPTOR,example_dot_example__proto_dot_common_dot_single__pb2.DESCRIPTOR,]) @@ -50,8 +50,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=2306, - serialized_end=2335, + serialized_start=2486, + serialized_end=2515, ) _sym_db.RegisterEnumDescriptor(_SEXTYPE) @@ -948,6 +948,115 @@ serialized_end=2304, ) + +_ROOTMESSAGE = _descriptor.Descriptor( + name='RootMessage', + full_name='user.RootMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='field1', full_name='user.RootMessage.field1', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='field2', full_name='user.RootMessage.field2', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2306, + serialized_end=2373, +) + + +_ANOTHERMESSAGE_SUBMESSAGE = _descriptor.Descriptor( + name='SubMessage', + full_name='user.AnOtherMessage.SubMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='text', full_name='user.AnOtherMessage.SubMessage.text', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2458, + serialized_end=2484, +) + +_ANOTHERMESSAGE = _descriptor.Descriptor( + name='AnOtherMessage', + full_name='user.AnOtherMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='field1', full_name='user.AnOtherMessage.field1', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='field2', full_name='user.AnOtherMessage.field2', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_ANOTHERMESSAGE_SUBMESSAGE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2375, + serialized_end=2484, +) + _USERMESSAGE.fields_by_name['sex'].enum_type = _SEXTYPE _USERMESSAGE.fields_by_name['demo'].enum_type = example_dot_example__proto_dot_common_dot_single__pb2._DEMOENUM _USERMESSAGE.fields_by_name['demo_message'].message_type = example_dot_example__proto_dot_common_dot_single__pb2._DEMOMESSAGE @@ -999,6 +1108,9 @@ _INVOICEITEM2.fields_by_name['items'].message_type = _INVOICEITEM2 _INVOICEITEM2.fields_by_name['invoice'].message_type = _INVOICE3 _INVOICE3.fields_by_name['items'].message_type = _INVOICEITEM2 +_ROOTMESSAGE.fields_by_name['field2'].message_type = _ANOTHERMESSAGE +_ANOTHERMESSAGE_SUBMESSAGE.containing_type = _ANOTHERMESSAGE +_ANOTHERMESSAGE.fields_by_name['field2'].message_type = _ANOTHERMESSAGE_SUBMESSAGE DESCRIPTOR.message_types_by_name['UserMessage'] = _USERMESSAGE DESCRIPTOR.message_types_by_name['OtherMessage'] = _OTHERMESSAGE DESCRIPTOR.message_types_by_name['MapMessage'] = _MAPMESSAGE @@ -1010,6 +1122,8 @@ DESCRIPTOR.message_types_by_name['OptionalMessage'] = _OPTIONALMESSAGE DESCRIPTOR.message_types_by_name['InvoiceItem2'] = _INVOICEITEM2 DESCRIPTOR.message_types_by_name['Invoice3'] = _INVOICE3 +DESCRIPTOR.message_types_by_name['RootMessage'] = _ROOTMESSAGE +DESCRIPTOR.message_types_by_name['AnOtherMessage'] = _ANOTHERMESSAGE DESCRIPTOR.enum_types_by_name['SexType'] = _SEXTYPE _sym_db.RegisterFileDescriptor(DESCRIPTOR) @@ -1138,6 +1252,28 @@ }) _sym_db.RegisterMessage(Invoice3) +RootMessage = _reflection.GeneratedProtocolMessageType('RootMessage', (_message.Message,), { + 'DESCRIPTOR' : _ROOTMESSAGE, + '__module__' : 'example.example_proto.demo.demo_pb2' + # @@protoc_insertion_point(class_scope:user.RootMessage) + }) +_sym_db.RegisterMessage(RootMessage) + +AnOtherMessage = _reflection.GeneratedProtocolMessageType('AnOtherMessage', (_message.Message,), { + + 'SubMessage' : _reflection.GeneratedProtocolMessageType('SubMessage', (_message.Message,), { + 'DESCRIPTOR' : _ANOTHERMESSAGE_SUBMESSAGE, + '__module__' : 'example.example_proto.demo.demo_pb2' + # @@protoc_insertion_point(class_scope:user.AnOtherMessage.SubMessage) + }) + , + 'DESCRIPTOR' : _ANOTHERMESSAGE, + '__module__' : 'example.example_proto.demo.demo_pb2' + # @@protoc_insertion_point(class_scope:user.AnOtherMessage) + }) +_sym_db.RegisterMessage(AnOtherMessage) +_sym_db.RegisterMessage(AnOtherMessage.SubMessage) + _MAPMESSAGE_USERMAPENTRY._options = None _MAPMESSAGE_USERFLAGENTRY._options = None diff --git a/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_pb2.pyi b/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_pb2.pyi index 1cb718c..6efd2fa 100644 --- a/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_pb2.pyi +++ b/example/proto_3_20_pydanticv1/example/example_proto/demo/demo_pb2.pyi @@ -438,3 +438,48 @@ class Invoice3(google.protobuf.message.Message): ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["amount",b"amount","items",b"items","name",b"name","quantity",b"quantity"]) -> None: ... global___Invoice3 = Invoice3 + +class RootMessage(google.protobuf.message.Message): + """Test Message references + from: https://github.com/so1n/protobuf_to_pydantic/issues/64 + """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor + FIELD1_FIELD_NUMBER: builtins.int + FIELD2_FIELD_NUMBER: builtins.int + field1: typing.Text + @property + def field2(self) -> global___AnOtherMessage: ... + def __init__(self, + *, + field1: typing.Text = ..., + field2: typing.Optional[global___AnOtherMessage] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["field2",b"field2"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["field1",b"field1","field2",b"field2"]) -> None: ... +global___RootMessage = RootMessage + +class AnOtherMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + class SubMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + TEXT_FIELD_NUMBER: builtins.int + text: typing.Text + def __init__(self, + *, + text: typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["text",b"text"]) -> None: ... + + FIELD1_FIELD_NUMBER: builtins.int + FIELD2_FIELD_NUMBER: builtins.int + field1: typing.Text + @property + def field2(self) -> global___AnOtherMessage.SubMessage: ... + def __init__(self, + *, + field1: typing.Text = ..., + field2: typing.Optional[global___AnOtherMessage.SubMessage] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["field2",b"field2"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["field1",b"field1","field2",b"field2"]) -> None: ... +global___AnOtherMessage = AnOtherMessage diff --git a/example/proto_3_20_pydanticv2/demo_gen_code.py b/example/proto_3_20_pydanticv2/demo_gen_code.py index fc8e363..c3f9f97 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code.py @@ -17,6 +17,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -129,3 +137,8 @@ class OtherMessage(BaseModel): metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py index 09bd136..ba105b6 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_protobuf_field.py @@ -20,6 +20,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0, title="use age", example=18, ge=0) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -131,3 +139,8 @@ class OtherMessage(BaseModel): metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py index 09bd136..ba105b6 100644 --- a/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_3_20_pydanticv2/demo_gen_code_by_text_comment_pyi.py @@ -20,6 +20,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0, title="use age", example=18, ge=0) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -131,3 +139,8 @@ class OtherMessage(BaseModel): metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py b/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py index 51500dc..f70afcd 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_p2p.py @@ -140,3 +140,21 @@ class InvoiceItem2(BaseModel): quantity: int = Field(default=0) items: typing.List["InvoiceItem2"] = Field(default_factory=list) invoice: Invoice3 = Field() + + +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + +class RootMessage(BaseModel): + """ + Test Message references + from: https://github.com/so1n/protobuf_to_pydantic/issues/64 + """ + + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_pb2.py b/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_pb2.py index 5cf0ee9..53c7240 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_pb2.py +++ b/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_pb2.py @@ -26,7 +26,7 @@ syntax='proto3', serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_pb=b'\n%example/example_proto/demo/demo.proto\x12\x04user\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a google/protobuf/field_mask.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a)example/example_proto/common/single.proto\"\xc3\x01\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x1a\n\x03sex\x18\x04 \x01(\x0e\x32\r.user.SexType\x12\x1e\n\x04\x64\x65mo\x18\x06 \x01(\x0e\x32\x10.single.DemoEnum\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\t\x12)\n\x0c\x64\x65mo_message\x18\t \x01(\x0b\x32\x13.single.DemoMessage\"\xb1\x01\n\x0cOtherMessage\x12)\n\x08metadata\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x32\n\x0c\x64ouble_value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x33\n\nfield_mask\x18\x64 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskH\x00\x88\x01\x01\x42\r\n\x0b_field_mask\"\xe4\x01\n\nMapMessage\x12/\n\x08user_map\x18\x01 \x03(\x0b\x32\x1d.user.MapMessage.UserMapEntry\x12\x31\n\tuser_flag\x18\x02 \x03(\x0b\x32\x1e.user.MapMessage.UserFlagEntry\x1a\x41\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.user.UserMessage:\x02\x38\x01\x1a/\n\rUserFlagEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"[\n\x0fRepeatedMessage\x12\x10\n\x08str_list\x18\x01 \x03(\t\x12\x10\n\x08int_list\x18\x02 \x03(\x05\x12$\n\tuser_list\x18\x03 \x03(\x0b\x32\x11.user.UserMessage\"\x99\x05\n\rNestedMessage\x12;\n\ruser_list_map\x18\x01 \x03(\x0b\x32$.user.NestedMessage.UserListMapEntry\x12\x32\n\x08user_map\x18\x02 \x03(\x0b\x32 .user.NestedMessage.UserMapEntry\x12\x34\n\x08user_pay\x18\x03 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12\x35\n\x0cinclude_enum\x18\x04 \x01(\x0e\x32\x1f.user.NestedMessage.IncludeEnum\x12?\n\x13not_enable_user_pay\x18\x05 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12%\n\x05\x65mpty\x18\x06 \x01(\x0b\x32\x16.google.protobuf.Empty\x12,\n\x0b\x61\x66ter_refer\x18\x07 \x01(\x0b\x32\x17.user.AfterReferMessage\x1a\\\n\x0eUserPayMessage\x12\x13\n\x0b\x62\x61nk_number\x18\x01 \x01(\t\x12\'\n\x03\x65xp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04uuid\x18\x03 \x01(\t\x1aI\n\x10UserListMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.user.RepeatedMessage:\x02\x38\x01\x1a@\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1f\n\x05value\x18\x02 \x01(\x0b\x32\x10.user.MapMessage:\x02\x38\x01\")\n\x0bIncludeEnum\x12\x08\n\x04zero\x10\x00\x12\x07\n\x03one\x10\x01\x12\x07\n\x03two\x10\x02\"-\n\x11\x41\x66terReferMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\"_\n\x0bInvoiceItem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12 \n\x05items\x18\x04 \x03(\x0b\x32\x11.user.InvoiceItem\"\x0e\n\x0c\x45mptyMessage\"\xa9\x02\n\x0fOptionalMessage\x12\x0b\n\x01x\x18\x01 \x01(\tH\x00\x12\x0b\n\x01y\x18\x02 \x01(\x05H\x00\x12\x11\n\x04name\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x10\n\x03\x61ge\x18\x04 \x01(\x05H\x02\x88\x01\x01\x12$\n\x04item\x18\x05 \x01(\x0b\x32\x11.user.InvoiceItemH\x03\x88\x01\x01\x12\x10\n\x08str_list\x18\x06 \x03(\t\x12\x32\n\x07int_map\x18\x07 \x03(\x0b\x32!.user.OptionalMessage.IntMapEntry\x12\x1d\n\x15\x64\x65\x66\x61ult_template_test\x18\x08 \x01(\x02\x1a-\n\x0bIntMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x42\x03\n\x01\x61\x42\x07\n\x05_nameB\x06\n\x04_ageB\x07\n\x05_item\"\x82\x01\n\x0cInvoiceItem2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\x12\x1f\n\x07invoice\x18\x05 \x01(\x0b\x32\x0e.user.Invoice3\"]\n\x08Invoice3\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2*\x1d\n\x07SexType\x12\x07\n\x03man\x10\x00\x12\t\n\x05women\x10\x01\x62\x06proto3' + serialized_pb=b'\n%example/example_proto/demo/demo.proto\x12\x04user\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a google/protobuf/field_mask.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a)example/example_proto/common/single.proto\"\xc3\x01\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x1a\n\x03sex\x18\x04 \x01(\x0e\x32\r.user.SexType\x12\x1e\n\x04\x64\x65mo\x18\x06 \x01(\x0e\x32\x10.single.DemoEnum\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\t\x12)\n\x0c\x64\x65mo_message\x18\t \x01(\x0b\x32\x13.single.DemoMessage\"\xb1\x01\n\x0cOtherMessage\x12)\n\x08metadata\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x32\n\x0c\x64ouble_value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x33\n\nfield_mask\x18\x64 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskH\x00\x88\x01\x01\x42\r\n\x0b_field_mask\"\xe4\x01\n\nMapMessage\x12/\n\x08user_map\x18\x01 \x03(\x0b\x32\x1d.user.MapMessage.UserMapEntry\x12\x31\n\tuser_flag\x18\x02 \x03(\x0b\x32\x1e.user.MapMessage.UserFlagEntry\x1a\x41\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.user.UserMessage:\x02\x38\x01\x1a/\n\rUserFlagEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"[\n\x0fRepeatedMessage\x12\x10\n\x08str_list\x18\x01 \x03(\t\x12\x10\n\x08int_list\x18\x02 \x03(\x05\x12$\n\tuser_list\x18\x03 \x03(\x0b\x32\x11.user.UserMessage\"\x99\x05\n\rNestedMessage\x12;\n\ruser_list_map\x18\x01 \x03(\x0b\x32$.user.NestedMessage.UserListMapEntry\x12\x32\n\x08user_map\x18\x02 \x03(\x0b\x32 .user.NestedMessage.UserMapEntry\x12\x34\n\x08user_pay\x18\x03 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12\x35\n\x0cinclude_enum\x18\x04 \x01(\x0e\x32\x1f.user.NestedMessage.IncludeEnum\x12?\n\x13not_enable_user_pay\x18\x05 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12%\n\x05\x65mpty\x18\x06 \x01(\x0b\x32\x16.google.protobuf.Empty\x12,\n\x0b\x61\x66ter_refer\x18\x07 \x01(\x0b\x32\x17.user.AfterReferMessage\x1a\\\n\x0eUserPayMessage\x12\x13\n\x0b\x62\x61nk_number\x18\x01 \x01(\t\x12\'\n\x03\x65xp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04uuid\x18\x03 \x01(\t\x1aI\n\x10UserListMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.user.RepeatedMessage:\x02\x38\x01\x1a@\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1f\n\x05value\x18\x02 \x01(\x0b\x32\x10.user.MapMessage:\x02\x38\x01\")\n\x0bIncludeEnum\x12\x08\n\x04zero\x10\x00\x12\x07\n\x03one\x10\x01\x12\x07\n\x03two\x10\x02\"-\n\x11\x41\x66terReferMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\"_\n\x0bInvoiceItem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12 \n\x05items\x18\x04 \x03(\x0b\x32\x11.user.InvoiceItem\"\x0e\n\x0c\x45mptyMessage\"\xa9\x02\n\x0fOptionalMessage\x12\x0b\n\x01x\x18\x01 \x01(\tH\x00\x12\x0b\n\x01y\x18\x02 \x01(\x05H\x00\x12\x11\n\x04name\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x10\n\x03\x61ge\x18\x04 \x01(\x05H\x02\x88\x01\x01\x12$\n\x04item\x18\x05 \x01(\x0b\x32\x11.user.InvoiceItemH\x03\x88\x01\x01\x12\x10\n\x08str_list\x18\x06 \x03(\t\x12\x32\n\x07int_map\x18\x07 \x03(\x0b\x32!.user.OptionalMessage.IntMapEntry\x12\x1d\n\x15\x64\x65\x66\x61ult_template_test\x18\x08 \x01(\x02\x1a-\n\x0bIntMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x42\x03\n\x01\x61\x42\x07\n\x05_nameB\x06\n\x04_ageB\x07\n\x05_item\"\x82\x01\n\x0cInvoiceItem2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\x12\x1f\n\x07invoice\x18\x05 \x01(\x0b\x32\x0e.user.Invoice3\"]\n\x08Invoice3\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\"C\n\x0bRootMessage\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12$\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x14.user.AnOtherMessage\"m\n\x0e\x41nOtherMessage\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12/\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x1f.user.AnOtherMessage.SubMessage\x1a\x1a\n\nSubMessage\x12\x0c\n\x04text\x18\x01 \x01(\t*\x1d\n\x07SexType\x12\x07\n\x03man\x10\x00\x12\t\n\x05women\x10\x01\x62\x06proto3' , dependencies=[google_dot_protobuf_dot_empty__pb2.DESCRIPTOR,google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,google_dot_protobuf_dot_field__mask__pb2.DESCRIPTOR,google_dot_protobuf_dot_wrappers__pb2.DESCRIPTOR,example_dot_example__proto_dot_common_dot_single__pb2.DESCRIPTOR,]) @@ -50,8 +50,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=2306, - serialized_end=2335, + serialized_start=2486, + serialized_end=2515, ) _sym_db.RegisterEnumDescriptor(_SEXTYPE) @@ -948,6 +948,115 @@ serialized_end=2304, ) + +_ROOTMESSAGE = _descriptor.Descriptor( + name='RootMessage', + full_name='user.RootMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='field1', full_name='user.RootMessage.field1', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='field2', full_name='user.RootMessage.field2', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2306, + serialized_end=2373, +) + + +_ANOTHERMESSAGE_SUBMESSAGE = _descriptor.Descriptor( + name='SubMessage', + full_name='user.AnOtherMessage.SubMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='text', full_name='user.AnOtherMessage.SubMessage.text', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2458, + serialized_end=2484, +) + +_ANOTHERMESSAGE = _descriptor.Descriptor( + name='AnOtherMessage', + full_name='user.AnOtherMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='field1', full_name='user.AnOtherMessage.field1', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='field2', full_name='user.AnOtherMessage.field2', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_ANOTHERMESSAGE_SUBMESSAGE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=2375, + serialized_end=2484, +) + _USERMESSAGE.fields_by_name['sex'].enum_type = _SEXTYPE _USERMESSAGE.fields_by_name['demo'].enum_type = example_dot_example__proto_dot_common_dot_single__pb2._DEMOENUM _USERMESSAGE.fields_by_name['demo_message'].message_type = example_dot_example__proto_dot_common_dot_single__pb2._DEMOMESSAGE @@ -999,6 +1108,9 @@ _INVOICEITEM2.fields_by_name['items'].message_type = _INVOICEITEM2 _INVOICEITEM2.fields_by_name['invoice'].message_type = _INVOICE3 _INVOICE3.fields_by_name['items'].message_type = _INVOICEITEM2 +_ROOTMESSAGE.fields_by_name['field2'].message_type = _ANOTHERMESSAGE +_ANOTHERMESSAGE_SUBMESSAGE.containing_type = _ANOTHERMESSAGE +_ANOTHERMESSAGE.fields_by_name['field2'].message_type = _ANOTHERMESSAGE_SUBMESSAGE DESCRIPTOR.message_types_by_name['UserMessage'] = _USERMESSAGE DESCRIPTOR.message_types_by_name['OtherMessage'] = _OTHERMESSAGE DESCRIPTOR.message_types_by_name['MapMessage'] = _MAPMESSAGE @@ -1010,6 +1122,8 @@ DESCRIPTOR.message_types_by_name['OptionalMessage'] = _OPTIONALMESSAGE DESCRIPTOR.message_types_by_name['InvoiceItem2'] = _INVOICEITEM2 DESCRIPTOR.message_types_by_name['Invoice3'] = _INVOICE3 +DESCRIPTOR.message_types_by_name['RootMessage'] = _ROOTMESSAGE +DESCRIPTOR.message_types_by_name['AnOtherMessage'] = _ANOTHERMESSAGE DESCRIPTOR.enum_types_by_name['SexType'] = _SEXTYPE _sym_db.RegisterFileDescriptor(DESCRIPTOR) @@ -1138,6 +1252,28 @@ }) _sym_db.RegisterMessage(Invoice3) +RootMessage = _reflection.GeneratedProtocolMessageType('RootMessage', (_message.Message,), { + 'DESCRIPTOR' : _ROOTMESSAGE, + '__module__' : 'example.example_proto.demo.demo_pb2' + # @@protoc_insertion_point(class_scope:user.RootMessage) + }) +_sym_db.RegisterMessage(RootMessage) + +AnOtherMessage = _reflection.GeneratedProtocolMessageType('AnOtherMessage', (_message.Message,), { + + 'SubMessage' : _reflection.GeneratedProtocolMessageType('SubMessage', (_message.Message,), { + 'DESCRIPTOR' : _ANOTHERMESSAGE_SUBMESSAGE, + '__module__' : 'example.example_proto.demo.demo_pb2' + # @@protoc_insertion_point(class_scope:user.AnOtherMessage.SubMessage) + }) + , + 'DESCRIPTOR' : _ANOTHERMESSAGE, + '__module__' : 'example.example_proto.demo.demo_pb2' + # @@protoc_insertion_point(class_scope:user.AnOtherMessage) + }) +_sym_db.RegisterMessage(AnOtherMessage) +_sym_db.RegisterMessage(AnOtherMessage.SubMessage) + _MAPMESSAGE_USERMAPENTRY._options = None _MAPMESSAGE_USERFLAGENTRY._options = None diff --git a/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_pb2.pyi b/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_pb2.pyi index 1cb718c..6efd2fa 100644 --- a/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_pb2.pyi +++ b/example/proto_3_20_pydanticv2/example/example_proto/demo/demo_pb2.pyi @@ -438,3 +438,48 @@ class Invoice3(google.protobuf.message.Message): ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["amount",b"amount","items",b"items","name",b"name","quantity",b"quantity"]) -> None: ... global___Invoice3 = Invoice3 + +class RootMessage(google.protobuf.message.Message): + """Test Message references + from: https://github.com/so1n/protobuf_to_pydantic/issues/64 + """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor + FIELD1_FIELD_NUMBER: builtins.int + FIELD2_FIELD_NUMBER: builtins.int + field1: typing.Text + @property + def field2(self) -> global___AnOtherMessage: ... + def __init__(self, + *, + field1: typing.Text = ..., + field2: typing.Optional[global___AnOtherMessage] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["field2",b"field2"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["field1",b"field1","field2",b"field2"]) -> None: ... +global___RootMessage = RootMessage + +class AnOtherMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + class SubMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + TEXT_FIELD_NUMBER: builtins.int + text: typing.Text + def __init__(self, + *, + text: typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["text",b"text"]) -> None: ... + + FIELD1_FIELD_NUMBER: builtins.int + FIELD2_FIELD_NUMBER: builtins.int + field1: typing.Text + @property + def field2(self) -> global___AnOtherMessage.SubMessage: ... + def __init__(self, + *, + field1: typing.Text = ..., + field2: typing.Optional[global___AnOtherMessage.SubMessage] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["field2",b"field2"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["field1",b"field1","field2",b"field2"]) -> None: ... +global___AnOtherMessage = AnOtherMessage diff --git a/example/proto_pydanticv1/demo_gen_code.py b/example/proto_pydanticv1/demo_gen_code.py index e602cfd..8ee014e 100644 --- a/example/proto_pydanticv1/demo_gen_code.py +++ b/example/proto_pydanticv1/demo_gen_code.py @@ -17,6 +17,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -130,3 +138,8 @@ class Config: metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py b/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py index 0e743a0..aa895f2 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py +++ b/example/proto_pydanticv1/demo_gen_code_by_text_comment_protobuf_field.py @@ -20,6 +20,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0, title="use age", ge=0.0, example=18) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -132,3 +140,8 @@ class Config: metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py b/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py index 0e743a0..aa895f2 100644 --- a/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py +++ b/example/proto_pydanticv1/demo_gen_code_by_text_comment_pyi.py @@ -20,6 +20,14 @@ class AfterReferMessage(BaseModel): age: int = Field(default=0, title="use age", ge=0.0, example=18) +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + class EmptyMessage(BaseModel): pass @@ -132,3 +140,8 @@ class Config: metadata: typing.Dict[str, typing.Any] = Field(default_factory=dict) double_value: DoubleValue = Field(default_factory=DoubleValue) field_mask: typing.Optional[FieldMask] = Field(default_factory=FieldMask) + + +class RootMessage(BaseModel): + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py b/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py index 8e6cf79..6fea5e4 100644 --- a/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py +++ b/example/proto_pydanticv1/example/example_proto/demo/demo_p2p.py @@ -142,3 +142,21 @@ class InvoiceItem2(BaseModel): quantity: int = Field(default=0) items: typing.List["InvoiceItem2"] = Field(default_factory=list) invoice: Invoice3 = Field() + + +class AnOtherMessage(BaseModel): + class SubMessage(BaseModel): + text: str = Field(default="") + + field1: str = Field(default="") + field2: SubMessage = Field() + + +class RootMessage(BaseModel): + """ + Test Message references + from: https://github.com/so1n/protobuf_to_pydantic/issues/64 + """ + + field1: str = Field(default="") + field2: AnOtherMessage = Field() diff --git a/example/proto_pydanticv1/example/example_proto/demo/demo_pb2.py b/example/proto_pydanticv1/example/example_proto/demo/demo_pb2.py index e4df9a0..76fda0e 100644 --- a/example/proto_pydanticv1/example/example_proto/demo/demo_pb2.py +++ b/example/proto_pydanticv1/example/example_proto/demo/demo_pb2.py @@ -19,7 +19,7 @@ from example.proto_pydanticv1.example.example_proto.common import single_pb2 as example_dot_example__proto_dot_common_dot_single__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%example/example_proto/demo/demo.proto\x12\x04user\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a google/protobuf/field_mask.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a)example/example_proto/common/single.proto\"\xc3\x01\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x1a\n\x03sex\x18\x04 \x01(\x0e\x32\r.user.SexType\x12\x1e\n\x04\x64\x65mo\x18\x06 \x01(\x0e\x32\x10.single.DemoEnum\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\t\x12)\n\x0c\x64\x65mo_message\x18\t \x01(\x0b\x32\x13.single.DemoMessage\"\xb1\x01\n\x0cOtherMessage\x12)\n\x08metadata\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x32\n\x0c\x64ouble_value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x33\n\nfield_mask\x18\x64 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskH\x00\x88\x01\x01\x42\r\n\x0b_field_mask\"\xe4\x01\n\nMapMessage\x12/\n\x08user_map\x18\x01 \x03(\x0b\x32\x1d.user.MapMessage.UserMapEntry\x12\x31\n\tuser_flag\x18\x02 \x03(\x0b\x32\x1e.user.MapMessage.UserFlagEntry\x1a\x41\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.user.UserMessage:\x02\x38\x01\x1a/\n\rUserFlagEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"[\n\x0fRepeatedMessage\x12\x10\n\x08str_list\x18\x01 \x03(\t\x12\x10\n\x08int_list\x18\x02 \x03(\x05\x12$\n\tuser_list\x18\x03 \x03(\x0b\x32\x11.user.UserMessage\"\x99\x05\n\rNestedMessage\x12;\n\ruser_list_map\x18\x01 \x03(\x0b\x32$.user.NestedMessage.UserListMapEntry\x12\x32\n\x08user_map\x18\x02 \x03(\x0b\x32 .user.NestedMessage.UserMapEntry\x12\x34\n\x08user_pay\x18\x03 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12\x35\n\x0cinclude_enum\x18\x04 \x01(\x0e\x32\x1f.user.NestedMessage.IncludeEnum\x12?\n\x13not_enable_user_pay\x18\x05 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12%\n\x05\x65mpty\x18\x06 \x01(\x0b\x32\x16.google.protobuf.Empty\x12,\n\x0b\x61\x66ter_refer\x18\x07 \x01(\x0b\x32\x17.user.AfterReferMessage\x1a\\\n\x0eUserPayMessage\x12\x13\n\x0b\x62\x61nk_number\x18\x01 \x01(\t\x12\'\n\x03\x65xp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04uuid\x18\x03 \x01(\t\x1aI\n\x10UserListMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.user.RepeatedMessage:\x02\x38\x01\x1a@\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1f\n\x05value\x18\x02 \x01(\x0b\x32\x10.user.MapMessage:\x02\x38\x01\")\n\x0bIncludeEnum\x12\x08\n\x04zero\x10\x00\x12\x07\n\x03one\x10\x01\x12\x07\n\x03two\x10\x02\"-\n\x11\x41\x66terReferMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\"_\n\x0bInvoiceItem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12 \n\x05items\x18\x04 \x03(\x0b\x32\x11.user.InvoiceItem\"\x0e\n\x0c\x45mptyMessage\"\xa9\x02\n\x0fOptionalMessage\x12\x0b\n\x01x\x18\x01 \x01(\tH\x00\x12\x0b\n\x01y\x18\x02 \x01(\x05H\x00\x12\x11\n\x04name\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x10\n\x03\x61ge\x18\x04 \x01(\x05H\x02\x88\x01\x01\x12$\n\x04item\x18\x05 \x01(\x0b\x32\x11.user.InvoiceItemH\x03\x88\x01\x01\x12\x10\n\x08str_list\x18\x06 \x03(\t\x12\x32\n\x07int_map\x18\x07 \x03(\x0b\x32!.user.OptionalMessage.IntMapEntry\x12\x1d\n\x15\x64\x65\x66\x61ult_template_test\x18\x08 \x01(\x02\x1a-\n\x0bIntMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x42\x03\n\x01\x61\x42\x07\n\x05_nameB\x06\n\x04_ageB\x07\n\x05_item\"\x82\x01\n\x0cInvoiceItem2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\x12\x1f\n\x07invoice\x18\x05 \x01(\x0b\x32\x0e.user.Invoice3\"]\n\x08Invoice3\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2*\x1d\n\x07SexType\x12\x07\n\x03man\x10\x00\x12\t\n\x05women\x10\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%example/example_proto/demo/demo.proto\x12\x04user\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a google/protobuf/field_mask.proto\x1a\x1egoogle/protobuf/wrappers.proto\x1a)example/example_proto/common/single.proto\"\xc3\x01\n\x0bUserMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x12\x0e\n\x06height\x18\x03 \x01(\x02\x12\x1a\n\x03sex\x18\x04 \x01(\x0e\x32\r.user.SexType\x12\x1e\n\x04\x64\x65mo\x18\x06 \x01(\x0e\x32\x10.single.DemoEnum\x12\x10\n\x08is_adult\x18\x07 \x01(\x08\x12\x11\n\tuser_name\x18\x08 \x01(\t\x12)\n\x0c\x64\x65mo_message\x18\t \x01(\x0b\x32\x13.single.DemoMessage\"\xb1\x01\n\x0cOtherMessage\x12)\n\x08metadata\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x32\n\x0c\x64ouble_value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.DoubleValue\x12\x33\n\nfield_mask\x18\x64 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskH\x00\x88\x01\x01\x42\r\n\x0b_field_mask\"\xe4\x01\n\nMapMessage\x12/\n\x08user_map\x18\x01 \x03(\x0b\x32\x1d.user.MapMessage.UserMapEntry\x12\x31\n\tuser_flag\x18\x02 \x03(\x0b\x32\x1e.user.MapMessage.UserFlagEntry\x1a\x41\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.user.UserMessage:\x02\x38\x01\x1a/\n\rUserFlagEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"[\n\x0fRepeatedMessage\x12\x10\n\x08str_list\x18\x01 \x03(\t\x12\x10\n\x08int_list\x18\x02 \x03(\x05\x12$\n\tuser_list\x18\x03 \x03(\x0b\x32\x11.user.UserMessage\"\x99\x05\n\rNestedMessage\x12;\n\ruser_list_map\x18\x01 \x03(\x0b\x32$.user.NestedMessage.UserListMapEntry\x12\x32\n\x08user_map\x18\x02 \x03(\x0b\x32 .user.NestedMessage.UserMapEntry\x12\x34\n\x08user_pay\x18\x03 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12\x35\n\x0cinclude_enum\x18\x04 \x01(\x0e\x32\x1f.user.NestedMessage.IncludeEnum\x12?\n\x13not_enable_user_pay\x18\x05 \x01(\x0b\x32\".user.NestedMessage.UserPayMessage\x12%\n\x05\x65mpty\x18\x06 \x01(\x0b\x32\x16.google.protobuf.Empty\x12,\n\x0b\x61\x66ter_refer\x18\x07 \x01(\x0b\x32\x17.user.AfterReferMessage\x1a\\\n\x0eUserPayMessage\x12\x13\n\x0b\x62\x61nk_number\x18\x01 \x01(\t\x12\'\n\x03\x65xp\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04uuid\x18\x03 \x01(\t\x1aI\n\x10UserListMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.user.RepeatedMessage:\x02\x38\x01\x1a@\n\x0cUserMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1f\n\x05value\x18\x02 \x01(\x0b\x32\x10.user.MapMessage:\x02\x38\x01\")\n\x0bIncludeEnum\x12\x08\n\x04zero\x10\x00\x12\x07\n\x03one\x10\x01\x12\x07\n\x03two\x10\x02\"-\n\x11\x41\x66terReferMessage\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\"_\n\x0bInvoiceItem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12 \n\x05items\x18\x04 \x03(\x0b\x32\x11.user.InvoiceItem\"\x0e\n\x0c\x45mptyMessage\"\xa9\x02\n\x0fOptionalMessage\x12\x0b\n\x01x\x18\x01 \x01(\tH\x00\x12\x0b\n\x01y\x18\x02 \x01(\x05H\x00\x12\x11\n\x04name\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x10\n\x03\x61ge\x18\x04 \x01(\x05H\x02\x88\x01\x01\x12$\n\x04item\x18\x05 \x01(\x0b\x32\x11.user.InvoiceItemH\x03\x88\x01\x01\x12\x10\n\x08str_list\x18\x06 \x03(\t\x12\x32\n\x07int_map\x18\x07 \x03(\x0b\x32!.user.OptionalMessage.IntMapEntry\x12\x1d\n\x15\x64\x65\x66\x61ult_template_test\x18\x08 \x01(\x02\x1a-\n\x0bIntMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01\x42\x03\n\x01\x61\x42\x07\n\x05_nameB\x06\n\x04_ageB\x07\n\x05_item\"\x82\x01\n\x0cInvoiceItem2\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\x12\x1f\n\x07invoice\x18\x05 \x01(\x0b\x32\x0e.user.Invoice3\"]\n\x08Invoice3\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x05\x12\x10\n\x08quantity\x18\x03 \x01(\x05\x12!\n\x05items\x18\x04 \x03(\x0b\x32\x12.user.InvoiceItem2\"C\n\x0bRootMessage\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12$\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x14.user.AnOtherMessage\"m\n\x0e\x41nOtherMessage\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12/\n\x06\x66ield2\x18\x02 \x01(\x0b\x32\x1f.user.AnOtherMessage.SubMessage\x1a\x1a\n\nSubMessage\x12\x0c\n\x04text\x18\x01 \x01(\t*\x1d\n\x07SexType\x12\x07\n\x03man\x10\x00\x12\t\n\x05women\x10\x01\x62\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'example.example_proto.demo.demo_pb2', globals()) @@ -36,8 +36,8 @@ _NESTEDMESSAGE_USERMAPENTRY._serialized_options = b'8\001' _OPTIONALMESSAGE_INTMAPENTRY._options = None _OPTIONALMESSAGE_INTMAPENTRY._serialized_options = b'8\001' - _SEXTYPE._serialized_start=2306 - _SEXTYPE._serialized_end=2335 + _SEXTYPE._serialized_start=2486 + _SEXTYPE._serialized_end=2515 _USERMESSAGE._serialized_start=249 _USERMESSAGE._serialized_end=444 _OTHERMESSAGE._serialized_start=447 @@ -74,4 +74,10 @@ _INVOICEITEM2._serialized_end=2209 _INVOICE3._serialized_start=2211 _INVOICE3._serialized_end=2304 + _ROOTMESSAGE._serialized_start=2306 + _ROOTMESSAGE._serialized_end=2373 + _ANOTHERMESSAGE._serialized_start=2375 + _ANOTHERMESSAGE._serialized_end=2484 + _ANOTHERMESSAGE_SUBMESSAGE._serialized_start=2458 + _ANOTHERMESSAGE_SUBMESSAGE._serialized_end=2484 # @@protoc_insertion_point(module_scope) diff --git a/example/proto_pydanticv1/example/example_proto/demo/demo_pb2.pyi b/example/proto_pydanticv1/example/example_proto/demo/demo_pb2.pyi index 1cb718c..6efd2fa 100644 --- a/example/proto_pydanticv1/example/example_proto/demo/demo_pb2.pyi +++ b/example/proto_pydanticv1/example/example_proto/demo/demo_pb2.pyi @@ -438,3 +438,48 @@ class Invoice3(google.protobuf.message.Message): ) -> None: ... def ClearField(self, field_name: typing_extensions.Literal["amount",b"amount","items",b"items","name",b"name","quantity",b"quantity"]) -> None: ... global___Invoice3 = Invoice3 + +class RootMessage(google.protobuf.message.Message): + """Test Message references + from: https://github.com/so1n/protobuf_to_pydantic/issues/64 + """ + DESCRIPTOR: google.protobuf.descriptor.Descriptor + FIELD1_FIELD_NUMBER: builtins.int + FIELD2_FIELD_NUMBER: builtins.int + field1: typing.Text + @property + def field2(self) -> global___AnOtherMessage: ... + def __init__(self, + *, + field1: typing.Text = ..., + field2: typing.Optional[global___AnOtherMessage] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["field2",b"field2"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["field1",b"field1","field2",b"field2"]) -> None: ... +global___RootMessage = RootMessage + +class AnOtherMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + class SubMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + TEXT_FIELD_NUMBER: builtins.int + text: typing.Text + def __init__(self, + *, + text: typing.Text = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["text",b"text"]) -> None: ... + + FIELD1_FIELD_NUMBER: builtins.int + FIELD2_FIELD_NUMBER: builtins.int + field1: typing.Text + @property + def field2(self) -> global___AnOtherMessage.SubMessage: ... + def __init__(self, + *, + field1: typing.Text = ..., + field2: typing.Optional[global___AnOtherMessage.SubMessage] = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["field2",b"field2"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["field1",b"field1","field2",b"field2"]) -> None: ... +global___AnOtherMessage = AnOtherMessage diff --git a/images/protobuf-to-pydantic_index.drawio b/images/protobuf-to-pydantic_index.drawio index a5f0e0e..2153bc2 100644 --- a/images/protobuf-to-pydantic_index.drawio +++ b/images/protobuf-to-pydantic_index.drawio @@ -1,10 +1,10 @@ - - - + + + - + @@ -12,7 +12,7 @@ - + @@ -21,163 +21,148 @@ - + - + - + - - - - + + - - - + + - + + + + - + - + - + - - - - - - - - - - + - + - - + + - + + + + - - + + - - - - + - + - + - + + + + + - - + + - + - - + + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - + - + - + - + - + - + - + - + - + diff --git a/images/protobuf-to-pydantic_index.png b/images/protobuf-to-pydantic_index.png index 4856f94544fc72f71276955581209bf52a58c2d6..578ed6e40373a6765133c560b4ee916234642998 100644 GIT binary patch delta 163525 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

R1{@^aO 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 delta 163883 zcmcF~1yq#X+BOUzB_*9Ajnu%oW z<8yH2zwdI(%^P}`i{Bc?C6c-?0r5zki$s-3wc&%%U)o3@bwL9{eCbbusYJISq^Zq6 zAg~l^tmk!&7&542mrcYc{MVU?Q&%-0c$YW*k3s9Cv1}lh&Mc6+F9#v68^)hulE0Qra+$cP^BNG6%lq^HWy$O*gvFMZ zHspgMC-P4nFog)DZtFk@Q{r-2{-0(P`JXcqUn2CwB9p%&A)3-uRDY>#{J%i(Cxvp0 zh=9zMx#w-?ZUE8+$}J!$lG-5z(M=I3q5384rQi$wV#MV`^ZiBI=V&k}Z%S2x2^x$a ziu`*G{Ch#r-~1HfPrbnp875PP%UBA!xjQQ9d){*QkkN&!D64YuA~6i8Lodm_oCQL^ zSdcP|^S&;#tZM47>xUtk@ZwS_dHMcI$IFQ?{J$oCN+Sj#8ov+-t0sgyH3ZnexNPZ&&8m+7G0tO%+zWqr2JBmVZV&zKdkZ6R=oWG%r`!vzwoUNl{d8( z1~CKw5b;CiFCC8m*Q7D1LaFUS5aVAgzqH+dn-=yrtkHx-e$g8uB$7HN@=p{0qH|1Y zvH;|lsnY*nPs{foz7ce||LlE0*+HGy)eDM25>fxE6VX&g2}r}GQ}g{Yi>m(uiO7G^ z1PJ+m1cUz~n@FnJ9SBt_sx(CN@1oU6Ltb4f8fYA#D{xQUy9K%O&+_s8RR-OZ_t-?K zlQIy)RC9F*W{TvqdzTK*$6F7g_5JTaK?FonU9=&LsSb*eluJ7b{0eKo_=}U;uLM#5 zFQUQz89IL9|0Q&MFeG#qe?tw89|lXkp$w_JR7SyH$o-aaM+ZXiuRwHJ6iJ=cfRJ4} z4*y>*?U#Np9U(^x;`Ubw@1!27K}s$i754Xdd}%At%8@z9rN8}6t_z))XhN1QZ4Hi; zrv-8TyL^E5KgcKWZx4l&3Zf_Q&l(1O!v9vte>M(qkjvz|t@jV|!30xzjUlY5T>6ln zzb;BNRm0#P>>MU^(MjGA(*E~OR!0BSNd(zx@9*uwNr+ODO#VSU0l{CevMz?cb5DolMq5?67MhNWcvw(B!#%>&fla5GbBPV4s=LEh*Rc@F1K;P zUkvz_$lXVFzuaqsEc{31gKp$MCI4sTgM;{)L%jbcWU8jcKd1!s8_39u%zx@3R*=c~ zzxNc~|A(Fee5o5>A$@;uPjCbwOqDo<6kn^gpbi9P-QQOWYo5S@mB%GMRy`O3Z!7S9qctR_819O(!dC!V)7S5Iw}2 z{MF@1-15pl%Jbl(PiHlNt1E1zAHFQTd8zMEuyIa&mo(;a3!&4al*=FZ(g%KBaDa{ zq2XnYmLP<|8|W{>iOQN`D5fFoOJm64HE%v@2Ndv#U@*=*h~a5#-Bg)XLu9~uLY}X? zY1%sVtvL8u*v?5jI`A2zZ?fXKnZc#>@=8*M36^>nFO6gen=P(2S0hf(YlRPtf(NYsCH)-`VntoKfM5AR3gG(FA6gBD{}nYH*@3nMFQ;uZ3uwr zSwJcKnZb93FApAU-_X&?-65ioN8Da6vaP6;zBgnSm+>5>VEgNM1HxHpkN<{56Cp9n zzD1TU)#stS~yS0 z7g3m*CRD`nzZl!;Tgi?*b4eUPuN#0b0eFJpXDe;Ey+a*wkn4Ay#<4RQNmEuiFjYdS10MJJfFwlUOUA0nJo^KbMdcU|K zG69iCMBihMu=PKMqbaB~Q&?#m?tT7-7Gb-kG}QYjy>7lrDlU|CbngeGpNf|Vrod{o zna~o^FY5U2ala=W(ET|;H-N>lc49&4PKZPc%3*PQ`ws!?_e3%LJ5|p+4`QJLW8q_h z+Ywys7%A3VL5JN_rEVpbG6vX_)c4=N|6nIaeqvH-YiqY#XCg5pJtAE0AmmBo5X8ug zSD@Ud5Y;O}q~NBIBF_awE; z+WTAHG(Bk&&qxF_v2%j((x0_c^Rz`Nux{5pbzgKJ%rcE=$rPNo)pe-u6p*kUX771E zQBdPM!_wdv%^xukr%AlBm`{m#@B19Wb>{v;$FVDU5ma312z-vuz9!O z^)qt(VkXYkGnP1&o%T8ov#>fKwQF>LsjHnHz57k2{b-$@d!u>fXD1!)|UEg454CGj&-DbRj%Dx4%&h zO!;!$9(V@SWS;ik@*5$E<-%qkz(+@uY{*h$B&eIPly$vxrFf_XD_-%{;$HNX=-!d` zPXhZ&()ljpzN&(;zl?@S6f~mfGxXB)-^`$g@TxyG%XcwU;NE*B#N>Y&5Bxj6E*}4lIoJ6>EBI3F8MPkZ4qOqc+cR??D^iE0d zw+E4~AWE{Oc|eye@h>zOobiLxh~k$ik)Bof67m3-u;*I5A7f!Ih;TO_xPsa(+6k85 zD=-0FR0XRn(kd@rmRX=}Qg|f|f)Q>lIY`b*g8q+<0!SC1I}ES=vs&p8P`oU2d_6CD z^EB|I_#jQ$4sanm6FN8(_JXyC)}OKIbFh{86Kr+|JVDW;dws9`o|FCz3;bRxcfj&b zun0zH1pP$^P1R0JI~4rrohG6)b73D!SfIHOcMfe7{!0jPVhX|$@9rpuX<^rtKR78X>tGi>)vpKpr><*T$ts12dyb?grWtn$6+sk9 za(|~bjJYW>!Y@6~umVj<+*7cwQh@TIMQD*m|FR$;svQHw>^LOvuIzuXCJ_AxGnG+= zVBDMA@=v;O>V{)ySdA9&&@BWrlT(so{39^})697>H0M49_o`iwIp9mqO^)W7f#SFM zd9rQsDY5+VEDzt@6Ia*kW}#k&If`VfIIl# z14#U+XJ)=khAq8NAt5F2w5V&}b;&P&eT7y{D8!ojPH%#&;X`cGY68bpTrNBh+x0Iu zII0PKL?~<0hW45aH8l<5gGY;0Yq*<!S(6-nGRPt|CZ}?s`eM~x;UJM07H@z37rD*HudSup+N-N|JlvO0)6WX^ zF+lZSPtUe>Ju$DJa-PT<(0_<)FEtuXG+|d-^{lHLQ_W(dTT5kNxLfz*VXcdeK!$|r zR#j=o=Sk0Yc*kP;azhm-?WX5lb5(i9=`(u14_YYI)Ienp3d@?! z6}Q-l4e9TnCeoyAZbImU`08M_heLWbK1UWzq$(XV*PF0Q67)K4)MR{P(2UHCqGwoD z8y4TZO-gl)e2Y-DxFu-nv%yPK@nG}w_w;Zu@iT-+4RT5*;tD*D)SP<%Q=)j#xP2z( zsTj)J!bYGbjw=W_G(1*6%z-zbYv=ZS`l9V^-h)_+Ym?rvIEl?q=;1^%dXtLsj~AG^ z26}TpUiI8cYBSr0$hxp(A?8^@Py$)S ztk*S%)+$}yIxg%UM&+bTYei)v)?)64DVP;A#U0SGL_Tbt zB>NqQ50OT6+OW?3$19-{HJGDcQRO$~bAu@OQSGfIq@?NJ4Z*+=teDUXx8lm#kw<9M za{*+yj{5w>6RNu2_IeV>m3%!5?L{B^=;xL?ANZAP&g^WHX=&YOVHr*fUA&4*l`#qR zJ?QDTor-lq;tJm+A5QHU8KiZ z*P$!3>1LBdUlcz#yT4cA{yw9G0#zNKwjqFDk!p;iU4z}qWkhJZ-3+>CSqA7JXdQIc z__|&fPWM^}XdGeAb)xKld>G*{dV~)D*}LJfd{Gj+tlX4n_V0G3rImgAcL}OY`7sNo z%J-+TJ!%Z?^+n5R?%k$qeo=>`>p&-~v{SVd&)HR!Y7(vOXWF*|#C%M9wZ!tspnQLj zV>4L-amC;FHcQ}MTbttDbrs_#d(_#plU$7l>|MHbidJu*43$d?R9V4U2nLkVHzcQ(7aY$NM#JLZ05A;Meb8>Rr?#k%;y5{b+ zIBLSmTN0^cm6o!%oydLbWAaU%SB*9G2Tb>y1*e7mI2B6LoXffr*?a8t8y(4Cee$sU z5ax1x8loxenol?CU@DxQ?Ok%fUuFn2Nn-IZ0{l-*09@BokJ5@=x%IGh=%`~y?%6k% ztm3Q9=dY%CS^6T1dlimD?r!-bD7UKW7M~o=1T9I6sR=Ye?Oa7neYb4J^~M57YI^P~ zKAd?oz5q*9g`!lC;Z^hSt@bwMvc1iEkmNqvgGDnDDXokd;()hzeLPW`xW^8FRka{Y z<2>3zBhQ*yI_Ts*^w8hMg6cl(EYAELa?O>%P)w(Fdf)g@aF#~*Wqr>xA}k<)fxzWU zTCVAI6ztd}peNij-%P#&UxqVq&G!aJsKM_Q=0PXoGKxK4_`Jg^^Jz3ldzgyTHWz8- z5npO|bc0al2r zQUjlG3W+ibYbVyp7V9+Q_)>9A%tW4`!X*;nt-J6gGOJnUSdGT-BKBggRO<#lY?Kl> z?6-+0Y5x{l~k3ct&(T=}e{G zzi&En4+j$lFq=X9Z`lkO9%!m7ig{Xz>fRhAJxNC^ZzCNR-=`?ckdU3t9y|y?J~&}n z@FNh19HGRH%Fc>V(nCUJrStdaopmdJ4-7!lKpGOTn2d7$u`PNCnl$|lc|<;Mie^^I z)#`F$*79=a*F{z+8sFMtK4n=hiHQr}Enq{Oebm9jmH`vu5Mjd^l8f_&yU5Wvyvktz z3g%tO(lRlWqW?B>7hM$VibnTDM~Va!~@&#%7MUK6_G9elN>(+ zkx|6HlX{W&Air(bAW8yk#dVfOtB~7oLNFyIB+>noog3?elJammz5EmQz(E68obU2S z^u%+JvmicPBhE2e?TnC>JU;eNlSiUqnF`K7{xzQS_jyn74(LZe*WLKHW_i`-D{Qx1 zMHZaXT)xX-zWE4*c>nMqFEcEn>T5WM=V1MPNAtH04=6ShN$^o&iN13*8VqOfcxqEk z=jCmFwbGJX*`&e$!-wV9=3E4*U++;akkzPO7US0LVC?kvvQf1xcc8c7E1x>L9Q_Cg zr`i`*>7V2mO%=h_BSs!PH~3@o^m$m#tRK90XfVm*>&%ZN3Ch$kH4Jtm`*Jl@J{f=q zHpe$W2O8+lto)k@^eWQe^ca7bg-QfYze^O~mr#e0Ri^EKiJ@t*&;eYZ{qnR?+wP|2 zbRsCnwZ|ZWa$lQ&Tv!7bX24BE-4VR$kCLTj2)z?+{U;XTDnk+}Ig4_BGEfaxaK=R> z_QzkdIWOc_G6v-*9y{7-{8!H`WY1=}1z+(JWVmHTPE|4Miws~L144QeG>hmBpRd2o z;yGJbeky342jg3*YgYSNbiCK(s-_X z^*2%JAuckY(rNe`YnLoBt8(vmL^wD$Hw=XeI{n|w4-(I_DO*WdfN==_#VgE_1$-Ho zUhMs=r6~-5LrFRx%+In)bM)H&Ws!>!?S8IQeeuhDI!JY*fcq(&3v&bHF5OU>->Eq( zYeKGoP#vBBSpuumz}*WZY_V#`qz;rVekzG9NahClRl}L#nsrMnoMl~4W#fjb*ism#k8(jdA z&-`?s%TVjJ+8;_wqep=IPdUU5NVPSx2|W%ZLq`$ycIt)C=%z)DcXC1V&aH;!p(kUC>2T$xtFYNINxKi8O>!SZQCkh5| zsgYRC8ORpFY)Bw6m`pCVorGTK{H;CNZ{an6q>;`??!Im5^Z=x>M1xA&U*ej$3H(|b zAs4vPk%u2VE|i9Rhsq%%36Zw|2eO4GA&4Z7(rdY2veNHhfg})19WehRO|Tp?dm0h1 zIs=#5O$;gtF<|su_oB5SxYB&)qRk7PA>YlTkO?Nk-u=4(vPnKYh$KmpJ9wsn1ZtWH zB!L%qO2@yXiBK%2?wEtPULJ{m<&J|>;^il&oD4Z5&r$i<}_5= zIJ(!)&aQ3myZ`mJ)f_*S`f1l#hn2zf{x@$PO39mRVDk$JS@k8*>c71I_$V)u4#%+a zv*P5`loYHqzRdMEJ)-F8NAwdzLDimunxO$CQ+j@@mJk5)_oJq#r$>cmy{!gfNIA5a zNH&S+ZbC?scAIuv->v2XXWp{1U5`=jN4z0>`HPGUNy11ojHy#kTxF&5A7AN6su*2oKCAWOl8(Q=o7Qu*u#%X?X z{waSepCf7cS?8;4Ow7VFT+M+nS87Q&H=KlR*Tn@7I`;-1x$^t6=vSgacHM%M-@hQ7 z+SJNpwJv%poLAW<^I^;T3V5mD5`b`!6e(MH$hH+_%G819(AR{3Q=^Uc`J ziu|uKRHdO<#ODygtLXFl)Hcf<_iNi7c{^5*a(@oO`8ph51|97aN~H3b8^0Z<6~rB6 zA#Rp5J>MCgi0C5^lJYKgjO__wip4$)8o4>0=X*B7vMsEws0T1})sxiE_)4gKrcs2l zQzI0M5YH5^E2x%(nH$FK1Sa~m$Uej^dcv^Logk65$N_tWBtdkvJ@4VLIl~IUafiFd z0j3C;-+jR+ExRdf_h&X% z1f%iz_U#(O!Mz3(o2jjJ_B5%P1Rq4j@poAETF#=6l;UembZ+EX1^JVD!#^2VFf-w1 z{=QsfS8d&^SD&jMk)UmQkA4OqD!Q7+##=6pfHBNn*!0||?8=3E*g;}#Dry5!8H%qd zVd`4n@7i&*_`2{>N$qxTj}>l`HLPRz>wIPCOWT7hynH;kb&57!;O2+s>U`x--BOP$ zygu%dEGtD+i*7b0#6}69^9CMtRy=2TWUaxEm;bzqsOf>xybySN^ywPh;MDdA$h2^) zM=Xbatzi~{R#yT`&qA` zvCF1Zm)DY$ep3M%vCW|T?Ld|g)a&=l6Xxd^?x-}liyTbiqpV}>HHao#ezlzG^)i26 zV)<=4HaX=fQLfB6T%Wlix=Ay zmaw8d-WY&68DOoFUWa0Z!*_L|<*PfM=gwrjrx4U@pOj6hj;hmXrnY6g_MBqy70KXh zTS?asDdsr7wGR$6g-6z8)E1NmGepon2jSvX>5y1ToxM84icx;U`^^8UJsCip6JCxoz!Iq|-)yW_vgLb#w6bh-V z+}0Pqi6>>)ay_LNNbCtQCN>qD4rh$Gs?kY1r{0CzW zas%M?+H@*R)pKLlmx0EW|FogPbJT9Hu zrxCoq-L7{Ad_6oqG0TyA0L4y!c2|dHGLZ}r-8p}!x7Qa5;3Y~BuNRZq&$*$0dm+x* z0Ou}Cpp#$t^cIR?v>&T0#_P1e_bBmB&l86xjaT!bX;k|0n7NaonR8ym8TS+BZAaNv z+LrFS(*zPdc%|z((34@`TS!EvLSrT0N_)H=s|H?{1M?vJn!wy);W{cRVX!d69x4ie zKjB9kl)Bjcts3WP1#~iF&Ca>M!-zo(!Jg!3?j8*jdK5u~2h|WY00)r}W&(HJJ)jD5p^UoUOMh?&UFyqr!hJd%c zKI`pnL{slosVoY zh%9>R7}T~7oivfu`6RYC@<3rRk5y=q@p_ouox{TVH63xuMka(N5lw<2pVt{S< zWnS3vO8cAMKBeUc~g@MCHX0~eBA`!nWHc2@}J=bEu#dY2SE;;xdMpd zD^Wh9H8fCDn4#BLHiS^k2ll+MwXX+{7Ck)hi;5}CtIJ=bGPaYDhKGl9C-$X?n}d<_ zch(^sDg7ybzq9dU(uh|4D!w49!X)BpGBDn6XJ2RGZpz|lB8<@oc5W>;br6fCMtyJfE-+mJp#$DIV_W6uGB=cyMz|!u3tRUDlKISW0rUi0@Xx zW)k17Dh#y|r^2syUe7T}jN8{kxak?~QxFKfB-T@gcPF zp1d=N{n!fsXjx z9B;c1jR^AIWQSFoNs(f5Y$QF&ju$s04m?o60PZLz_Uba@pE{^>-BBRB@vMlfn&!do zUH-Fq)l{pg&#Ja=MzvpJipIZ87nHiMB6h^8y4=*OWuVOD%<=}~Edd@I zC5Q-l;=QA>S-J104>Ui$AXBfZmf)arnlK8T=8eT0IR=0u9LLsmL!Ilh@o!#RKT^*Q zjR4S`<~3Tdg-n}Brt0k>58Yfwww{gGIp8IHLcQMnov*pkllGzO+gf)|$+P3tRn6yX zRt>@dQl`cL;l0DEJ`Z9wgb{&w-HivIVZu&<=BSVdTc`#>I1ZK7 zP}+mPk7#U!NJaS4rss4*=5bLZA?D)4)K~TdgHBLSB|@}>RnevHpzd8qk;HDtpTCE4 zKIrC&dJ1X(isF5+Q;q5`@nzRpH#~3H5gm#HeXhHW32r_jgS-MH*ibacU%PMy#R3dr z2mmj-=+wox6evL5g49s7Z=$TAlwN_7N-LMYlug#s4+?UFFRZt>IIl8&grG4mx z!nXEY1-;!f-ID>cT@vLSf8L4cYq&h3M0Q;`5X;=Ozx6SStx*Gk8HTg`noYd|l(B6% zY=fz3r}fd-o>#0g!&iP9_^V}~LxA~nnQh84O&YT3UY9lgbQ&Y{e8Tuf+kqqexrO9# ztB(usy+6*WxePA)y}T;D^u(OIP&a(GFZzjBI^rEaO6~^FwBwE$8C9G57cIc8KVh~i zw!&I8h#ZB6Abver>crNT=3Ld*AGp@Oi<+e_78+*#zPyFw%Z%fDYArMdLJk0Z){Ss> zF&&{9a15=W6mfoSoTm9GBK7EVr+uB($kGT!wzaIHBBtrA2Tf%lu>$#$`A7q#aB9$5 zB-`7rk2mXE2K40%Lh|@`A*1M@1o*9Hf(;uu@i{X^aV7+x4sFYpd<>`6EpaL9irpH# z`e;>nUTL`Vi;fW8tUpc7c#S#m!8UH-Mc2ecw4NuO5dPd|8B@`UwF8X62{C{70~~=LDevbuad?`o9%JpQc+C#%m-0xYe;5lP)GW`h!_e0@2)aU4VFr| zP-F@^g~H6c;$q1yCL2buegu??yvCe*wW|<07o*JGcEt*}j&QHh#r8yDOIRno@QaMG z=XcBT5V|b!iG;<$E8;)j;HOpIrr`Cx>iThTjjb6+XvLvDhP9r-Mn{CZ(UwJP`!4_I zyXJ@p)wMNw>>RP7ga_7B`#>MLm`gJ+3Lq3bP`dTYm-a@kI| zG?hX^G5mJw8~e!&Og-AK`jjlmUiAoe&Ed0g9>VCDlF5M)eAad~6qrm%*D&fd@sGIt z?%5=BwS6&C6xJssFWJSlbyjTJ(Asb7jeoP<*ov)BX9)QoZeUA*u{wj|h_%eUB2=u5 zYL8_?OcpN`!P4=CydeC0WO(b1#GE&xD9R}ADE*>}L5a=PJg@c*o}vu93I&vT7d zkA;Q#aHDhsR>`(BiKZz-yz=xBa#k_X)8YFqSknG;E+Z)j&?N*1xHg1m#1_!h``%n- zTnVIWhoIDZY4Vb5HR=!TM_JMK>79w>dR}?bAO0+UuH`-|iAmbZrx>j6E5S!my@67z z*T`E{*sq6TFlr^`EJ`oe-W2!RbFY8K#@*~8`!Syj5Fl{levNJw3rp4%C&M)Rf$@0@ zFycEV%Mu&}E z4kMSkFq{|>1EubqM*UfelrERTIUbLrB+XNBN#G7L<%c8<6NyXQ&2RR z?sdcDRn_v}kqStm&M&+V&!b0T;?SCGevo5fw_+*z`ef3O`KFTOr3yGl-cB z?xw}YmqE0=uMh5j3dZ1WDnu0YBWb&2#ghvS9kgs_*xSAHoeW<5#@7s!P6L%tBvVlt z;~TwuOm1j11oqcPA?VLF_;;R3a=W_HEO>XCaC6$$$W&hfs+F0#!v8iy@ea`vgXS~Rucy?h(4 z$erctFDCRM&E6)mYPFN84<1uT!oZGx=-1tc;(T@4`>hFQbIzE{5w?ML)GEc$jCh5U zs?gX7d{X&FZfre`*qPXi(l={v#dmRblK|}V-26Q&6ya8lq?Mf_=kE%(oU8Aszj-*? zHj9IEjMWlUgj#Wtw>Di%+rf&@rg56U+k%Azz^F!H8mW z4uaoHEI|0np>6f%y3g9X;pt>|<+T%(w&9Vnn+(t`?Cv<*tmG##G?5q`<|ud$YQPJ> z2!-YZ=0w;BVGc3OXZX}1fjnxXql?Y8X+iKv>}PkM)=ESt?a#u&*Y2btG z8)s-AP!taD$VHhZvoMAGM%UO5>Rijhs&ihXFeO@HvVXAVrogJrlt4lyMOVx9Fh%ez zH(cgm<9(YMs&w8f_D2tI2tAq%0#aPm?oevdaL18Y^79rvVFY@=;JtWEtP_*Jz=w;X zKD*46sQEIEW`!xm=;<)mQoB3t!X}%7bN=vvJA7R^d&FDfAvuPXnqgNs!xe3HXaM?0 zrJ68#D+Mz}9J(wD*Moshcfz=`1WtIb4jGA{e(V-5n;L(mQ1UdcJP{DI{KEy&d~c1Rz_8Jz5@;Q9sWF&{@M_$k0+@1 zQkalF?AD`W$NcdD6G0Lk_8ONXJZ&CLux?heSWTGXnpdN8^Wd6INZf2r z5+2HDgT5lB&dGwDS#B;*#C+3khFG}?J4u}nV=REm#hF~_F;WUONr!lnG8noP2Q0 zp^O=yrtLYaeXrC6yp zKskt5Qtgqx@czn^5$AiZz5M+~BH@J-Z*MwQ-((ra`ezZWOVWvT0~Y{1ryl&+-Y-O~*$XnC7Y2Oc-2$xJBtN@S%vaQ{ z`F;4!;!f^UJ76o?!(PV`J3EdQrO~8+W}@P*^;C6FNyw`crQlzYzsW$@^eb0wi8 z9cI8~bpP&HC_NBm9MkMytqm_+d+SAmvDh2sMU7#uaLzX)4abh{rSUs{2iS;WC9wLB zY1!k&_DYQQ?T6rIOfz?3=_thUWovrnx)~SIlEkpnyY}HsEvQk0p+_?@eAk+eUwK~; zj8-)8+=lsqBT;g~^v=)_jy_!O*#p8`Pxj6!AHQKU;4@$FLQ(kQ$xQX}v$H9nup&u) zt(D!A?`Fm-Ne$FDGjf9uxR$#7_J)*}etaB~7(Y^439f7OubFeByJ3C+c`@iQ$j`h$ zqVol>&FXz(a<~j1xkndGrxXI#E(x=gFlPJooWR)l=}(JZ|67Mp(^oxfuC5C(#e@;# zv5$xEvvUWu|C0@#e%C&H=v`nBQ}ibA;C;b2u{&ddoEIoORj*Y>AYN+Zlop>UFbbn`7-Vd z5XluS23%=@Q%2fYS6OAS{Jmd8m|Pnsjwi5e)5l}hnH56#d@MEAx) z65^a!7J0p<+?lChS|a`pHw%K+F%u+SWXFas@D}lrBoDe@=Zu?W#*FDp&UaA<#$4j& zDLS#N$tIPi(j>zyf0Qt86c3+d=7gLTJ1os==dlgCvshe*JxmK^pjNPmY%2+4j30P- z7Nu}C;VwuSAMp4J<|IOS4HtP-9YPLAUUSWXY3dF&j|o`C;CurMDjJibkK;So*B4F9 zSS5TLH?|t|xsh+72Tzy*zOSelMrxC3bRmG-<^w?9>J`yDO_h=P4@vWHvzp@)a8~c) zmvqgO!4)!iZwfWbKk{%685rPjQ{|JuU>(h>am&BG5S+5%Z1IAqKG(JLjcxvL1j9ss zM6&LJ4qd=AnBjGWgZ!2_tm3D%=>@`kr1E;03oiL@x>ng@>YQcNflNwLQnG8p?D!?? zQ7_^ptsV&hy~4d1pLp|W%ZcThiGsxw+#r=|S@4E*(x9(O{$I-PmrL%0}YL5YEFr16dIZGALv(;!-BdTQ3>t53Y0zh&SGQCyi4Xm$VWr!AWve7#sh(S{F9YNk2cuLS_C! z>8z5=lMy)*14>$XeDTt#l)DOa+7(f+-nEL;CpYT@QUUEG)FgrIYVRE#dGJ>`R!}3v z;ZfiA4RKy9U6W+v(R#WJ7v!UA$&CB~Wr)$#S>V61`h$;Zs`INNU2*?#8bRh% z8se|K5M!^>La`=cCC?KkbGAs@6(v*H(F$7fo24|IM;v!oo|7ZA05OTH#f#A- z@$8NV0dIS_ia&s#k?8?sQqd*J!)+ChoR)HZdUTb>h&{|T%aZz>?tUcVYL7-jmvKcQ z1?c)eey?e@J?WBHJ9z4>M3f@t>OD`@$Ayz=QdBa(L#kj4d!4b)#C9Jx=XhK*xpyc- z^k__@^xh<{0=8Ri+D|}OkH7eMFx&*Ec5r6UB|~$7lf;_66kz=1=ryJ}Uf8d8LOG`vLn+sXef>;o#cbYuGU?&T#?7gMF8OB)=s-;B;E~e);}MhQ z$P{;lje|qyyuBK3CPQc6lh0R99p(l~)rctt17D7tb5mk&j$b?WYQX~}$m<& zlspAKq0*G?BM8b(z*?I4kE6+fV1>dokN@jmb#D>8!8>x_^w$o)PxdO^T1i-uL8}jd zJ?EZ{y*T~8?h2OR%poAfI+pHy1epcigYrfSl@a6*A}4LY@g~vkn}N?`4Ws|yqRJey zhL?yYS;D&3T@mgjl%+76=PYWDwtq~qBCoI z#}>7Qyf-;89UOYjw6`OKGqT`{88cT?CPIRSVNz6y^ z5C)CrpgSqT46{wsRLBbN!Rfh}vA4P6Z8w*=KLoQEH@l9L4~8{1ptQa$UyvZ2|>tKEv%@n&AE!61cRZL_(QYiT-Ch`I%Dgs45XwO2T#bg+3`&5Hl)$}d#n4ehp zvwBg?gVj|=Di}`2R3#7?evC-!Y_r`4#d)n8Y3E4mH=Ss~ldwz{YktJlCb-y>0 z`n}zquFCp3{0EARoC$E6fD={wSwE6k$vqLcz-}-zGcg`lb{hYO1hex6j5s|f8WD_c7=Sv5`6y0UeH*~jdPH;Qr~DKXlV)VokeSVcqMsB zscS4%y^9!_2tl0DPfJP-vZo)6o zv$|GMz2vIdQQr;d#9x0!w^q%JQ|1Ibj_0;j8yu}E#kW=U%|^mFo|uN5zHT`l`lfLw z(yhiOF(aDCAC4V(a5%@nT#ue=%16Ch0TAg9kqu@ThXkAu+yGAfjYVf8q$&;LId*;s zPL7>ZO%(K5ImvTL9*!I>%(ecMYJjhg*1T3X|2TC5bsSOTW%Ys4ne-SR?{WbNUx*s) z2gDg~ruS^#?_L=)()25PU@_IuZy)+IV-rm2daC2&XpnXuIgP;w)~*bUgt(Y?&Wt9hB1+N?EyO>Z)9(|g^${6Zb z_j=m3e5+~7rtqmgUlFY=|QE<5v&w zLw#a0&a}j5?-(UwdXG4BLwBW)v9#QjYqk!;$Lp7Mbv~WAOgt~w1Ps$cm*R{I8wnOV z=EURQN`B*d(R>Djr>b@56bKxjmN<5ewbemVP0oo1GqXY?iap0a9=tdGN<|m=aJ8|y zK5g(W6Dw=SVCeSow&Ysm)H2@(Gj}5}zxmNsbtM8>|Jc=^z5zaN0b(ExK^CS4sp+L+ zK0zKWFF%`<1AZa;`n{0yowspO7@0HpO%mAacIjAn(lZCHqGBV{)8@vh$@W5)bRV>| zoi`WPZG{`C)wdkbIR}p4DOxY=vkJ{CvBk<3+WI?QS+tc$TyOTk8H2u_7>)Vbmr-lK zFRvukSUsFyd++qbACk z5EH;bWz-dE*G8@D`k7+deMSpjC=z$wTXeJ5ddQHb+7WyJYEFiT0{M6$W=Q@9=#-nbq9e^6J)@+bJxRN9RYtxzM-Wxx~5C6`_D?Tf!avTFdZ}`kbwa)&Mcd zy?`Ys8Rn=gIb{xJ%cU|npR>>5^<7wW5ka6k< zogXKjw;MUNgpZl}aTU+JJtHG0M=TAb$lkXBQeu zyqK4r6acGF4bSeIp#S3E(_N&ugGtC+-{(`f$J}*a@W%X6L4FnGb}P*6xhvmC&4hs(E{&?5Ju@aUuQ8VqY{F`TwEotApZ* zyDgF61PSg04MBpt1`Y1+Is|v8ks!e(xJ!`W?h@QJxVy{X&JN$J_v-CdEj9lEGdZ~s^H9ud2%0PhrC1SI%x0}4tmi`{T{&DAywWIPsl^Gn9gl%`3^%6R zJEl2pzoD5r-kX`o7$V=T-`pA9xUIX(2{=k5bk8Xw={SB~5d<MOFoyl^@M&Iiquc-UTIDxW zx6C5vG{kfWPhV3Z;6Eix1s_BwypF}(BD$48)4oj%ij6UijX zVacX}z?khQzj5G*pEMeij?cIU(feUKtFIefD8lFQW`hs16XfLp!tO+Zut&Qkg&ved z_pcwH@BS4EnjsdHX1(fn_zLtz5I|IIZP*D335HY-ZadFsfRK8bb`YIP!Fn&AftZsM zo8V>2`1;ksv^_U*~r0<{5SP~O`nXWD=%%c!p*!4uj zOaMDt2sNALN(;i_pfo;ff3$r%4-sO2*Xyv=M-adSH$3ol^75o%jq{ycSVjbY!M>)V zqG5hzHuvMO!OI7o23rmafnezW?L5{)v&Kh~bZPkO;^>q1XZ#Z6F zZZ1qyQ`3rI@XB>`dU?w!ud0Ry(N~_6$8NG0Lot)2U%&8O!F%WeS22^c$jjfpeG8tD zo=kr(%pUTh8;C;}*A5%Tw)8;f@p*k*<2p9?iC~YCRS+qEy*79a+8ItF`pQn(VE}j@ z{I-4UYPJ|lw>;Nxw9oknQYk7J7%e(}dh#abbsoUDDOAXcV$`X}Z_g_lc~L?&2j<7; z3;+_yPcRuSqqgb%8@HGs;OWfwmT!xoaX&Zw{AyZ(=;o6^FSF-#G5uSkg0c5*Q<*)= zX_xh_A?Sbr0Jllmrx2+M@Lzd@iuqqI%j1|Ps_kVsgAur~x#0>M(i0D{TCku{DhR#a zk7q?P?^=B6cw#+m+9NI1X&A#WrTcS{X_1*@{$8|kTfpn)*dqxOTij#C;N=2u>yla~ z5r58SUzCjXhlT`fLv1boWCsAb6Ym|IPnVneY-JPCfxGTtV8R1Jr6hc{c<0RBRmWE- z8P+EmMa*(&B4&SAN)(oX-DKB(ziXS7mif>>u!KP7J%LeS*LJN=`8|c=bFW@l0Rwhv ze0;o;nwr_UBX36Es}_Fj=>EEfzDF-cWJ(M7NbI$8av{`G0!X%HZ#2!VUMND*wPmGG ze)beud~28qXc@t9)oQ;kv&+fJfqwg6G_*AYDa$jp>v(phY!hu%+%{`Blz4L0@H`}=4Adf{3?hh>GfJJD zvJSE2>nY%hnIs5FQjJ{ZFJ@^afDG)({W3Ym^P{0Q(ewCw64YX~2EhJy)J#)A?{M+Q`l@f4hdBOTy&%@$s>(-jvF# zI~JU)1Up+mveHDvFLN=LX=#ADkR{Lk^?|1%68?pplt^NFn(WM6pX`{1qQd>?crKnRC5r5uG`*@ieU`&|P&i4T|=HUcGdFZDN9%r`?O<=L<8kZfXBMlpbt8C1IR%Sv77gmBzetWuNB{t2PnqYiV0X!d( z<}jDRp*!a#4~4Fcd+)MIETZhKz|VjvAY0%(-D5%HqEW-g_&odac>oQmy4=>+Q!#xT!~1vB!>vR z3$TBLFM*q(hw*`^gc{K+;ct}-@-^GN?=zwWA3#Iv_4$imu>D(3n@gN_h7zLb)p~;{ z%3a?oB0nev#+DY8DsF3O)m#=hLDG3#8;JROc}inp+UT|E^=IU41cECSwo-Gp>Na8Q}dho2FIZc5hZW$)3!vbQriQL8adsa>??zqjFY`1cTOT9~Sh1p=%m;Hu zcZ}l{n3?{4YgaDum&UvUD?c08Enr@GZ3yW!5Lh12?OWTCwTk0QBFo}p1&qm~>QgFh zCW45mi$prCbGtX@wRCgwGBH?%L<)x8+8!sA_?Tkn$1&mc+7| z6w`}R5E?Ww;MA}Mx}P#~MWtVL&qd-U$Bx_ZhZ}*tHY(xIfy2x3P1XK&q<+ZuoAM4@ z0_Av>2}fh0S-Zs>CJ{cy4q~!)50*^NE0)^(aU}ye8rgI&oGD&clcjdIEI;A{doM>C zT*i!On=c&!vwvrWM3fGqom=cX=9N1TSPwLTZb+V|dBo@XvSI3FS(e^^yAai)#l0&8ApZ3aH;{s`8AGr@->gfIpV zQ%SApf_LlDF#djyQK*q9|NcPmqlpipR+IB*w?xIP`tBQWls0g^n^{lWw88Ep|8VpY z-V<@+)6sc;MnEI(gyGGN+#h*sTMLSobrX%If;YS9>fpO7;?FIZK*w%|K8IU6aG!~_6jgq2b zXem2iz1y?y9$6rAY|6cLA!B_5SyMD)tYYIxR_?&Ts%5(G-Jk+5|fq&n}jehI_Osd zlp$TeANAuw>J8E=DlQ8gv=)^xhxa~prJ^pRxTS;l&iwno~ z6A)w-B|Xeva#?c})@sMtj9BD~77~uSYYWN%Id!b|>l5PRCpF5<;zKQO&Lw4LV=-t3 z5h3}xxjcvfLt;`>HcdII*rea^GIb7^z_riI-F!z(N0-gWS%UKoSLlzGJfVN-0#DPx z(3q%*FX!rNE0R9tpV_cY%zKdSErk$0T$xIeXxjLGo<3&tXR-~Sto&Pk(AI47%pM_h1>A>Yu z)mjd&?55v&OHv2N%ii0#(OpjqDpmIh7WPFR#{z-Ba$MIBadUD1;6;ZI(yG* zhqXB&|3b2RMsFInLxq{{6OwE|m!bfbYQ>ix=FzQ--}SS11q2hu z1@^h9eCdGI*e2`ylDQH{|0?9Q2d$@8Rll)!bwwJ7A(i)I!YgT2Bn&Ztgvhw*rbPd2 z<`ox*FDx_+4vtR>(npb!D~o=k%%6*oPk%b%1;>l9A8yL@; z*J%5mHW>V{UA&)hG|>Q%F=ftoWezyu6YJu$Ieb8{=x{?Kcd~sE-K66`3a<9u zU9uKT5zCt;0SJ7S5T~|G&XL2&jjkU@G<^=086tz-&r1cvzo85dbIhKPJhIt`@`%;X zzHhh^m3_3ObMX@D%r@6|Z)%Rk@FET8h&`AFay?2oBM7+?MZIp6E51oyDXp=YjrsCA zpBgX0HLD$Q^1t3unj%5Xx`Vr9;wN@=*EMxXMf4?hfmiPEeQdUP{j6ZST}!5XZKO-- zz3y>>Is8P z&8Zw-bYXtCB!ObW2)W22+Tde4Ed2xeX57pxfEhd>Z!$SPCT2aml*;LM${ z-zi+Hi9T@`GcxpNWbfokR(Zyfz5AmhK%qdjzJJ;;fYrx(mRC%*h@__&|AKn9F*<%Z zw?R6U;bm8KQE|JQ02|rwXgExRmK8>muSBHHd1KZayw!T%Q}$$cA(@l|9%MY`QRQX7 zDPD^7>i@y^7(&hc&UyXkM)}iohtSj?J%m(tRHMt__omz1kPSz25{dW94E{dLzRc)iH*5}D74#aX+^cXPD+dvjLS8f&5hI7#+AmFa@gRIU*07BahVbsI$V_vRa2 zaN9V|;3t%5=A9L{6a7xY)ePQ@xlEs@59}|G!wSqNZF7UOjgvL6hR9oPl|~9o(*+%! zPSHYvGle|p5!S=GO_xJI`ZC~Gfo~hJziaoWQh88j*}y>no6m)i)M(r6LOqLB%!%z)ffxFk(PO!W|F_$ zkJ}yaShFu6NzON02?;XVnmL!~ntL|{B4nlx{8`Adr_mc`gbmqB_Krya($E(=Pfwx+ z#}B9tYW8&FAg2Lo1DPK8AI;@F;<5J%S}zPORx|kn)2BXkalm(VrA_lwjG#r9QT1gQ z?nQ}e4t*n^b4O$+Ey{Md&OEbv>@`?X)j0DknqB=j3aT3_<5fZa1ou4&=I}wiWmi;O zB@E_}w=6M8S(xt4ieUk@GT}N=lUE6v0#p{}m+m#utrk8W@N9$u*lONihYybVDCu`H zBb@sK+;q(xBURRX@2)^c4qMYi$+$49G37$LP}n&s*8Z+nM8&QxohZG)^Qjl|S^VYC zA$010-Cc1wME2!Nzg9_z;(z@L2)DzJY-7Fio(+%|F`U5lnO%d|OBb*G%;0J_ACAUYne*Xy4kOb0XTZO`>C4F~sa z^;Mc&(#T6>{WW#$8JrYzZQsM0X#9ewV9mrfJP<`8~4kwHG#Sw2^;hN*hZPEAE?fIW`))?WAtk?jgWaC`;99F*O%z@{AufX-xy*i z-;@b%cB*7*6ukNBVOSWL(CF|w*oCQVFr}=|o{b}W`PDubhTMfO=V>+bNxY={Lsb%; zz17H%rNt`ijW{dBNZO0;D>|ywNaqs)#KPme(gp8EWniMw;ZhKlbRu-XM+tk~FS;QG z&Z%L-YaWqxGpf==fr>N|!Cq2Bz`(mE10kzDn%V&9yO({R;f3DC_=sNuF=Ai->rJ1L zr*)F4i1!{lgYB{&TPmzFL7kp3hj3Wf+W-<_A$31kN0oiY=CLr_K`kpv#>PNoi!fHt z<+~R?@Vm-pz*pdO=!JcP*j;Kg|IIM4@8Xd^fu3lCR6^9UVCKLs%@1PA0(5Y;q;TIK zeYQr~u(;->N=OL#?g^4PCSwr{6liDUi+VCQH`;w$!IKOyI#>A$Kd@@{%7yp#zhyxO z4n+MlJvtTL-rX^=xBq?CX_Sl=Rq0?sm0XG^HmXN}pzY)RT_NK?o7MQ7lh(VNesN9^@oY8936V#Y`GInrBU48uU{+8Ss6%cd zd1W@;j3d@h!?u6g@pmwYc)+^n4h_D*yADe7%rF@Bw0rVom`QXC;mmOIUvbaEG%s+( zaoW4iWb1u>LP6Yigmq}SF#)DfJlb9JLmLg4Z}p`#Poob038v0=0B66CdTG5n_G!J1 zX=#xBZ!*CR-#YFG_5gn6SL>$me})?;Nw=fVTHm9S_IjopMu~sW}c)# z*E8}08BTbT1_`mO_7s~FB48yDp+r)Bcl(%BlX>=(h6kOd;$e4Yrq|ltGR@mL^l+^c zo}Jg{UvHCY&&`UQ^)N~RSjn_!2!x>EO(TVYB<65OH95}k;*PhHWVePV?xcsbL_n>S zQ4Iy{g9>DAa#1?464#cUBNafsh)G)@y_`-RCH)I`;aFcb2eP#2c2jM-{ z#z3FUg!@&`9#6vsk!z-W?P#Hd4gsjNyTTUZ`e>%fR3SF;!}!lF3wrcO4(&9B8h1Af zbF+okDKv>R`z##VPN(*&{1i0X2*WO8W(c5FQYB} zp~(YRdF*&=xQXaMz@Dls^<9hssApKbn~SV{_-B4P>(_luu? z)hNT`@I{2Gr!Q7z{YT+7`*erSSJ!elw^#TGe2!E@GK32!mE@O=Br#_)iL>%iMAUO? z8qTQg#_1=1;0R9}l~BBabHlXocYI2M!pP)~_0#B34^o54pw!2fPUyle)mb}T%fP

iZ|b_9q7(!gxHBQveYnE-ULBIwUi%yw&}~__xGzT&Kwigs?xu0439&pc3L%<9#p9dVxG~7~xC^u=@W!ff-;)yEl4Z-(>W(1@>8< zB5<9x-xC2Te{WOJkET;V3F*Q$LQlnvWSsYS+W<@%}|6@!{GK z=Tz7NgoI7X=-VDp3uOB3zjk+V6ZwSgQ^qprwf+pr1oop-`j(`c_tcSpau zA2|#svR6SP1g3cS1Cf08NQ4a}{Qj{SqJ!Ur`fqFY>^Ubjf68u2XFi%zwj$l;xj2a( zoqjXY{*XA+Vf>T-=VWhud$)vf)aqy-NytFOx@Arr=2KO3Kp&vhW%bXmxwu{8>ujGn zFw{ig0@V)H3_ao;l(mmOeE?H5(5lXX2mgh0M6>my6HGO)0fP}Pc32JAc4Gy1i9!)=Oh^c zN1O)|(n}LVAn{%CD>Pi>jAPLUxIZKS;qWKjWtY~QB6^FP9cHI8g4=??#*%m}yYaj0 zKY_nwxAcABV=%qu%@ve%4>@gzCq9avJIyQzejeef@tM}1wO!RJ2{|uyob8R=vz(oYx<&2rWx~IIdEBy=X3Hu>5{2ou3Sa$Z3Bn_-Xj_`a$V` z$=)x%*l`>|OWK105FVgJ{iN0bIX-J!By0Gu{#BiN!giemG44Gz(#Lc|;f2A!)K)%2 zYX3QZNb%%KB0Duonj6r^^@yyvrGfk;|8@@pAouY5JHlZpSu{d#Jr1hhnhr>(J$?l= z?K)J0(mQx*O`05J71WQ#4&MPlZ5rN=Xtulnu22r*c5R<5As zakQA77VKTU)X5j9~Ly%kBH&)D2<;Bn#Z5Y88p6bPGwgK8}X=@GHzeP<&j{gK`(&o zxnZ>y6Rj-BJwgh#N(FUv&M2=Fk`c~$69Da%F!uW z!eO(Q*P0&sg?)nD4{=pxm$#mB@wXoUX)4VGA(m{D^7C_OUV54mn1B@V3IXPr6SL%G z;?AcZ z)oJcONc0&)0jH=SRTc6OY%$>eU7x_G3=nzXBNMI-)RgoUU>DWGAQNh6vjQ%{;@FB4 zjTE1YdEYwP6J^M~T4W{rU(c&3jpQ8{oT;StAnFY?DZH+KwEyW%Oybhtig`6vY(lOE z9fNl;lFrUC|ZQxx|#B z0Pb=}wm)@Cl2YTZ76=WS)TLqnDOnpa|6>D2xOV3(5E}?os3go(&USqJU!FGA6Xef| zCKL0ZsrBzaB1*qr^wcRPXqWkQgaEis`XA{Q9GR5G=@h%){<<5>zSzq}MNh{(7kTs3 zp1CtOdHdfoX#aj4KLEBvT?GF>ZUBj*SZ|?KuBiN>^w-t0$yc^{J$ z|IOSLZT3UNzd`!Yzio@v#(%%EqP$2o%SDxgP@Dz_>wXdB>n^{Spv0|7`O{xP$j>h@ zdE~5lc{w1f@Hp+)ZbDBf%A0z3x+fb|_{BQpg3Lkq86GFr}oVXd=I~>YFiAl zA>i3{`27vCdhkF&$h8Y z?cQ94l*c2;Y}L$j&Ti-jl3VWJXx9v8``x<0Sizp!!WD?PHTiC!t&z7S#@?9-C8_LV z-Md298?>D$otDe#(zAi)4}@0iwMTAvnU*?QZI`MkEWFM4j*pF)3~69Eyb1xCC!Y>& zShr;#0MA!{6`#X{A@iO^i4G`N$rA~*(4#bvbvC^cQogULQS4CSw&^CSF*-0-rWfYo zC}`%t9clA^IbF)%)4+;(3a3A=_adWijN%KjtYS!ir7!$Y^I{wh@O#huL_4@%qZ3Db zqkB+R3Hb>S4M%6mQ;t6E?KU2hbAXN-x{RIx#L}w50i6f>SyO^|_^qOl2{>;5*&iI= zVRvWv%I`I0Y!pSd4kzyq+aLj}#L9VPR*@@j?&M!_=UX-0QRFuX8u=zAWq*Di)5f8u zg#1qsPFY0-e0e!MIy~%D>N8m!`W9&j6(O!gi1BQ`U%i-9H96cFJZ6 z_NEDw(K=po;JXCrw%YrT5VP4lM)vbPLY35$=FIpZU0rbx{o`plzq;rhob*$#Ccbp~ z`7@HX0~P9Eb0@llGrTdJ095Pg-P$%VIz~mwhlhT~>)zC2ILc*ks3NQxII9Q3Ivh*= zJ^T$5s&I1sbVH8Bz|I-l{`l@2P#Po-#LL@~-rakuB-_yx%?PK*b5>dpBc-pj6`2fc zkI{16QxsbgG(Lu4?4ONyer#J{w_y6_!6TLx`B%H>?{q&px`yP%=t56Fb&Lk&a2-tI zf177}l7hVHBoWxVdyXGJ`tz6guRpY>M+t^VSo91JW68)99;7t=2cHH01JFvED#z{< zmf@n?3GuI_(+v!N6l5ZG@`d)ygB5gnvX8AV!~I}97HgR(R-MawkhO~als>Z8KZFop zzWVYH>@2PVq88wZ4KF5WoVAe}E-+$$k7P90cWtWa#N+#f%EVOO=yk6MV(gsEr)0U| zKj6V__3;MB$y&B$0wzk5bd{FnwA}fm3+5XDWkGS`L+)pP$=6?Ao>3gT-GyLv~YX4i9~pZQMFv*JPki%C%bx zf09G_BsKKT1YnMeMx$|iu)scicJOwm+O?dn(${BFb3;>`XgsVf(;bF{R{4sfKlPAM zGvi!r_@5@Q$giQ{cCyK7kMm%mW{<;KjpH|VqbsuYe5F5xz9>meO)ak5qOKS_U1eZH1rM}VMyuoN9&OWarg9^M`x;8R>?1?6`sj@&ukVj~ z?1UnDW4$icn&L(38@h~Q;c$gYem$>_Td^jtFS0?FT_sUMnJxPa5T9~u5#zo{3bhC5EU)7N10S?0Y;m96Ta~26aJ#}Ne~J7i#GQ4o z6OB1pK>7S|OLNoXklqF(wS0*(d_%4Kfzvvh4{#gVoxC^Gu@CLt5fV)QyKq83z{qYS zE%djmuI1bYpchDcP@3ycn(mK^(}@{sy)Us`t$zO;f@(BgUv@WZ^)>m0{md7r380Zn z!y+Y>@9+=2f5eK8F{6Wu!~=kP7Gnj{P2ISJ;OnDGvmN4G<5B+Cn@3&#${_3-Sevg# zeRCZc@yQS&Z4Mjnl+VAscgYHv_^L_o({5S4N0?WAPhJtN7b4{uiQ*H}!|W7I43IG$_|wG^rj6cby7&+eM|X+XV3oTE85>{Z=QNPfff_?<*5Qpxh2<+ z*W1vW)5`PpMwDPW`BvV09^a)p*fKA2!KVIXPOp@cHn0#biLbxNzY0L3-(2Fe-LFDw zTKmnr6xKs4VU^In6$FzC|D>EMD;451+raz189&UeyVz|XFw5++RmOqt5C){2WSql! zt7K(WMTj9j;$QS%<$1HBaLLTeS#e?J!{g&whg~BRjJn9GGcrGf#E^(C^mJ}hGV?H= zW_$j<^R3d>8yCdC&iNq?(C|cxA26GR)=RfU?05|_pRwHB*e3>7+1lO%o%&&zqX=xX;x zoo$5Le5Y{3Sq3dJ?Hnw|RF0r7Z~i0B9FGj^_B&eubD`-T7-6rMsc^d34f^H=e)(m| z2|C^!?ONoZ5B;gInK-Gi{-Gfqhi?p%bP_dQBrvapYqaaA#%G0sBAWL9nqRP$ z1*V}HEoYNMI@6GF!0CZ&be$|7U~=iQnQ&j$ys=s6nVg=yo3j$@CRcDJ0c1B3` z;(y-OoA(}`;0vTNH}mm7R@nUntz?p}v|!ZPArvZzJF!rw9Dg7kg^Vozdt#COby#Ti zIaJgN*Z#qoy~@EMzrbRlx+ah^b+Xt}MSV0)G)#c6RlJ0g9T~}dvI|br-uNkl^6vie zjLy?Mdt;?|+T&ya=m!uY3)>!P76>&wugFmGT&s*4QY<0->4pBx{==DHNO_CZrmJ17 zISz&XZp!Yabd1fl=3@cZvfdkOvRO~a64AmiPd<}Y$Y9hXG1zV+bMKDKuaX8d{MYYh zd#L2ebh~#~LQcR5FK+jepwizCHJMju)5&dowC+{d!KBH)jVU#;`BHhH zI~U2&$;o93ixmR8;59jYd&XD_D&xundNK_W|6F2$i{tG7Z?7+`#nB`M#_SvL3 zAhKlzq?VuHy&l06R_M2-Lc_uvvJ=|WY_G2iLpX#foHm3IW^6(#k7Wr&8uG71qe8h~ z&n^Li6VkBJq`{3r6Y9s$O55d=RVvq;RO+#R#MFduMeb2E>gbh|c`fln>Kvhe2#47& z_PzZcx>e!2fn*I63_WSauDdY{;rpSwOCF@*`)B(GikqCMD~M|pz|5dU z4iB-nw?F`t(L0=uV3MY~c%d3!t*`}S>Pit5PZx-Wk+Fow@iT7QU;+eM0pc);25+8~ z*bl%^dOpBbNmI3lm!-H&$VT*pc1@bkpvyU1L8bL5=TqvnnY$NyV#Pv~Ok3je2iwi1 ztVC60LYd(6qK_F`9j??YNOaDG7v%cIMDyN@J;^VOS`DtSLGta+rxS7Ad>W4Xn%>0| z-1+xqs3*A=FV%XWI(TAKz&t7|g3*Ote8L@-kJE0`^oqdQQ$V7Pv_xs%*mQbdFs6`e*e@}3ZDJFf56Nh3bhk_n42B=}v8=>|+1Tu?~~K%YWDa3g#5KVy`4 zgttVYG#)}kZaXX|@jv+m4$P4G3E7NP`4Sm*mFYlF2VQoQttM{lDJ<#yL?Ig8na)Vk z>%&@BpM-}SJ379xCBfi&-!rw0Qzdz6)8PZ-1e=3VwX>*S%x4absh8nu6luw%vHqa&UzwSl4L%= zuxRDy&FT2~(vkpu&z~zTZZ$4Pv(lr>p_r8J5Qgr-a+A~VsFu~wYdsU1oY#-U1C8)b z>UtA*DtyU_cv6H=uTT$AHvYMV;c6Q12mvz$yk{Bb={~fIo#<0fzdp!XQvc!|DYaE^ z-Ua^J_2>$2Jw2}`LX`E}1`P<9r!hv>^pNvMLXB>Ha}&5Y*q_WhOW|=U7%BS<8FS~o zZ3fR0a64Vrf;5KFv#HstWj$U#4Hj+cYcLuI`$7IXJg?4 zz4_4BcjE{KN*yi+o*bOaUSr6zwl5L*K17mSUyR2m**MqK0t4xv;otYC+yA)cLz{XY z(E0SPHs_b zCb&!0EEc)0%txnwQ|;)kdY=r*njp+Uo#J!EL>?t#kvPQ~p|=XZO!A0nr#k55ISW46 zGLE@?#pkUhldNZ83h1--U4C3AVR&-??2rghe8rnD;_Gg{2lsM0Iow!%J$k_9Jms}~ z9HXygr1aw0d(>KsPsTAt_&@EZ=k2gpQYsNtG;1_Dnds0dv$fHQ>V$TlfL->=CN%DATB~l;f_JWb9J3?f$Be^6D6O zEd8v5vhBy*WkUG)icsi_kL^dtXNp|oofjq1ZWOd?y8SXq0^Lw55X+8>6frW-#@;75)M0a|twOUhIE z*x1`9gEhYnCasUQAkWhWDk?hq?^7==HsRrxy`7L_;aI1+K-tXJ?yiK0JiONKMXBt| zc8c7B%G0-v&pw)9vIlcRYr6Bh=Agb@F==UJeSQ7>-@n0-ZQ&mX2nJW%yf8mK9hOuq z`a*X12X;>1oGdL>$3c`Wz?fADM~rmP8cGYl2N!p0`Am-?UN zGnvFc{MoV7)$U(cU_%wuw6cbLB1{JTGMHY0VWV*=U#NK`#CHkD zhJ&i7h<7jwFwsjNYl**f$PmD`n$JlXV{3j)>6v;(5mxac>RlMkHSl>4ww9A+i%IK28UYLFpz98zvyTO;%p%sFm^rP%F^g6x} z3OMn?QYCt{=LV7K(`slO&N=HV{ zx5AAGqQ%(OR*L;!AKH?}0WF0JXiCr=MQFfRO8I>70)k4TreV**0)ZZQS3f9dd)G8R zBOZki%kAY^E#3I$^4YCpUVp(2Y}K_!N+2mQ1y?;vSWD2s;`JBa(KL{w!V{H1Fg4Wh zOX;p@L7NZ|SRfEQ-C~7BEGse6WFEIE4}P8#ny9?RcyO}`r>0H1W833)Ge%j8+PUcb z(ZalA5og1pl5i@B=z@*)?2p_71KSR0%P(2AA66R(9|T?icKQg5-K&HA1x9L8@0YB9Zj-TD zcGSSe`gdQ=s_y!n%t5x87s~!=&iRCZZ2`y_|0^MfrSx-WbE!^Cs>~z7_m%x=J48Lc zpsoFFF#0!W7)PjA-`ddqJT~D2PW3>h zG%KaO zjvo`#pV&t9aCJa!2U-djS6 z4kpZjF?%N=Ad3yx0zL{~COJCZW~)W5rm$+u8GhyQit?xn35{4VSVzhYzF`JDybQd@ zPHx5Lxp7?{pb_LiSB}yT=-X(B*obOptTOLsK1$2tb`t4-j*i%#tLPx_#_!6zb>2-= z02sP6bK?rxBIyf4NWbn`zsvCN3gEw(8|ii2rwU$e$g}%+I}e9#ts}9@)U*ayvA(-3 zU`}r3`xM;0Cv!_wVT#>RQJxJphyyA<{Fco9``=^&1eB2v?&`YlNY|jTJCGgpM7Q$u zsl0uB21iD=A?oPOj@Rc3#-m1&K=`M-t?1VK-8Ack>RwkH0xsM1(LUpg?Ll_C)$}jz zcU_5=y83N>kwi<2_yV5n5D^t8WEH&G{l!i~Oow^NDx|Qe=7#|2K%-+Y&;WC;nZ0eB z4Qr2^j6OiG$hy%xamUH0YHMMqb0{NZj0&J)*wguGXa8X%7gYQ0^D>A-pT_`Va7BPA z_nKz|!DaiJM=NDpU|^sTL)k)8?Y!`1<0um6@uyguB5Z^l_e;ydB*0mtU?;p7z-)UF zJR;nHk*bfWfIGUx8Pjw z7;nhUu3vKmB>Z=$5<8!z*5wRw$t!^2CIxHCcL8VBfyRTfiA|nxAa6MKOc+G_#4~|o zE0bRSp!j4`p7(WXxbnt_2#@4{H1SEt@MS<`iui6k|d*USzzSn zuYLZ+v%&7R1M<_u?Vcqz-?l#!ahA&^<4|dZRr4y?cnYr=+6}gmEqOjx6)-5YzIV$lYq&?u7mQrcrP61itpqr{2IL}j#`W)QPasfYZt=JRNm@g3Iw+TFL zkr4}c{!%MZ2ZQ^`YCw||>ZO`SL}LQC%c5zVHtshI7B(F+5U#EFWa3ZV4}?i-XN6h5 zxsa$uQ~iPcLmGebTdKzjAKysVDIKreE zK*6wH4-0z0_^hg5uznM=dI}0dicwK_42b*2;3Jd8n&krJf7P4W$4WZcHvhOw;?P?a zXatsRcgz@A)yFZq8sNzyNP^=GS!F!=jQ;ij)(Y#;o}4jl7_Nk!s^B z7@eo2W}rC+N@>hLfuSGxCCc@fLh zWiFbwhy{E|WhQMs=gcO#frp7EJ(UPg5|5N(F%p5V(uu7|I!od39i4{6wI1owKYy=R zcMsR0v?3A?Z%gBr`Vrj6W|rTaEvhRBN4DW7Soq$CtJoArU4mBaTl_ z#%2n5T|neUiVe1UMC_~eHbiSawBMqcK@;Sst(WqrZMXVMXAsRUP_>|*E(E-|O*EEz zcDy{$Lk?4-=4~#0uKm9C>qU@;kG=faUtRBqqdcAG2R+qoq-6dTQUmEE7{lJo4e3se zDOe4tBd?ngHR#MZfzP<`Fly8_8l5HXFfufMIu-68zZ_l0ef$L%No}ItS*pp3*oys0 zOP4BMtFzKtn>?)?2Uy~IA|qGe?D;Md#-EXNtaBh*ZP`1U+c-dJ3T8|+p>{2%BdXFE~8hYqws!QKj(H5UlpmTU?xD}dR;g!#PKLcM~5og54V#XyU zX|S@jCjKbuYK+BZCX8$wQ!i<~&*p4viKWqu6sNPdfsESj77vTHYv>`v zP|Z<4$lw%!ko~5Su|-V4S=gH7LNKmjuRc3xbDJ%3HovaVE*=<<@I91iDnjt~N`?WM z7o8{Z@Mi&OU#6)9myv|EgljL4lLUSV;gJM!!_?MQ!3)oTK5Dke3gQ^yt&^n&Hm`ar z;x33N)56PZC&l7N%U{U(BsOVlNc>DGmb&U~2EM?3uWPF*(qLd|Yb(Ee2CsNLP1mpT z=v>xF`UFb`W|vx$siAi$njNB>81b9tgP85vz!kR`yq=gaDO3V) zHlUiJ2ft-EC3Xo4xX>Lbve)*S=;_raW?Ycq5f8z8l(#?fgQb#vot>SDT#F@|9FON2 zPqQ`*J;fSTQA5SFm-=93)KUABukiiJ+e9Yqe8K2q>&?en-yZzcOBqja248 zxmh_zbnq7_g21xb7iHe?=C#vk0n}(lfSrJ${)pYjM+!x6wx40jvA>0@ zLKZ*%08JcO+gp4!_DVpVI}XQ#+3e;EQ9HzH&ZF$x1K|D2nL2ev`l`lnkJ?Qeb1qnI z)DSyCbiL{ifd@-D;X%d1DWU^G#3R14$J9JsUMr7& zqU?QeX0qDZt+Q&M>lAvX-`YsSN<i~8gA}~K z-vmb3#(oLQz(p79I{nH5|8$b{>?L43^iKGQLv~pFq2k-$X!I2ggjPB;Bx_%$R>9M$ z|JoB+RE}kDbn6ZlsoJipbNAvnFp?~^p7xOl-fkqAPDe&XZGdHGG&D3JVPS}12LX}= zGtU{>#&C}4UMh-QABT0u3ij-R6ENR$gr<}m6 z#ONK^E@W{$Q3mpXGvX@0Wd;$JdB+PUERE)$k394pDt;w?Do~X_6LBjO;(0vhZ2#8NcDXqjALx?&X=E(m!7R zI7F!19W)Q|46_79mp1JDF_n0(t0DKF=K8GTaz)K)hAm~b_6 zDPt!Hc+?u(O&$M|KZ)kAw$k}408u&zCOoB?A?w0?tNnOrF8JtZ8ZryxC5T}86bl}`j!E0iL$>abrs}O z{bK!xxbZ$y=6OQ&U#_=oTO=|oi<@Hrd8hToWt%oVYZ?{OWf?lBZYxecBX;77jZLt; zQ1Z)5*=T%scb*>1aih@}v@=n);-O`aSaMC~m>E7NFD!3+?S=Kw7k7Zws6MN{H1$3| z6Lqp^{J^Mwo3i=J*{+|fzxN{a3VOBT6y4Nl@`7};ey!xBQYpr|Z0MI4IPqS_8kZ|k zIUyAM%9^wssMnz9^o>?|ecX17Yu|E0D*bWhcoI%?h?&`L2h53|I}HpDx1o-k8w+=g!z`iPeY^p_S6!C@MDbqa@2Lj0`2WfXRGTZvv^v`(E+hutJQoBJr-!r) z9Z1}lG5XFMhZYl~X+>wwXB*Y3lzOhEH=M_GniK%JYrC6+_bej6$8S|hJn*Wl1+ptJ zt@$qG_pBOp`h9V17G}e+A7nC!5fsTA>`BSLErXf7?nSsEX*PmRX1>w6XQt3XmB%u) z6LQ2rDtxJ$@mYc{XM#x9Il?D@V8M?Xo+2^Uf*DS6V1^6|o;; z>VaOjFQ-yo;H#C(-=^y4q4bo2-ub!tpL;~EcZ7Q$UhBI@KeESGsM&Yf*;=Lwc2uCD zWzyIwvXvdZewGhRc8rxbp8bd%c%}2}R54Jkpf*}^{u4#CnxvGt3jaIt3ad_?_onx7 z)7g$gH|5E|t&a8Cx<3-ttzqA`$$a?r<|#0A5;@OIpY8t&AMZhr{p#lfx*8*y&&uLwUw8y?Ykj(H4OjanuRK@fwyNg;YJXFNw_GJ4;y0BO&yD%~*3D*PB zlib8DT0!Hv>OLhYeBw-tidONni-TViximPH76fqvrYHO>^rBfan*9{7@q1X7MHfF- z_O*t9o^SnnOW#UHY`y)J{-Ll2NShN9q36%*kO#?aNNv(IT4*Z&qRC@CxZB?*ZKxn5 z$K}nwcB`D$ZqR_zi1f5nm0Zg-#f3+@E%*g)7oDXcFi!ZS%Su2r~Uy zmbA|>_7-%*D3t(r2`y9kg`7leQ#8^!YxVUHA{Tf`;-KJoC$_|% z&y4dfhv7-m2I^3>7!(^eZ?J}jG|tVak|&cj!Dl|5U2eLe>+)dt$kIl zttg{_EZ|PjRmAl(+s9nT6|PQh3TB}`E=>jAWOC19x`s~So8Cr#-umX+@W79y9#j7D zLfyA@b_>zdtKw1?h`=1(cDSNk|LSU2a(DtS?;Y4oK*OAMqU0@);Kk2^n70CT4r~ii z&9NdNhoQq_q~Czk@o`)trcZbIZtse=6>;h7pn%t)qPnf)5aeDC4p)OK8Aq{l+k#VA z-agB@+A|0i7PA&$(VUj!E?EgzBoiU7wU9P*V*j?hoJ+nk0Qhqh28D3Fdi`BkL1DB+ zuMIX5&4z!x(OMgo|Hm-1sD9kYr|!^!Dvj`+#ZXaovXosuUW>^&=T#o|D%pYs^s;Z_ zoc7_>Wta#E2vm>rxl*?nyWcK5FPPCVD$fCh$4& zVtZ3rsOu{l#Q;dOw&Yql-*6;jyFfk4PceqF-d7OgrkJd@XG3d z#_#k&WxU}p$2Tj}5ihLhFZYzb81#j*5~HHF|Dvv_=csFhcmEgb7Ko6KK(w43k&(fD zXiCxn63EB-J<*AF<9N#N{Z?yLkArDmJ%RJ5fDI6_}HMV~T7 znJ!1jCo@fhRR^3}Ej|y30Fw3akH_@eW0K=|btS*ffz$;%5;3PPB4!;qsuo)o_H6tL zCw0;Xc#K!nS0zkde7R)=0z8b=lUd}3{*M=XGtq~UiE%cgt2_1uCq)YZ3nA`?3;DJl z@by%=ClC&TLuOaOf2cU0oLr{W-m^ovYtZiXC z-<>TT=!Z$mvAmN+GnD5a&$H%u>3-asr6)PxM7CBIAlb0cU*YA1Pn(;dtY=!4)4G+;-6R(ged9)lJSbvIc$U# zxw&y3G@&6>+Bvd~Qd%-lM=$g?(qh&Yvm)!-KO}^57`77Yhx8HCHVMvrIcGpO;%=JE zOo0(FC(9Pi>7DB*vMwXaiKT61LrO0HnxZ#QogUEl1-E3dh`~a~^yjp9H<&WKC;g82 zOvaRDv7@8=H)sH!%k5rkKNl11{Vm8KM&RT=T^5CLmwyoAY%8edND#@01iE8T4>!gR z^b_$v!dPF_>H5$^H*$3QFOC=1nEz$n*X0FV%k^( zJCgY?uGVVU32|~s+7z8smP|wS>0K<}kxIXEbqoxoQvw9j%o+Vz>!-X;caCF0mBV)y z^x;<~OL1FN$*e_ecT|dNtK{PQnJ6tc8OMWvt2M2MXks#i0}B)({=P>5sfckI$iP9{ z>z~AMH@RM385j7K=QX7B&GQt6xOT5&wz3|}*@S0-yJXu$hy6CGgqN}+R^aJsa4ieG zB!$f*KpShhCPH`oWV2B72_`f){;O%7CK>GiE*qwP1C|Y2U}6vd4o3OGGBw$3j{%y# z@pZ!V$q_KM+j4cVPMvw0<9&Z*;^dGg`6TAxNI~MRoqRPwCHOEqgxkPl*7errvt+tO8$qsx%HUyGZ6%_Kw&}=<-a?03Wg6-RSp$O;tg0#S zlfQ~y$cGMu2vwkaAYv+JZ9~ni%F6{~`+51PYX_n}~ zW8KWsXI%;?@HVh)vZH5e4IbzN;w)j5--8#zuQy|FnV zvA)1J*4jc+!sTo|v~kE6BRYwRmioHtu$Lms5wRZa5Zv78T*&_bnBVC3=djd6`|a#| z5snpUSNQy(9z3yqEXYD*bVK^iMZOLO8(A#c5VmTYonB(>+o%!7i_`11zXvnS+*Q?5 zp5K}F=X-7(jb|%A;?U-d=@3^B@C6ZkBh~wCdGJ2e_u($y!yCuzFP;_&b)jFlJ;>ZMs3XyWniY4Woc)K@tToQM_g@+ zt_!4`K6;t#zPXucmpFNB&;N`T9g6Xa?hj=%25J3B@GLBWm+q6H@;eY?^h^%{}d-&xQQ#52Q1hL(dXYg*djCY-MqGAs3)l@`-R(iR=? zEbjBjh!rh*zF8s@>#wT-w(U0z7T(dcE!jfX;Q;T;yne1&^vVhN_5FRO8SeMPna;1u?=Fk}&CkjBuP}ht zYV}Sv{F&0_TE^ez-4{GSk%#j|3lCuS$0Y188!1Gg}@T(8az- z*ZtCP^XC^r9hj3%L6R#89KtETaRU+T-z=-E)1R}_OXa@RjpIkEm9GISPusm^WAU0E zN9>EX<4=YmTotSjlPR-jv0O1xghHQJ(8;ou_6Piq4DWf}*}?~QVL78R)o1KTS6WkQ z7FA<1A|%rX3=EbxbyN(;@?{9!(EtCFMWwOIGqiQ?>qI_y+F@(!o<~ zA-?!us<4K{L~$%f>t1IUS|h2Tx}W~Jm>?%*RC>P=n^MQ8r3GFhuTOsPrMpw?QTGAa z>c`dCIde6~xUGLyWa46DW_wUcFY%|Db!C7yXR@xYE(h+x)YRE3nRTO4hb?bGgUtPk zHNyC@N}mOVh5d7Ma(~plrB6{)s$e6?AK}JB%dZ!#A8FL>wd!~M+6!RRWbv)l?I<%H ztzQyP_Zo~ZRQYnK;LKHiL~u>5fAO+J_%_j-J%QQj#St$p&e`r#HA8S?%EmtFgb6L+ zbkXDZo5gIl$|Kd4NEF5%Msr}oX3-b1xKI4777!9qp6v!xMqah6^}VvQB{ zU|nyd)F?}ZX;+=quK?$XiC1~KpEYoa1GHS9fU8j$nF#vh-}UDkh^TK9QR88i#YEv? zVIl^L2sPMHiW&NTah3epC7yDTfdOI zl)H5K)9%d6Dpao5P+qwA=d$$9!K%5>p=pt~!5o3xW-(WAQl{Q%X!K#cSp`S6DNTbw z5TO5$OYm^`a^r)?xYsxhb$2fdENEZHnj>qb6($Rak0 z$^Z|(Pnv;?LHGA8WLixu?MN!*Esoj{2 z`*^;%A;maha$q>^wZdRnuSPA2zBqG0+~HH1nqcYZ0BOr zU^XorC7elb4KdR434xlUNkoLUh1iI_>s(GZVr)gjl5ei|@|liHwJ5h#aKs2E;fT@g zu;YEjmoFi$A1v;n{m89e!R@ycHAU|V`$9SzAv>bv2nL~hGG#}XeyDoHcyIxkev7aO zVlKPyaWUbF4dFcd(KUUQ?Oy?Y&F<`wu$D>VsO7?*IoJLaHPX2k8qRo^ocWLbqw0+(RjojhZb5 zs;|oFi8+A_{uG@9Il{_ao!xl`3kOCq4`p+7#o6_(Hn4ROc=epJ@{rjLa zY2U0LL+O-C$UD3h;16!3TQ-L@6tgV#=|T;|u*$pAtdx^???hBW9<#eP_Vg&Q;y%gd zWQ#iB#sR6Tt2@lC5zyIw;2MIGpq*M#%P)~rC*aYDL!;lYD=CZOA_~F55{C|cMdU>P z@&&Pj7ZjGLs^55WV-E+EPHt1QJ!}QGC;&Nz$vjWS!mWmqQ+{zO>Y5|SAD80p!r^BN z`BuJlIRZu0$Vrb3D+$$zgdTJBv-ls80iqsspv+FR$}5O$)4|f@U=)Se$NtBm2w5e) zqQiY`?)e|0??4k$1$9PB&pcrn*PQ{9Qa} z1t?h(4Tp>+r2oB8P`p?lh@&l@htEz@r=k6K__*icrKBvkVv!L!6WxQJ#g_q&@YZzX zSz0o%2Bgo5DiU4y3!{&gsDXCEA)OdvNWgja2vJ;CDX2z0;#WW}C&;Q;h5=`Qc?1rG z{sj;Bd7W5(j^GEs?>}?Fac_!2JG(1Ro+9|?f5Rl;Hav- z_-FiIP53fU@^t#j(jfd59woAP7P!8su6OofbTcHx^){y6d4Wc(4fSX&j{*ooFgr~~nWCD16tjZ- zL%w1qegNvP(19>${k`5AmMVp_`{$j z7%-JAY%UD)g#OL|_gNuRB>r=KI5cuQ_b1))CYbM0R6;wSZ-$e39kWe{X2b;e#KFme zZUTTnEv^Ro&p$xj^$gTOOcvkeXfwxWv66y%G&riN&wDIElVo1UgnKCl<=>Z87cug4 za*`mLkS+UHPdLu{tD=`vi3L}utno%&K}gijTf=in>FGkS9i8*B@x*@x3#;?&_YFNi z{r_59wkYgt(Cra5fF7l^H;}AKq=Iw+Z9c`_aDMRv=tGW|rgGV$4fM|@tRAC`2mZ{& zlJJajf)7b(k9`w*0pWXYXwxAB*V#hdY+AcBix;w^6=kE{H0VuTmRHh^n$g(Pi!4CITck5G^3ZQP)Wt!YgvH^A;GH`|F?+(|Hqkv zYUiKPHDf{!s)x>M#`-ovV4bsoa_oxnxMM05&<@WI4VpTlEnJkaBJq-*MKNv8zo?1{ zx0eqtcRxRs^g|>F#+ytpK`&h^0P*8QyZQ%Vr8i+@l`?_ca^c8b4S*p(=sv3@GuN*p>#TRVDbd z^&g`KF&y(5nK3oPh8$e(fNL`5{y@upPMBuYa#>RliIAR{P0N(W>@pKqK^tsTIK zj@G~S2@Y8R&ONZeNBr6bkuxST^6fQE<&C2vH~mYkvOkKWsPV8|-eJu7TlaS*DO zF6~vdFyXT0L6@AU`<=zEf6Izb&+r0tQ*nQ#4;?$_xf6%MMRx4W|3DxJJi^F7E~|w1 z1}cT|SYe5IhHN5$(*o|!c5{1CX+Z6R`qXB7-*C zj-_KVY3pA+75C4428zrd(9xx8DDkBD8~6eP@2r`pJp=(=@F}8Y8PfL*+Kl+Q z{R<8m=- zzo{D4D4|v|;(q8`JAlL}{hf#D8qLMdFF2}ZPO-=h!VUb-zcuWbmo+QN1e|5CaBu=A zV4Bx<+JutZsYvkbFi7158;&6#ixT#Dt8M*Xqw(YFfsm&=k9!fgQQmm?X*JmaG9IrG(p$6fB%p1-Et8X{*Pk?g+IrZ3BkzqhSMm-)(Nh!@>W*@0qyli zmYEE{zL{&g7)xunRVis|3A6+Mz;?Utvxi|O1xGO}-5*D>z}VT>+y@NFji=2S1b?r=x(ave?nSrl7M;*Z}! zQEZe~4nL#BUco;oQM_`nVey|IQgY%pAavkB9+36bouTwY{p{+$(-pILPrUnjbn2K$1Ft)IV^b^oF+GmKz~a+xkq1$K?WfyX_=l1 z@=~R*KCsU9a;6$jwbI-ycoj)=^#V6BN`}%63u0 zQDF&;y!repBV1dOv$w>4y3;CFGCl`ZR_z|PV1G<$-lpzFwW&ajRic|wX?3-m;ayob zS4k^U;fg70{r+&@)@Mm>kWW^~y$l+Y~MHl3zTpp&fD|KrqS z$4LK%e9zd6NFRqtNg|2`0cibDA)1hb3lnH51SP_N^~4v!DbO#Vq6O*%kkU&(Cf70% zq|euPuJ(0!SzmXV)ULM(ANqIcFfMu$_A;-e_Lk-#2Bo&r#a1xV3R zkCyR$wDU5Zb9im|(qg;&olY*Nb)qgdIYZ!p_A{HB62_jg?z!YfJBl-=a1S$8up`D7 z%2C74q+)L4+cuqxokc9*cm+DtDN;#mP3)+W%ER{8vel)6g*SFt;~9IhY_;-{lw`zQ zaK_AVXFSDq*8c9c;YDwt17I+Nj*CxVe0Ww~x&PYXt~<8N(}ribGJLkh1%C^uR~~p@ zRyGbdBBFKbA-I}hwNF#~=3rz71~1rw;0yF-umjUA{+Bz9h)LeGr}`hWZ^1rHeOcA` z6I|H-1QWH(?AoBspXfv7j3r3aZ$Cx-7mkBlASWK>D`8}yV~wlD2@>6FO8ONAKPIP$ zfRjdCKFXDH)J&MoUHu*PnXGPDwYHLup1({i!YP|u0|UL1mg8ctb*X*;}Qpc-PereN=C06u=K1-uD6`=c(vrVeJ*>+aYB27cvTaVvRi(lswQUF zv_Ru~q5d`k4y0_Rupj=obT@s`H{R~_*05odQjqa$&L$0h*ZZp}YQN^Y8Ht>9IIE_r z?xLMX1oE0=Bd*1LkL#(2j||oo{zs*wB67i*LVZ3d+%Wv-Kw(HGbY2inFK9mEhsX0? zKlN`^mX)6rB?&^p-}vc!EVK&=$I2`K??>Xo)~}({cd4uz^AHFP?ip*@rV^nXtPS=8 zvo`1^PjzTR>SYpQjmO_3MMoK!JEOeHv)+c&<=32y<=0B+K)rdL>Jk zkAvxfm5)N(4H}?7yvjdN%$1D!^V5G($~XksqZt(WA|wEi)Fx1dir#%4iPQZPqV7?-(N@n1(r|IPs)B`+B;Y|;C8KXcLphTh?!e7U)5z%ex5dRpBD;Do zdce=NmMmxex*g&H=^!m{vW46amWH~J>T7?=g5VdOSjl`D)P^F3Va=?QczJUD5p0w= zs}CMEQ8GFFQj2PEPz>lGWu_ReLYl`cR_!15XPgONeDv^w$U~K|8n`&M@ra6BzP!L2 zcTO+F;dFC0=4l>9TNEV6-6Y7&t>C@D0}`;Fr|$6@j4uSI>CPmGWKLe5O?@E>2Y>tO z^}h?sb$s)tEA{_>2>KKs|GuDZgHls)e3Q zR#8!()2Js*4XiVd!cjUX_kO?f8qUL5#IP$Up3iyfU}$ud9D3D&Y@rN5Bc3e$)3!HT z1MxtBoWIw6L|Wy?8=ffiru8dIg`2SSrtm9+D8M*K30?6)n0|>6b6)*gW`(doGE-Ly z>v%By%4awr_ZX1$eA07Sh@RTrCR+j|8PWo^N&)!;Gn?ZqYz8TZ@G1t^Mt(?Qaj$;D z?ofm%z>TAJ*cVkNeFabvPKt`gLLo<|Yn=JjN74{;s2{K=%#fx*32Ucm*Pz$&nQF{ODtG;yLn__LI;Nd)-!uQ6oJtko=TQn2b z<7@z+Wpn)9Q#v;{r@J>RX*-^rd0v4GgG97T+=kV`2SFr4(m{dphBpXDHfargV=;Ua z84bBAgd}w&VkVSN-hx#k(rAZ{Q7)<+W`4>j?vMr|P$5WRL}MNNwoo=|Pc#muq_F-} zx}!nWL484$u&th3&P0B+kPGEbjy1-G6d>P9G){(9Ua3VaI4^Rlkpny6Ax6c}cHinZ z9isA6aZGV#5zb6v#{~(?&B#rh^@qcv$by}`n9$X@$dc=y(sx9=hQMJ-9o z@JiXx^KB6P@A@xKJS38p%-eXr!_9=CoA<@sBXm^n9XHarh&V4!tusL~c1o@bk%9FU z)!H>eGbjtVRW&>Xj=jMBp=$=+nS1Dfr!CK!0ZYd1JN|FV8x>(HnR~7@VD|cmb$u{$ z-GFbeYnx$6Ig^8&Z?#3d%Flh!Z(+N`YsHo&JJx)e?xz5+L+(SO4!0LH17^Aw;Ias}~&m zI+mErYWOpq$_;M=t}>+wE}?*%%|H|pNB%ThQ8u^5u)P!^L+8OF1(d}VbCb+RMlpt= zV91d=S=I8q#dxxUVt`_o=9}M=s{QQ#3_w|-P}k2x$Tx(Fp<+iszkE`6Sj2_8`ee2e z;sjNqN>=5a?w2&G$^UKFdzyoST(ZBqP7G&_R(Ej9e>Ke@mG#Re3Z7jV{0SL^sXL@U zspvs%gp-C-cmZXRStWfW4I_Q=LDfN6dxRlBu)b_O`N)MoR=seYdV`uY|Ltt^_=q5M z$JkzEVNs&X`&d=LpBcFPjJ$uWX2`lNCco>FFOX{IjQ_?Wz?;7VDapE+NBvh|YO|%n z)$y7(6Cr=enDDC4xE{2NrkyQ%Ilpi-;i;~b+myR)#<5D?le zJ<_yR$JTv%g~IZpzDQ`^q|0(!H@4<}^Trb|1G%uR=M{IKsk%u++aas{jdtCme~B;ay5v;+7& zfoYRq%h49-5dVNhJZUywONu&Ql*NSju>GTVczWgD(-iYuPN@sd$gSH+{b)UZIUD%7ppKnyim` zLiolI8h9Lj)6YuQ?=7z=6yO|SYu^EoV;BfRBtEfn2l`Yk9$h-?W>uyg7q>udNGf;S z@lURf&nDXR-d*>sFVmhoias!phN1cR%F%BNK^58MjN zH{S1&<8jtI3HUFBC>;9}e;0pgfBAxojP8`p!AtNp6QiiF;fvotzrM_IglBFt;l=m4 z!K%{T{J|fO#v>>58C< zPWbf26_=(O4=FtSJ0r^*=@gW@WLO>FeO{ZJRk-`?c&miDsLvOuJ@c~#B>gcabUyQ> zY*O;Fp(8f$4Qx9r-@bmWBG!uJv8um2D?p7vaa@Q*k~-e<&Ngd~gASWm$?Hya1JOxi zkGlsxhq+CD<3yNlH)}JQ8lk$6FGX3o{b^=m0T6FT! z;5jQvCo5u7HI8ALRg38tyte>6XLgU9=)q;6($x_zWh07F*->Pbbi%LeC@eB5l6XtT zV}D9IOhyl^++CUazrpHF=|4yMNv;>wbN3tIOUxgiB3as`{Ak|cBJ#li{zGQZ(@hxtWB|;G;DBLQ#%mc!CIDK{C>Z3vpRH4YS zh&gF2eod06m%_HYAbBHrn&}`clp_&OF3IgkuMWRi9g@Ph(oUaD{>rxJAIxm}#tPcP>_xdc)b3@`d9(0% z@@k^OdXXdUhZ!Kg^5rm6esfZAMJmt2J+>dZ{)oR^Jr}q;SCSK!5D;_x2|T6w>btyt z9@698x;(BidfoQOFtiWe$+)FkI#?{`mco?B)T|9AMJP0UNWJF80Ge-17@sT|ex}xm zAH{Gbw_A~wu!To-*GwT@wBM9l=vU!Wu&hLOZtDh2?*bj)-oowtEJaY01IR>%PHw)-~z&2|pB^=TqS3PE)O&Hh7t6J$QYwj9+bGB4?1bnS#hB zZ>Ho9^zW$gON*CNtg84OetpPMUOQdg>Fl&ET+dNU%0wt{1}9h7^(} zK-lkA))Pwt6yp>3({gp=8JCMqWVJfH0?c1}J?MB}J}A^2yCK745JlaWz&P|!8;|Xc zRoI}EL-ot{qo_C(G!g=-x+?i$MCy86#FmAYKZZv7Xeoda?-7!uakyAvC$2=s2Xwj- z{;ui=>){(ao~8^T?CrF9q^sZ12Lev8!NGTI=A-PJ4->Z~$_+kKg&t2!Piy^Xz=D!x z9Tnel#C?DfFZNG1mz$5gb+&ZV_wO(NczL&Qd$IRDJ}7)9TMk}46Gz?E3c4c)$xhnt zo#ShP%3sriXD_CI=sm%;X;oKyBGl61r#HL3k%|A~lHDKf)9k*cq^59rNpku2;0Mj? zj7H*b5gI*<9&Cd&tQj`|$=o6cnC- zwGmHV+$~{tg18R9*sl^+YoTc_zg!E1j({_iTL6X#-DI?dKX z-qnhwi(?$A|$yo4xXt|I2SKG+_>#pG;WTuGXi zVQs_ODz~G5FW*TpgN-(sq;+CY!Yh4}pCI zcNVc7aWiBwu{<8bueS)bz7w^7(5IElUr!+!V^dhC|25}H4S_b^;&2`%MHpMcC`{>t zo%P&$>rw2l(`3IJw?&u&j2#SF4$I#Xo_5H??8edTY+imIUEj-O+n+p?wHR~e+a7l0 zco|^b+blK0q)TBTS20SWWyZ*nnta=>V~>Vzy?@h$9N5_B;>e;}?jF?U=6YBtup%;S zyX8rQ6g`JA6Aw6h2R$wh$E|+*`p1cw<1v6)v@H7Q<+j(-d+FZSfQo4IC?_xH=)q10 zO1Evp))$dC>y0rEy$9 z<5K*AEB0NC`nLUqCS+@U(ysYo%(Y*A3V#LGebJRFzx{$yaKtMU7^~B`|7Ek$M7r~{ z5r;l4Ps)CYtd)J}1X%7rS9-5@YF<$JXRD$oZ@tZNX0P)~Kf6V*Fls2~`=+QNx2y z%hhk|iW)uy4E>bw(2TDkF+!uA9lj%#o|}pl+9RV%5^#PR9lsA(GP($p}@)g`|%iblq^ zb05FM0V&CLmz)8#wq2qGtAf&?j?Nohq4Tvqz=hwLG$?l%|64YH3ehJeT|qeADVI0r zD1vs-KQf;3yGDyfS^MnAE4^y>^0#hy0R?>FT?eTp)R>CYJaA(q>I@nm{%p23i&R{^ z3{6|CPl^ibpc+lE#ucJq+oq1VD9yh7L3hGt$iiZHpO^k`m2r4LebCfLyvT#^$ID zr`<24ZuskNmg4r#ki+)QJ*l3#z|gFdRT}SlI|hEVCUSG`<(O-;6Ao{#UK5FSW%o?y ziFyt7aa5+?k!;7Qcv99g6zUkq0o1ZHlx3FY7nYlSUKAr&C?JuorZbNqEe1@G_xP9S zr1^G6OATp9m@ddScur{oH-?*59!8Vn)!G90HNP1^>S2SH#bW+~l#ZtxG(O`pq`EhR zP*%9@DTQSCv5_>iFUfc$Vsp09oTWsK#D<$;F)`m)Y`w2EWUY_ktMQe(2x)9}7*nsi zb^S{kVWiRpH_>8zrip4rmIEC@BK?y4hxutw4R&&c*PSC7Gi_eLT1J+mc~rA4tDg6o zYo;_H=(Qky)4xtJjTl`Bp`uaPb4ME;T#TXPn5MtfzHBE=%J}MsA(9-^&8Jl}opI^Z z8bHCbX#Uk|+uEk}`(nnkvkI6D*@QV29+N!Tlvt{MmbT z{^CAnSp5CO&zHSItuh}4`>S_y!=PUjLykYGz9y0TC!rtuBCWMfCL8}s5K zXR&4A7d}>M8btnqV|I?ugOvbNaR?IXmR!)dtFHN-S(Q1JXp{D_y~+J3apk_bW`ey; zfgC42Ln;&5cbbGwPJ+Vs2i6)!?;R)bavUv|-jw|L*c^{BEESg3Es=S1Vpei*Nad;T z#KJGo4s=fOd0XZEiJ#3zAaB0(W?WSDJD?Ehy%wmNxMq109SaycUFH#(s5uvn+DQ$; z5KhSvU9S@w6Fsgg)DQ%~L$jbm^Ad7HO)pQ^QoU|;c!HjKT3<|%oR06Q5~HRd1p+!n zH=p_v+`Coj_-7Uxn zWU7S=Nf#iz^Zgo{QBUDNf)%S=AJe~O$+DJYZ0E6PmuvbhWP)m8ExKOVpSp~#sjkKt zcu0JZlD0(+T?)*RtX`8k3`OX7@5y;8=zR-ie5@!rc&{U;^K|#1)FlnWmJ7BO+G0|E zQG{exjYP)prk2(>Ss?#5Zy|aXc4fR_1;qBRwFHsAS{Oo^vsc~NHcKB7 z(?etXEfY7B+0BK%UIaT$PZEqL>J<6o>*9tHD6d z=lM=mKff!>QbuU&Y9G}0lR(%sz>(kb22EselU35tX$ z-Q8URQi5y{HX$J$(kMy^g49_&&+q--_Z#1L&KSpk9D}jh-1l5_tu^O0uX)WjJu!2` zm5anB$>_QxFGK0=5#{LCN^bs+;m#Z-jcJ^Kxt+(~?S)M~p_yw&m2moSc|oB*$VW*g z9ckyHyyhxCUSCF2>b~aWi@!K$6fv^74O%GHQ(tVc7+*MU&_^~Wm8KXCC1?7m>=byS z@gAfRUt`hY_iTzNb>SAs~65W7S3p*brgaaehd-lw}tm9Nbve!C}n%EFhRRtnK6yqw;bm= zaq_yw2rgqsRa8{MQa)s|4Ma}9wINPRMotZg>)NqsgBBxr%AnW?EV>q7+9jV0y8SC( zJXfy@mFl?|JbP|A<3Lov(U0dk{a~3l>;8routMeRH#z!y9Pzk#tgQ3o6`93ZU7d^Zbs;YOpwHJOS4|o zgFtzD6b)Z$xOzr{HHO+dXnelQU03h=INMz#UA`H>Y=0^#PSc?vF*HqgfkKk{w@sk<~1C3$->6_sFacM zY7UcZw*3ha-Z&XeY!h>N{*Qn9qB2a^CQV~B-WC41dNZ7t(3S@}=Ql2r>BRJ7c^o_S zeIE%hSN>IgqhN>jO$JwNHRhIL_&##6tY<#ocxc6wIGXRhA9FN0SJvd8&7OGvBtHuH z`Od={5-=#Nug4%saTi@DOsd`*I<6fyj*ZYwB-GN$>>|p7mJ?$e8HkC~*w*UQcDPl1 zsCr_K+c@ey{^0J2X?EQ0k~H9_Ku2JoHCEd85)g`ZHD?83;FQ zbS$34f`WJLv9753Adbvgy68hDW>|{d&}WXQMVvY61{L)P2MQq3fc`52c@BvD7F zGX=zHL@NT<7FbP*1=W)go0~><-1VRGbsBL7p9NUfP(^(ze)XilzAdr^lmu z1X=U=7jB=IXP;n#u@sRax0=v}AgDUh>OdW_%P zIPsyuPkV$nFo%TR6bmP~x1YE?kI&PSbct({6=AvJ&YRHw-ALnW1?mZ(5(N@95S5;>mnnK4cVv31ee=pSzwMr=#pzG;L3DX5SSREhh;hKSusyNB#_JC`bE1{v?@ zv7l*k3d0VLZw-6FODh|4I-&HcujTC`brqW5DczB0((it$^zWA?K(lkSxX#=eJ4co` zOx>A@_iJX{?VAw}uEM1dcw5-fH+r)B{S+;}3}#gJq!c?ntD_;{L(1Nx?QO9cpqwO| zHAthASXPzs%YGa0KzDH~Ixs4ALv%yzM+EIGZp>eIKG2a`sIDLZq z`|E3bGt&n&BiZ)wfWuq@L&dSkv&hG zUu`%t8f_STNwCfDP$9c2@%2%DL#}@`^UY0hE8v*u7BwdFO`75lU3u%bZ&|v%N zOJv_@_|}#kE9dZ34$_a>>ZlL8W*?)gGX?MH%iwGad;l6}0D1O{6jjg`nndOzs86D^ z58m*=5-Z+Ko zCC+@%4*WpVtIx0|pg;5gVeeeN^^j=kS(8lHz-mK-xxE~8uNO7!3-Nv)0a@^>oT`e#9b53O zgXS7+F3P)LrzklN;TH;%_2&ww@%G;H+=BZoVQ59xQ3!U!U^(wOSH7V8bPzt`r-^_B z(%QNw(nB1{2YmW*b;EfC0yCzn>^NM!$Pg2Dsodu6!5SP(hyw?q!So|ypE>@&;o6y$ zyL|jW|J4Jj&G@GOIxuM4bFAhd*{aOsY@+g2w6dP(UF4#bJsLW}F@!g6U1C+I0@W zM0ygD&(&m(s;7GXX#ARB`e?OT$4leX^86IsJ#dp$vtKdaXX$+!hq^OFcHhkwk{+G; zf#cwzO<$!j6HfV_4=r-cwj%{|`m}j(Tw>gmr*&M&>J!?>N=0gsrh=AE8+WUr`1|CS zA3L&XeZPRNSv1*u&6HJbNmY~&iX_^?X++j+v!h6f`Z{hBG+b7n3n+HOCHF5ZaB(nYVk|15si4gp| za{QTSP^O*{(O`RVEt%}s>b@LH%9WnX%w%+TIHVp=)&XMMv=-POr&C%9wN=1{epH(M zrt-&zxqttvi<%P0O;^qI($2aQcJka9!{$Ioq^9U~vX}+RC=XOMX6|-^!I;l?>4F9L zjio#yV~oF(}h|fu9d>lZ@VwxLGaSk{&*}%k5L+<*RC)E?%AaGBR0C7=Q62!k?@}DvqjAOtdx`&e75j>hw=)!eF&|XQ!f7hHQ0(vN5|+LHLd<|V^4}s$Rfa4|m+IrL zd^WJEj+1JGNhSX*>Nu!l;6Xb%{=9Arg25-FA_3bO4)qS>G_*bD)OLZBFa4u&QGt)K@3PQm+gd;=kpu5-NA<{wCv6KMuG z>N+E4EuQ1t$@YSF#o{Xjzrz?+NzMAYO}5YE<8z=;{-w>!wj{@Wxg*>*Z9NY;TGqBi z?HwlGa#q~dyOa93&z_L(KXgotV({MgDr%d5nuc3_#5N|*zx+5Y7yV}J97M9g#A7yHNr=6Dn;|XS2^iF{MIr*IuJSvsTxCPtSoMwwynI!C?ZmgtoEqD;lXFtE2US z;Z@KHfeci#!-4ubvZA8m`Prg}*U`X=sqZ=89&0Uym7}Xdq5yh9pc#4hwLQ~tOAKd` zktdYV0X7d=F*s==qmu2Nb;7?=xz9LPA0$GS|O}2#JV7ES z;doU~) zH%0b4V!8tR8eY2FkXHi_1XpqlRvEQfL0>ZvIuM_BBU4haC|aEQu#HUco1N)2q7)p{ z`Hy+1l~X~q{l`|SN<2Qp0XVWlwMCc7MOH~^0G4=s&*d+PW{+|7viwcb69@Q{^Jf&^ z@1)@7^^#|EwpjR~(9Ok8O5XeqI&E;i=qt>c`P?VRCnpVoC-zJn|9K-c&L@L|GG3bYFu=u()%*Q zNB=vo9MF@5{_YmVnx<#;aW36YOd{~;I!!$m@Qx;z`xv(&D`&e+I^}c)hM4^2qpUE= zLPm;#+c%+-zQ*JZhqu(cxU#rw_V_pDdc6FXO;#nC80M|m(ih{9p2g0+@z;stt}h0( z!dVG!3IPB-b{3lfktu|Idp;=RxjR9>MF5$ti2Co(>3FQlRf*UQ6WjF{Sdn&&hF)wv z81QABkC8Aj7;Apy_A_ORyxst=oX z-APw14MVxE??hXEKJ4d0i=J>UrMOS|y0gPM(|>UZuC@Z*Kfh&CRXQSffU*m&DljPo zvP7k%sD01}2L~DH>E*;AfhnWJsr30F1i$?mU)}q-s({<;KPei=Kqu&zM{k${z`)V- zHO!Dm>sInn#HUz6BN;PGG!yqO&l1z2)+f4|iBqRXApgJc@V$4WObzoaP<+ zUc{>b8}a90d<(9;t-XW=SpDJQBecJTueo(Cb{IcgWsFr7DJS-Ue6uJ~TG#3)7#Wq4 zWzzbY()_Epc+T*sdz1aB=LpyPwPpX+0dSHgt1JJa8;=|8xjC)F+TF5$B(m)D?pZ(L z?BXDW!>~{zOPEa15&sNSY9aAgAt`D+UBm(oV~@bRx1$+DOSWINi|!vZ=7U+8nbEYQ zFN*mSGh^#6ydFGH*qbpVAH~_+-YyJ0*`9mvK^uB?q`U-dU2nuWoNZ+24cN?QOWxxj zx1JS%ky`2EzU;fqMbbZJ>kKW*YJHMM)A{aAm1^{Z2jeMmOR$@TYRy@);nVM29@Sv5 zlq)W_x`k-QBv^LTV)A{hHLt3EKn>;ton@{gmzG{-*!}&z7jShnm?A_+SCEz9?l5){ zP3f-vHGJdO@`v}xI-)${C!!wqfrL*FkKAfAMC1E^M6nd%V56!cr6Wm}-Be&Dx=5|Y z3~<`LDU^i~gBHN|kh^XFE`0;p9N?+u4u5Ri$f95<)iGW2o>DwNC^qz}Zsv#er!n6h z|IT;)1ubf{pI*7l!{6FbeNuc=d-wBA65fA~e&#hom_x-{dfh0x=ZgoDwImD^1kZoe zyk7@boGkYecn#u zHtgPaF!k@Wp{XpgU)Q{9b}kPyYvY~}^J;=1uHiGvTs89+lwZ|%?|r5MLDn=`4dr}; z?<~@W5+@gE9|&UwvM&@Pm-*jF@)ra6!+tQ0w0#>$qq$o%S0L1XcU?0>PbtD&c??<} zb`8L4pQcKEw9J1mXwydpr;rRS0!xs^ zLI1wymVwSfB#5kt5;H{__h#_4YP<{ku0=iBy;DLZgXei(uL!+u`5F5pF%R zk*M;h4^gF@KHG3OJU3uJBa!UHfmFP457(mJDjYdJ!@PQL$PaTH)2b>GPUXMa9ioMZ z!QDDOe#Rs)Wk@dwqJ<`s`jO)y#zZ;^2}&Si-*N06gG+t5R|e&=Ur!c{Qn|R{`V{C% z^j(9oE7bnWiuS0eeJ9vx^0pm=?cHW9sh7V#;CXp@Gj}x>4w#;@%yA_IFWx6DoGbH$VQYQClz_TbYEEwAD*I zhmT&M5u(HU1QZ&Wn3;cl9v#j3=J%ltd5(0G{_&r?-_5-{JpJ!EPW9^U-tR>Xd0|Z?wh7N$kuC*b%3zf=G^<%Di!-`xaR$qq| z$~LnfvN*anE#MOo`L9#jS@tETr4^Ky)0z0b3%mhBJ$eG-fhH$;SlE7j%YZJ|qT zUZqKl$_wX1Y#!$&REA*?H;=TR#Qzi!cpV$-P89M_2z983R8+vX_?!&pU-#w4Q$hD2 z@8Y1Nv$YFLc1?e_TysOJMGnP60pmV7N3Z zZ=Lpc$dxN!BmX5{W4ve5njji~>crgg2 zre@m&;@veU-4#r!9)9a@T(sZY+k34PNA|^e{nINigvI>iksE`iqwYH`{cC6M1VU`s{L2TkK`fL z4*LujHT=(5|>IQ?&BCgk?!>X!LhyS^YM7Z04~luk|x%@lM6ks8X!=|9}&anKIwS$fZIE3kGJ-- zllt7~zK!#Gj#DrgG{ZrF?{ocZv~Awf?VslXIv^v0%owF;dC*Ne;p0At=)S;Ix;LGy z^pEPMWZ!&_>?C@G)V2~7FrE3s3Gj%0DQ1H=ey!e$RB&NEDnurNNit=?liHg3%Var# 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 ZwBwcptnAeJ@;~5@xQNX2Y@yd){|DGlJ^KIv diff --git a/protobuf_to_pydantic/gen_model.py b/protobuf_to_pydantic/gen_model.py index 39bacfe..95c9c8e 100644 --- a/protobuf_to_pydantic/gen_model.py +++ b/protobuf_to_pydantic/gen_model.py @@ -30,7 +30,7 @@ get_message_option_dict_from_pyi_file, ) from protobuf_to_pydantic.grpc_types import AnyMessage, Descriptor, FieldDescriptor, FieldMask, Message -from protobuf_to_pydantic.template import CommentTemplate +from protobuf_to_pydantic.template import Template from protobuf_to_pydantic.util import create_pydantic_model if TYPE_CHECKING: @@ -121,7 +121,7 @@ def __init__( pydantic_base: Optional[Type["BaseModel"]] = None, pydantic_module: Optional[str] = None, local_dict: Optional[Dict[str, Any]] = None, - desc_template: Optional[Type[CommentTemplate]] = None, + template: Optional[Type[Template]] = None, message_type_dict_by_type_name: Optional[Dict[str, Any]] = None, message_default_factory_dict_by_type_name: Optional[Dict[str, Any]] = None, create_model_cache: Optional[CREATE_MODEL_CACHE_T] = None, @@ -166,9 +166,7 @@ def __init__( self._creat_cache: CREATE_MODEL_CACHE_T = create_model_cache or _create_model_cache self._pydantic_base: Type["BaseModel"] = pydantic_base or BaseModel self._pydantic_module: str = pydantic_module or __name__ - self._comment_template: CommentTemplate = (desc_template or CommentTemplate)( - local_dict or {}, self._comment_prefix - ) + self._comment_template: Template = (template or Template)(local_dict or {}, self._comment_prefix) self._message_type_dict_by_type_name: Dict[str, Any] = ( message_type_dict_by_type_name or constant.message_name_type_dict ) @@ -630,7 +628,7 @@ def msg_to_pydantic_model( local_dict: Optional[Dict[str, Any]] = None, pydantic_base: Optional[Type["BaseModel"]] = None, pydantic_module: Optional[str] = None, - desc_template: Optional[Type[CommentTemplate]] = None, + template: Optional[Type[Template]] = None, message_type_dict_by_type_name: Optional[Dict[str, Any]] = None, message_default_factory_dict_by_type_name: Optional[Dict[str, Any]] = None, create_model_cache: Optional[CREATE_MODEL_CACHE_T] = None, @@ -654,7 +652,7 @@ def msg_to_pydantic_model( :param local_dict: The variables corresponding to the p2p@local template :param pydantic_base: custom pydantic.BaseModel :param pydantic_module: custom create model's module name - :param desc_template: DescTemplate object, which can extend and modify template adaptation rules through inheritance + :param template: DescTemplate object, which can extend and modify template adaptation rules through inheritance :param message_type_dict_by_type_name: Define the Python type mapping corresponding to each Protobuf Type :param message_default_factory_dict_by_type_name: Define the default_factory corresponding to each Protobuf Type :param create_model_cache: Cache the generated model @@ -667,7 +665,7 @@ def msg_to_pydantic_model( local_dict=local_dict, pydantic_module=pydantic_module, pydantic_base=pydantic_base, - desc_template=desc_template, + template=template, message_type_dict_by_type_name=message_type_dict_by_type_name, message_default_factory_dict_by_type_name=message_default_factory_dict_by_type_name, create_model_cache=create_model_cache, diff --git a/protobuf_to_pydantic/plugin/config.py b/protobuf_to_pydantic/plugin/config.py index ab7a09b..9095f5f 100644 --- a/protobuf_to_pydantic/plugin/config.py +++ b/protobuf_to_pydantic/plugin/config.py @@ -5,7 +5,7 @@ from protobuf_to_pydantic import _pydantic_adapter from protobuf_to_pydantic.plugin.field_desc_proto_to_code import FileDescriptorProtoToCode -from protobuf_to_pydantic.template import CommentTemplate +from protobuf_to_pydantic.template import Template ConfigT = TypeVar("ConfigT", bound="ConfigModel") @@ -24,27 +24,15 @@ class SubConfigModel(BaseModel): class ConfigModel(BaseModel): - local_dict: dict = Field(default_factory=dict, description="Dict for local variables") - desc_template: Type[CommentTemplate] = Field( - default=CommentTemplate, description="Support more templates by customizing 'Desc Template'" - ) - comment_prefix: str = Field(default="p2p", description="Comment prefix") - parse_comment: bool = Field( - default=True, - description="If true, the annotation is parsed and the validation rule data is extracted from the annotation", - ) + # output code config customer_import_set: Set[str] = Field(default_factory=set, description="customer import code set") - customer_deque: Deque = Field(default_factory=deque) - module_path: str = Field(default="") + customer_deque: Deque = Field(default_factory=deque, description="customer file content") + code_indent: int = Field(default=4, description="Code indent") + module_path: str = Field(default="", description="protobuf project path") pyproject_file_path: str = Field( default="", description="pyproject file path, In general, pyproject.toml of the project can be found automatically", ) - code_indent: int = Field(default=4, description="Code indent") - ignore_pkg_list: List[str] = Field( - default_factory=lambda: ["validate", "p2p_validate"], description="Ignore the specified pkg file" - ) - base_model_class: Type[BaseModel] = Field(default=BaseModel) file_name_suffix: str = Field( default="_p2p", description=( @@ -52,7 +40,25 @@ class ConfigModel(BaseModel): "For example, if the name of the proto file is `book`, the generated file name is `book_p2p.py`" ), ) - file_descriptor_proto_to_code: Type[FileDescriptorProtoToCode] = Field(default=FileDescriptorProtoToCode) + + # gen message config + local_dict: dict = Field(default_factory=dict, description="Dict for local variables") + template: Type[Template] = Field(default=Template, description="Support more templates by customizing 'Template'") + comment_prefix: str = Field(default="p2p", description="Comment prefix") + parse_comment: bool = Field( + default=True, + description="If true, the annotation is parsed and the validation rule data is extracted from the annotation", + ) + ignore_pkg_list: List[str] = Field( + default_factory=lambda: ["validate", "p2p_validate"], description="Ignore the specified pkg file" + ) + base_model_class: Type[BaseModel] = Field(default=BaseModel, description="Inherited base pydantic model") + + # other config + file_descriptor_proto_to_code: Type[FileDescriptorProtoToCode] = Field( + default=FileDescriptorProtoToCode, + description="If you have modified the resolution rules, then you can customize FileDescriptorProtoToCode", + ) protobuf_type_config: Dict[str, ProtobufTypeConfigModel] = Field( default_factory=dict, description=""" @@ -83,13 +89,13 @@ class ConfigModel(BaseModel): ``` """, ) - desc_template_instance: CommentTemplate = Field( - default_factory=lambda: CommentTemplate({}, ""), - description="This variable does not support configuration and will be overwritten even if configured", - ) pkg_config: Dict[str, "ConfigModel"] = Field( default_factory=dict, description="Customize the configuration of different pkgs" ) + template_instance: Template = Field( + default_factory=lambda: Template({}, ""), + description="This variable does not support configuration and will be overwritten even if configured", + ) class Config: arbitrary_types_allowed = True @@ -98,11 +104,11 @@ class Config: def after_init(cls, values: Any) -> Any: if _pydantic_adapter.is_v1: # values: Dict[str, Any] - values["desc_template_instance"] = values["desc_template"](values["local_dict"], values["comment_prefix"]) + values["template_instance"] = values["template"](values["local_dict"], values["comment_prefix"]) return values else: # values: "ConfigModel" - values.desc_template_instance = values.desc_template(values.local_dict, values.comment_prefix) + values.template_instance = values.template(values.local_dict, values.comment_prefix) return values @_pydantic_adapter.model_validator(mode="before") diff --git a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py index caeacae..88deda0 100644 --- a/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py +++ b/protobuf_to_pydantic/plugin/field_desc_proto_to_code.py @@ -77,7 +77,7 @@ def __init__(self, fd: FileDescriptorProto, descriptors: Descriptors, config: "C self.config = config self._fd = fd self._descriptors = descriptors - self._desc_template = config.desc_template_instance + self._desc_template = config.template_instance self.source_code_info_by_scl = {tuple(location.path): location for location in fd.source_code_info.location} if config.base_model_class is BaseModel: diff --git a/protobuf_to_pydantic/template/__init__.py b/protobuf_to_pydantic/template/__init__.py index a342ac0..e9e97ef 100644 --- a/protobuf_to_pydantic/template/__init__.py +++ b/protobuf_to_pydantic/template/__init__.py @@ -7,7 +7,7 @@ _T = TypeVar("_T") -class CommentTemplate(object): +class Template(object): def __init__(self, local_dict: Dict[str, Any], comment_prefix: str, **kwargs: Any) -> None: """ :param local_dict: local template var diff --git a/tests/base/base_p2p_validate.py b/tests/base/base_p2p_validate.py index d192661..92b6935 100644 --- a/tests/base/base_p2p_validate.py +++ b/tests/base/base_p2p_validate.py @@ -34,29 +34,29 @@ def test_number_model_in_validator(self) -> None: for model_class in self.number_model_class_list: for i in [1, 2, 3]: - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(in_test=i, miss_default_test=1.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, template=CustomCommentTemplate)(in_test=i, miss_default_test=1.0, required_test=1.0) for i in [0, 4]: with pytest.raises(ValidationError): - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(in_test=i, miss_default_test=1.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, template=CustomCommentTemplate)(in_test=i, miss_default_test=1.0, required_test=1.0) def test_number_model_not_in_validator(self) -> None: for model_class in self.number_model_class_list: for i in [1, 2, 3]: with pytest.raises(ValidationError): - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(not_in_test=i, miss_default_test=1.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, template=CustomCommentTemplate)(not_in_test=i, miss_default_test=1.0, required_test=1.0) for i in [0, 4]: - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(not_in_test=i, miss_default_test=1.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, template=CustomCommentTemplate)(not_in_test=i, miss_default_test=1.0, required_test=1.0) def test_number_model_miss_default_validator(self) -> None: for model_class in self.number_model_class_list: with pytest.raises(ValidationError): - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)() + self.replace_message_fn(model_class, local_dict=local_dict, template=CustomCommentTemplate)() def test_number_model_miss_multiple_of_validator(self) -> None: for model_class in self.number_model_class_list: - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(miss_default_test=1.0, multiple_of_test=6.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, template=CustomCommentTemplate)(miss_default_test=1.0, multiple_of_test=6.0, required_test=1.0) with pytest.raises(ValidationError): - self.replace_message_fn(model_class, local_dict=local_dict, desc_template=CustomCommentTemplate)(miss_default_test=1.0, multiple_of_test=7.0, required_test=1.0) + self.replace_message_fn(model_class, local_dict=local_dict, template=CustomCommentTemplate)(miss_default_test=1.0, multiple_of_test=7.0, required_test=1.0) def _test_bool(self, model_class: Type) -> None: model_class(bool_1_test=True, bool_2_test=False, miss_default_test=True, required_test=True) diff --git a/tests/test_gen_code_output_by_cli/test_p2p_validate_proto/test_p2p_validate.py b/tests/test_gen_code_output_by_cli/test_p2p_validate_proto/test_p2p_validate.py index 27ca150..04811ce 100644 --- a/tests/test_gen_code_output_by_cli/test_p2p_validate_proto/test_p2p_validate.py +++ b/tests/test_gen_code_output_by_cli/test_p2p_validate_proto/test_p2p_validate.py @@ -35,7 +35,7 @@ def _model_output(msg: Any) -> str: "conint": conint, "customer_any": customer_any, } - return pydantic_model_to_py_code(msg_to_pydantic_model(msg, local_dict=local_dict, desc_template=CustomCommentTemplate)) + return pydantic_model_to_py_code(msg_to_pydantic_model(msg, local_dict=local_dict, template=CustomCommentTemplate)) @staticmethod def assert_contains(content: str, other_content: str) -> None: