diff options
Diffstat (limited to 'src/aristaproto')
35 files changed, 10013 insertions, 0 deletions
diff --git a/src/aristaproto/__init__.py b/src/aristaproto/__init__.py new file mode 100644 index 0000000..79d71c5 --- /dev/null +++ b/src/aristaproto/__init__.py @@ -0,0 +1,2038 @@ +from __future__ import annotations + +import dataclasses +import enum as builtin_enum +import json +import math +import struct +import sys +import typing +import warnings +from abc import ABC +from base64 import ( + b64decode, + b64encode, +) +from copy import deepcopy +from datetime import ( + datetime, + timedelta, + timezone, +) +from io import BytesIO +from itertools import count +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Dict, + Generator, + Iterable, + Mapping, + Optional, + Set, + Tuple, + Type, + Union, + get_type_hints, +) + +from dateutil.parser import isoparse +from typing_extensions import Self + +from ._types import T +from ._version import __version__ +from .casing import ( + camel_case, + safe_snake_case, + snake_case, +) +from .enum import Enum as Enum +from .grpc.grpclib_client import ServiceStub as ServiceStub +from .utils import ( + classproperty, + hybridmethod, +) + + +if TYPE_CHECKING: + from _typeshed import ( + SupportsRead, + SupportsWrite, + ) + + +# Proto 3 data types +TYPE_ENUM = "enum" +TYPE_BOOL = "bool" +TYPE_INT32 = "int32" +TYPE_INT64 = "int64" +TYPE_UINT32 = "uint32" +TYPE_UINT64 = "uint64" +TYPE_SINT32 = "sint32" +TYPE_SINT64 = "sint64" +TYPE_FLOAT = "float" +TYPE_DOUBLE = "double" +TYPE_FIXED32 = "fixed32" +TYPE_SFIXED32 = "sfixed32" +TYPE_FIXED64 = "fixed64" +TYPE_SFIXED64 = "sfixed64" +TYPE_STRING = "string" +TYPE_BYTES = "bytes" +TYPE_MESSAGE = "message" +TYPE_MAP = "map" + +# Fields that use a fixed amount of space (4 or 8 bytes) +FIXED_TYPES = [ + TYPE_FLOAT, + TYPE_DOUBLE, + TYPE_FIXED32, + TYPE_SFIXED32, + TYPE_FIXED64, + TYPE_SFIXED64, +] + +# Fields that are numerical 64-bit types +INT_64_TYPES = [TYPE_INT64, TYPE_UINT64, TYPE_SINT64, TYPE_FIXED64, TYPE_SFIXED64] + +# Fields that are efficiently packed when +PACKED_TYPES = [ + TYPE_ENUM, + TYPE_BOOL, + TYPE_INT32, + TYPE_INT64, + TYPE_UINT32, + TYPE_UINT64, + TYPE_SINT32, + TYPE_SINT64, + TYPE_FLOAT, + TYPE_DOUBLE, + TYPE_FIXED32, + TYPE_SFIXED32, + TYPE_FIXED64, + TYPE_SFIXED64, +] + +# Wire types +# https://developers.google.com/protocol-buffers/docs/encoding#structure +WIRE_VARINT = 0 +WIRE_FIXED_64 = 1 +WIRE_LEN_DELIM = 2 +WIRE_FIXED_32 = 5 + +# Mappings of which Proto 3 types correspond to which wire types. +WIRE_VARINT_TYPES = [ + TYPE_ENUM, + TYPE_BOOL, + TYPE_INT32, + TYPE_INT64, + TYPE_UINT32, + TYPE_UINT64, + TYPE_SINT32, + TYPE_SINT64, +] + +WIRE_FIXED_32_TYPES = [TYPE_FLOAT, TYPE_FIXED32, TYPE_SFIXED32] +WIRE_FIXED_64_TYPES = [TYPE_DOUBLE, TYPE_FIXED64, TYPE_SFIXED64] +WIRE_LEN_DELIM_TYPES = [TYPE_STRING, TYPE_BYTES, TYPE_MESSAGE, TYPE_MAP] + +# Indicator of message delimitation in streams +SIZE_DELIMITED = -1 + + +class _DateTime(datetime): + """Subclass of datetime with an attribute to store the original nanos value from a Timestamp field""" + + __slots__ = "_nanos" + + @property + def nanos(self): + return self._nanos + + +# Protobuf datetimes start at the Unix Epoch in 1970 in UTC. +def datetime_default_gen() -> _DateTime: + return _DateTime(1970, 1, 1, tzinfo=timezone.utc) + + +DATETIME_ZERO = datetime_default_gen() + +# Special protobuf json doubles +INFINITY = "Infinity" +NEG_INFINITY = "-Infinity" +NAN = "NaN" + + +class Casing(builtin_enum.Enum): + """Casing constants for serialization.""" + + CAMEL = camel_case #: A camelCase sterilization function. + SNAKE = snake_case #: A snake_case sterilization function. + + +PLACEHOLDER: Any = object() + + +@dataclasses.dataclass(frozen=True) +class FieldMetadata: + """Stores internal metadata used for parsing & serialization.""" + + # Protobuf field number + number: int + # Protobuf type name + proto_type: str + # Map information if the proto_type is a map + map_types: Optional[Tuple[str, str]] = None + # Groups several "one-of" fields together + group: Optional[str] = None + # Describes the wrapped type (e.g. when using google.protobuf.BoolValue) + wraps: Optional[str] = None + # Is the field optional + optional: Optional[bool] = False + + @staticmethod + def get(field: dataclasses.Field) -> "FieldMetadata": + """Returns the field metadata for a dataclass field.""" + return field.metadata["aristaproto"] + + +def dataclass_field( + number: int, + proto_type: str, + *, + map_types: Optional[Tuple[str, str]] = None, + group: Optional[str] = None, + wraps: Optional[str] = None, + optional: bool = False, +) -> dataclasses.Field: + """Creates a dataclass field with attached protobuf metadata.""" + return dataclasses.field( + default=None if optional else PLACEHOLDER, + metadata={ + "aristaproto": FieldMetadata( + number, proto_type, map_types, group, wraps, optional + ) + }, + ) + + +# Note: the fields below return `Any` to prevent type errors in the generated +# data classes since the types won't match with `Field` and they get swapped +# out at runtime. The generated dataclass variables are still typed correctly. + + +def enum_field(number: int, group: Optional[str] = None, optional: bool = False) -> Any: + return dataclass_field(number, TYPE_ENUM, group=group, optional=optional) + + +def bool_field(number: int, group: Optional[str] = None, optional: bool = False) -> Any: + return dataclass_field(number, TYPE_BOOL, group=group, optional=optional) + + +def int32_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_INT32, group=group, optional=optional) + + +def int64_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_INT64, group=group, optional=optional) + + +def uint32_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_UINT32, group=group, optional=optional) + + +def uint64_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_UINT64, group=group, optional=optional) + + +def sint32_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_SINT32, group=group, optional=optional) + + +def sint64_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_SINT64, group=group, optional=optional) + + +def float_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_FLOAT, group=group, optional=optional) + + +def double_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_DOUBLE, group=group, optional=optional) + + +def fixed32_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_FIXED32, group=group, optional=optional) + + +def fixed64_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_FIXED64, group=group, optional=optional) + + +def sfixed32_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_SFIXED32, group=group, optional=optional) + + +def sfixed64_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_SFIXED64, group=group, optional=optional) + + +def string_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_STRING, group=group, optional=optional) + + +def bytes_field( + number: int, group: Optional[str] = None, optional: bool = False +) -> Any: + return dataclass_field(number, TYPE_BYTES, group=group, optional=optional) + + +def message_field( + number: int, + group: Optional[str] = None, + wraps: Optional[str] = None, + optional: bool = False, +) -> Any: + return dataclass_field( + number, TYPE_MESSAGE, group=group, wraps=wraps, optional=optional + ) + + +def map_field( + number: int, key_type: str, value_type: str, group: Optional[str] = None +) -> Any: + return dataclass_field( + number, TYPE_MAP, map_types=(key_type, value_type), group=group + ) + + +def _pack_fmt(proto_type: str) -> str: + """Returns a little-endian format string for reading/writing binary.""" + return { + TYPE_DOUBLE: "<d", + TYPE_FLOAT: "<f", + TYPE_FIXED32: "<I", + TYPE_FIXED64: "<Q", + TYPE_SFIXED32: "<i", + TYPE_SFIXED64: "<q", + }[proto_type] + + +def dump_varint(value: int, stream: "SupportsWrite[bytes]") -> None: + """Encodes a single varint and dumps it into the provided stream.""" + if value < -(1 << 63): + raise ValueError( + "Negative value is not representable as a 64-bit integer - unable to encode a varint within 10 bytes." + ) + elif value < 0: + value += 1 << 64 + + bits = value & 0x7F + value >>= 7 + while value: + stream.write((0x80 | bits).to_bytes(1, "little")) + bits = value & 0x7F + value >>= 7 + stream.write(bits.to_bytes(1, "little")) + + +def encode_varint(value: int) -> bytes: + """Encodes a single varint value for serialization.""" + with BytesIO() as stream: + dump_varint(value, stream) + return stream.getvalue() + + +def size_varint(value: int) -> int: + """Calculates the size in bytes that a value would take as a varint.""" + if value < -(1 << 63): + raise ValueError( + "Negative value is not representable as a 64-bit integer - unable to encode a varint within 10 bytes." + ) + elif value < 0: + return 10 + elif value == 0: + return 1 + else: + return math.ceil(value.bit_length() / 7) + + +def _preprocess_single(proto_type: str, wraps: str, value: Any) -> bytes: + """Adjusts values before serialization.""" + if proto_type in ( + TYPE_ENUM, + TYPE_BOOL, + TYPE_INT32, + TYPE_INT64, + TYPE_UINT32, + TYPE_UINT64, + ): + return encode_varint(value) + elif proto_type in (TYPE_SINT32, TYPE_SINT64): + # Handle zig-zag encoding. + return encode_varint(value << 1 if value >= 0 else (value << 1) ^ (~0)) + elif proto_type in FIXED_TYPES: + return struct.pack(_pack_fmt(proto_type), value) + elif proto_type == TYPE_STRING: + return value.encode("utf-8") + elif proto_type == TYPE_MESSAGE: + if isinstance(value, datetime): + # Convert the `datetime` to a timestamp message. + value = _Timestamp.from_datetime(value) + elif isinstance(value, timedelta): + # Convert the `timedelta` to a duration message. + value = _Duration.from_timedelta(value) + elif wraps: + if value is None: + return b"" + value = _get_wrapper(wraps)(value=value) + + return bytes(value) + + return value + + +def _len_preprocessed_single(proto_type: str, wraps: str, value: Any) -> int: + """Calculate the size of adjusted values for serialization without fully serializing them.""" + if proto_type in ( + TYPE_ENUM, + TYPE_BOOL, + TYPE_INT32, + TYPE_INT64, + TYPE_UINT32, + TYPE_UINT64, + ): + return size_varint(value) + elif proto_type in (TYPE_SINT32, TYPE_SINT64): + # Handle zig-zag encoding. + return size_varint(value << 1 if value >= 0 else (value << 1) ^ (~0)) + elif proto_type in FIXED_TYPES: + return len(struct.pack(_pack_fmt(proto_type), value)) + elif proto_type == TYPE_STRING: + return len(value.encode("utf-8")) + elif proto_type == TYPE_MESSAGE: + if isinstance(value, datetime): + # Convert the `datetime` to a timestamp message. + value = _Timestamp.from_datetime(value) + elif isinstance(value, timedelta): + # Convert the `timedelta` to a duration message. + value = _Duration.from_timedelta(value) + elif wraps: + if value is None: + return 0 + value = _get_wrapper(wraps)(value=value) + + return len(bytes(value)) + + return len(value) + + +def _serialize_single( + field_number: int, + proto_type: str, + value: Any, + *, + serialize_empty: bool = False, + wraps: str = "", +) -> bytes: + """Serializes a single field and value.""" + value = _preprocess_single(proto_type, wraps, value) + + output = bytearray() + if proto_type in WIRE_VARINT_TYPES: + key = encode_varint(field_number << 3) + output += key + value + elif proto_type in WIRE_FIXED_32_TYPES: + key = encode_varint((field_number << 3) | 5) + output += key + value + elif proto_type in WIRE_FIXED_64_TYPES: + key = encode_varint((field_number << 3) | 1) + output += key + value + elif proto_type in WIRE_LEN_DELIM_TYPES: + if len(value) or serialize_empty or wraps: + key = encode_varint((field_number << 3) | 2) + output += key + encode_varint(len(value)) + value + else: + raise NotImplementedError(proto_type) + + return bytes(output) + + +def _len_single( + field_number: int, + proto_type: str, + value: Any, + *, + serialize_empty: bool = False, + wraps: str = "", +) -> int: + """Calculates the size of a serialized single field and value.""" + size = _len_preprocessed_single(proto_type, wraps, value) + if proto_type in WIRE_VARINT_TYPES: + size += size_varint(field_number << 3) + elif proto_type in WIRE_FIXED_32_TYPES: + size += size_varint((field_number << 3) | 5) + elif proto_type in WIRE_FIXED_64_TYPES: + size += size_varint((field_number << 3) | 1) + elif proto_type in WIRE_LEN_DELIM_TYPES: + if size or serialize_empty or wraps: + size += size_varint((field_number << 3) | 2) + size_varint(size) + else: + raise NotImplementedError(proto_type) + + return size + + +def _parse_float(value: Any) -> float: + """Parse the given value to a float + + Parameters + ---------- + value: Any + Value to parse + + Returns + ------- + float + Parsed value + """ + if value == INFINITY: + return float("inf") + if value == NEG_INFINITY: + return -float("inf") + if value == NAN: + return float("nan") + return float(value) + + +def _dump_float(value: float) -> Union[float, str]: + """Dump the given float to JSON + + Parameters + ---------- + value: float + Value to dump + + Returns + ------- + Union[float, str] + Dumped value, either a float or the strings + """ + if value == float("inf"): + return INFINITY + if value == -float("inf"): + return NEG_INFINITY + if isinstance(value, float) and math.isnan(value): + return NAN + return value + + +def load_varint(stream: "SupportsRead[bytes]") -> Tuple[int, bytes]: + """ + Load a single varint value from a stream. Returns the value and the raw bytes read. + """ + result = 0 + raw = b"" + for shift in count(0, 7): + if shift >= 64: + raise ValueError("Too many bytes when decoding varint.") + b = stream.read(1) + if not b: + raise EOFError("Stream ended unexpectedly while attempting to load varint.") + raw += b + b_int = int.from_bytes(b, byteorder="little") + result |= (b_int & 0x7F) << shift + if not (b_int & 0x80): + return result, raw + + +def decode_varint(buffer: bytes, pos: int) -> Tuple[int, int]: + """ + Decode a single varint value from a byte buffer. Returns the value and the + new position in the buffer. + """ + with BytesIO(buffer) as stream: + stream.seek(pos) + value, raw = load_varint(stream) + return value, pos + len(raw) + + +@dataclasses.dataclass(frozen=True) +class ParsedField: + number: int + wire_type: int + value: Any + raw: bytes + + +def load_fields(stream: "SupportsRead[bytes]") -> Generator[ParsedField, None, None]: + while True: + try: + num_wire, raw = load_varint(stream) + except EOFError: + return + number = num_wire >> 3 + wire_type = num_wire & 0x7 + + decoded: Any = None + if wire_type == WIRE_VARINT: + decoded, r = load_varint(stream) + raw += r + elif wire_type == WIRE_FIXED_64: + decoded = stream.read(8) + raw += decoded + elif wire_type == WIRE_LEN_DELIM: + length, r = load_varint(stream) + decoded = stream.read(length) + raw += r + raw += decoded + elif wire_type == WIRE_FIXED_32: + decoded = stream.read(4) + raw += decoded + + yield ParsedField(number=number, wire_type=wire_type, value=decoded, raw=raw) + + +def parse_fields(value: bytes) -> Generator[ParsedField, None, None]: + i = 0 + while i < len(value): + start = i + num_wire, i = decode_varint(value, i) + number = num_wire >> 3 + wire_type = num_wire & 0x7 + + decoded: Any = None + if wire_type == WIRE_VARINT: + decoded, i = decode_varint(value, i) + elif wire_type == WIRE_FIXED_64: + decoded, i = value[i : i + 8], i + 8 + elif wire_type == WIRE_LEN_DELIM: + length, i = decode_varint(value, i) + decoded = value[i : i + length] + i += length + elif wire_type == WIRE_FIXED_32: + decoded, i = value[i : i + 4], i + 4 + + yield ParsedField( + number=number, wire_type=wire_type, value=decoded, raw=value[start:i] + ) + + +class ProtoClassMetadata: + __slots__ = ( + "oneof_group_by_field", + "oneof_field_by_group", + "default_gen", + "cls_by_field", + "field_name_by_number", + "meta_by_field_name", + "sorted_field_names", + ) + + oneof_group_by_field: Dict[str, str] + oneof_field_by_group: Dict[str, Set[dataclasses.Field]] + field_name_by_number: Dict[int, str] + meta_by_field_name: Dict[str, FieldMetadata] + sorted_field_names: Tuple[str, ...] + default_gen: Dict[str, Callable[[], Any]] + cls_by_field: Dict[str, Type] + + def __init__(self, cls: Type["Message"]): + by_field = {} + by_group: Dict[str, Set] = {} + by_field_name = {} + by_field_number = {} + + fields = dataclasses.fields(cls) + for field in fields: + meta = FieldMetadata.get(field) + + if meta.group: + # This is part of a one-of group. + by_field[field.name] = meta.group + + by_group.setdefault(meta.group, set()).add(field) + + by_field_name[field.name] = meta + by_field_number[meta.number] = field.name + + self.oneof_group_by_field = by_field + self.oneof_field_by_group = by_group + self.field_name_by_number = by_field_number + self.meta_by_field_name = by_field_name + self.sorted_field_names = tuple( + by_field_number[number] for number in sorted(by_field_number) + ) + self.default_gen = self._get_default_gen(cls, fields) + self.cls_by_field = self._get_cls_by_field(cls, fields) + + @staticmethod + def _get_default_gen( + cls: Type["Message"], fields: Iterable[dataclasses.Field] + ) -> Dict[str, Callable[[], Any]]: + return {field.name: cls._get_field_default_gen(field) for field in fields} + + @staticmethod + def _get_cls_by_field( + cls: Type["Message"], fields: Iterable[dataclasses.Field] + ) -> Dict[str, Type]: + field_cls = {} + + for field in fields: + meta = FieldMetadata.get(field) + if meta.proto_type == TYPE_MAP: + assert meta.map_types + kt = cls._cls_for(field, index=0) + vt = cls._cls_for(field, index=1) + field_cls[field.name] = dataclasses.make_dataclass( + "Entry", + [ + ("key", kt, dataclass_field(1, meta.map_types[0])), + ("value", vt, dataclass_field(2, meta.map_types[1])), + ], + bases=(Message,), + ) + field_cls[f"{field.name}.value"] = vt + else: + field_cls[field.name] = cls._cls_for(field) + + return field_cls + + +class Message(ABC): + """ + The base class for protobuf messages, all generated messages will inherit from + this. This class registers the message fields which are used by the serializers and + parsers to go between the Python, binary and JSON representations of the message. + + .. container:: operations + + .. describe:: bytes(x) + + Calls :meth:`__bytes__`. + + .. describe:: bool(x) + + Calls :meth:`__bool__`. + """ + + _serialized_on_wire: bool + _unknown_fields: bytes + _group_current: Dict[str, str] + _aristaproto_meta: ClassVar[ProtoClassMetadata] + + def __post_init__(self) -> None: + # Keep track of whether every field was default + all_sentinel = True + + # Set current field of each group after `__init__` has already been run. + group_current: Dict[str, Optional[str]] = {} + for field_name, meta in self._aristaproto.meta_by_field_name.items(): + if meta.group: + group_current.setdefault(meta.group) + + value = self.__raw_get(field_name) + if value is not PLACEHOLDER and not (meta.optional and value is None): + # Found a non-sentinel value + all_sentinel = False + + if meta.group: + # This was set, so make it the selected value of the one-of. + group_current[meta.group] = field_name + + # Now that all the defaults are set, reset it! + self.__dict__["_serialized_on_wire"] = not all_sentinel + self.__dict__["_unknown_fields"] = b"" + self.__dict__["_group_current"] = group_current + + def __raw_get(self, name: str) -> Any: + return super().__getattribute__(name) + + def __eq__(self, other) -> bool: + if type(self) is not type(other): + return NotImplemented + + for field_name in self._aristaproto.meta_by_field_name: + self_val = self.__raw_get(field_name) + other_val = other.__raw_get(field_name) + if self_val is PLACEHOLDER: + if other_val is PLACEHOLDER: + continue + self_val = self._get_field_default(field_name) + elif other_val is PLACEHOLDER: + other_val = other._get_field_default(field_name) + + if self_val != other_val: + # We consider two nan values to be the same for the + # purposes of comparing messages (otherwise a message + # is not equal to itself) + if ( + isinstance(self_val, float) + and isinstance(other_val, float) + and math.isnan(self_val) + and math.isnan(other_val) + ): + continue + else: + return False + + return True + + def __repr__(self) -> str: + parts = [ + f"{field_name}={value!r}" + for field_name in self._aristaproto.sorted_field_names + for value in (self.__raw_get(field_name),) + if value is not PLACEHOLDER + ] + return f"{self.__class__.__name__}({', '.join(parts)})" + + def __rich_repr__(self) -> Iterable[Tuple[str, Any, Any]]: + for field_name in self._aristaproto.sorted_field_names: + yield field_name, self.__raw_get(field_name), PLACEHOLDER + + if not TYPE_CHECKING: + + def __getattribute__(self, name: str) -> Any: + """ + Lazily initialize default values to avoid infinite recursion for recursive + message types. + Raise :class:`AttributeError` on attempts to access unset ``oneof`` fields. + """ + try: + group_current = super().__getattribute__("_group_current") + except AttributeError: + pass + else: + if name not in {"__class__", "_aristaproto"}: + group = self._aristaproto.oneof_group_by_field.get(name) + if group is not None and group_current[group] != name: + if sys.version_info < (3, 10): + raise AttributeError( + f"{group!r} is set to {group_current[group]!r}, not {name!r}" + ) + else: + raise AttributeError( + f"{group!r} is set to {group_current[group]!r}, not {name!r}", + name=name, + obj=self, + ) + + value = super().__getattribute__(name) + if value is not PLACEHOLDER: + return value + + value = self._get_field_default(name) + super().__setattr__(name, value) + return value + + def __setattr__(self, attr: str, value: Any) -> None: + if ( + isinstance(value, Message) + and hasattr(value, "_aristaproto") + and not value._aristaproto.meta_by_field_name + ): + value._serialized_on_wire = True + + if attr != "_serialized_on_wire": + # Track when a field has been set. + self.__dict__["_serialized_on_wire"] = True + + if hasattr(self, "_group_current"): # __post_init__ had already run + if attr in self._aristaproto.oneof_group_by_field: + group = self._aristaproto.oneof_group_by_field[attr] + for field in self._aristaproto.oneof_field_by_group[group]: + if field.name == attr: + self._group_current[group] = field.name + else: + super().__setattr__(field.name, PLACEHOLDER) + + super().__setattr__(attr, value) + + def __bool__(self) -> bool: + """True if the Message has any fields with non-default values.""" + return any( + self.__raw_get(field_name) + not in (PLACEHOLDER, self._get_field_default(field_name)) + for field_name in self._aristaproto.meta_by_field_name + ) + + def __deepcopy__(self: T, _: Any = {}) -> T: + kwargs = {} + for name in self._aristaproto.sorted_field_names: + value = self.__raw_get(name) + if value is not PLACEHOLDER: + kwargs[name] = deepcopy(value) + return self.__class__(**kwargs) # type: ignore + + def __copy__(self: T, _: Any = {}) -> T: + kwargs = {} + for name in self._aristaproto.sorted_field_names: + value = self.__raw_get(name) + if value is not PLACEHOLDER: + kwargs[name] = value + return self.__class__(**kwargs) # type: ignore + + @classproperty + def _aristaproto(cls: type[Self]) -> ProtoClassMetadata: # type: ignore + """ + Lazy initialize metadata for each protobuf class. + It may be initialized multiple times in a multi-threaded environment, + but that won't affect the correctness. + """ + try: + return cls._aristaproto_meta + except AttributeError: + cls._aristaproto_meta = meta = ProtoClassMetadata(cls) + return meta + + def dump(self, stream: "SupportsWrite[bytes]", delimit: bool = False) -> None: + """ + Dumps the binary encoded Protobuf message to the stream. + + Parameters + ----------- + stream: :class:`BinaryIO` + The stream to dump the message to. + delimit: + Whether to prefix the message with a varint declaring its size. + """ + if delimit == SIZE_DELIMITED: + dump_varint(len(self), stream) + + for field_name, meta in self._aristaproto.meta_by_field_name.items(): + try: + value = getattr(self, field_name) + except AttributeError: + continue + + if value is None: + # Optional items should be skipped. This is used for the Google + # wrapper types and proto3 field presence/optional fields. + continue + + # Being selected in a a group means this field is the one that is + # currently set in a `oneof` group, so it must be serialized even + # if the value is the default zero value. + # + # Note that proto3 field presence/optional fields are put in a + # synthetic single-item oneof by protoc, which helps us ensure we + # send the value even if the value is the default zero value. + selected_in_group = bool(meta.group) or meta.optional + + # Empty messages can still be sent on the wire if they were + # set (or received empty). + serialize_empty = isinstance(value, Message) and value._serialized_on_wire + + include_default_value_for_oneof = self._include_default_value_for_oneof( + field_name=field_name, meta=meta + ) + + if value == self._get_field_default(field_name) and not ( + selected_in_group or serialize_empty or include_default_value_for_oneof + ): + # Default (zero) values are not serialized. Two exceptions are + # if this is the selected oneof item or if we know we have to + # serialize an empty message (i.e. zero value was explicitly + # set by the user). + continue + + if isinstance(value, list): + if meta.proto_type in PACKED_TYPES: + # Packed lists look like a length-delimited field. First, + # preprocess/encode each value into a buffer and then + # treat it like a field of raw bytes. + buf = bytearray() + for item in value: + buf += _preprocess_single(meta.proto_type, "", item) + stream.write(_serialize_single(meta.number, TYPE_BYTES, buf)) + else: + for item in value: + stream.write( + _serialize_single( + meta.number, + meta.proto_type, + item, + wraps=meta.wraps or "", + serialize_empty=True, + ) + # if it's an empty message it still needs to be represented + # as an item in the repeated list + or b"\n\x00" + ) + + elif isinstance(value, dict): + for k, v in value.items(): + assert meta.map_types + sk = _serialize_single(1, meta.map_types[0], k) + sv = _serialize_single(2, meta.map_types[1], v) + stream.write( + _serialize_single(meta.number, meta.proto_type, sk + sv) + ) + else: + # If we have an empty string and we're including the default value for + # a oneof, make sure we serialize it. This ensures that the byte string + # output isn't simply an empty string. This also ensures that round trip + # serialization will keep `which_one_of` calls consistent. + if ( + isinstance(value, str) + and value == "" + and include_default_value_for_oneof + ): + serialize_empty = True + + stream.write( + _serialize_single( + meta.number, + meta.proto_type, + value, + serialize_empty=serialize_empty or bool(selected_in_group), + wraps=meta.wraps or "", + ) + ) + + stream.write(self._unknown_fields) + + def __bytes__(self) -> bytes: + """ + Get the binary encoded Protobuf representation of this message instance. + """ + with BytesIO() as stream: + self.dump(stream) + return stream.getvalue() + + def __len__(self) -> int: + """ + Get the size of the encoded Protobuf representation of this message instance. + """ + size = 0 + for field_name, meta in self._aristaproto.meta_by_field_name.items(): + try: + value = getattr(self, field_name) + except AttributeError: + continue + + if value is None: + # Optional items should be skipped. This is used for the Google + # wrapper types and proto3 field presence/optional fields. + continue + + # Being selected in a group means this field is the one that is + # currently set in a `oneof` group, so it must be serialized even + # if the value is the default zero value. + # + # Note that proto3 field presence/optional fields are put in a + # synthetic single-item oneof by protoc, which helps us ensure we + # send the value even if the value is the default zero value. + selected_in_group = bool(meta.group) + + # Empty messages can still be sent on the wire if they were + # set (or received empty). + serialize_empty = isinstance(value, Message) and value._serialized_on_wire + + include_default_value_for_oneof = self._include_default_value_for_oneof( + field_name=field_name, meta=meta + ) + + if value == self._get_field_default(field_name) and not ( + selected_in_group or serialize_empty or include_default_value_for_oneof + ): + # Default (zero) values are not serialized. Two exceptions are + # if this is the selected oneof item or if we know we have to + # serialize an empty message (i.e. zero value was explicitly + # set by the user). + continue + + if isinstance(value, list): + if meta.proto_type in PACKED_TYPES: + # Packed lists look like a length-delimited field. First, + # preprocess/encode each value into a buffer and then + # treat it like a field of raw bytes. + buf = bytearray() + for item in value: + buf += _preprocess_single(meta.proto_type, "", item) + size += _len_single(meta.number, TYPE_BYTES, buf) + else: + for item in value: + size += ( + _len_single( + meta.number, + meta.proto_type, + item, + wraps=meta.wraps or "", + serialize_empty=True, + ) + # if it's an empty message it still needs to be represented + # as an item in the repeated list + or 2 + ) + + elif isinstance(value, dict): + for k, v in value.items(): + assert meta.map_types + sk = _serialize_single(1, meta.map_types[0], k) + sv = _serialize_single(2, meta.map_types[1], v) + size += _len_single(meta.number, meta.proto_type, sk + sv) + else: + # If we have an empty string and we're including the default value for + # a oneof, make sure we serialize it. This ensures that the byte string + # output isn't simply an empty string. This also ensures that round trip + # serialization will keep `which_one_of` calls consistent. + if ( + isinstance(value, str) + and value == "" + and include_default_value_for_oneof + ): + serialize_empty = True + + size += _len_single( + meta.number, + meta.proto_type, + value, + serialize_empty=serialize_empty or bool(selected_in_group), + wraps=meta.wraps or "", + ) + + size += len(self._unknown_fields) + return size + + # For compatibility with other libraries + def SerializeToString(self: T) -> bytes: + """ + Get the binary encoded Protobuf representation of this message instance. + + .. note:: + This is a method for compatibility with other libraries, + you should really use ``bytes(x)``. + + Returns + -------- + :class:`bytes` + The binary encoded Protobuf representation of this message instance + """ + return bytes(self) + + def __getstate__(self) -> bytes: + return bytes(self) + + def __setstate__(self: T, pickled_bytes: bytes) -> T: + return self.parse(pickled_bytes) + + def __reduce__(self) -> Tuple[Any, ...]: + return (self.__class__.FromString, (bytes(self),)) + + @classmethod + def _type_hint(cls, field_name: str) -> Type: + return cls._type_hints()[field_name] + + @classmethod + def _type_hints(cls) -> Dict[str, Type]: + module = sys.modules[cls.__module__] + return get_type_hints(cls, module.__dict__, {}) + + @classmethod + def _cls_for(cls, field: dataclasses.Field, index: int = 0) -> Type: + """Get the message class for a field from the type hints.""" + field_cls = cls._type_hint(field.name) + if hasattr(field_cls, "__args__") and index >= 0: + if field_cls.__args__ is not None: + field_cls = field_cls.__args__[index] + return field_cls + + def _get_field_default(self, field_name: str) -> Any: + with warnings.catch_warnings(): + # ignore warnings when initialising deprecated field defaults + warnings.filterwarnings("ignore", category=DeprecationWarning) + return self._aristaproto.default_gen[field_name]() + + @classmethod + def _get_field_default_gen(cls, field: dataclasses.Field) -> Any: + t = cls._type_hint(field.name) + + if hasattr(t, "__origin__"): + if t.__origin__ is dict: + # This is some kind of map (dict in Python). + return dict + elif t.__origin__ is list: + # This is some kind of list (repeated) field. + return list + elif t.__origin__ is Union and t.__args__[1] is type(None): + # This is an optional field (either wrapped, or using proto3 + # field presence). For setting the default we really don't care + # what kind of field it is. + return type(None) + else: + return t + elif issubclass(t, Enum): + # Enums always default to zero. + return t.try_value + elif t is datetime: + # Offsets are relative to 1970-01-01T00:00:00Z + return datetime_default_gen + else: + # This is either a primitive scalar or another message type. Calling + # it should result in its zero value. + return t + + def _postprocess_single( + self, wire_type: int, meta: FieldMetadata, field_name: str, value: Any + ) -> Any: + """Adjusts values after parsing.""" + if wire_type == WIRE_VARINT: + if meta.proto_type in (TYPE_INT32, TYPE_INT64): + bits = int(meta.proto_type[3:]) + value = value & ((1 << bits) - 1) + signbit = 1 << (bits - 1) + value = int((value ^ signbit) - signbit) + elif meta.proto_type in (TYPE_SINT32, TYPE_SINT64): + # Undo zig-zag encoding + value = (value >> 1) ^ (-(value & 1)) + elif meta.proto_type == TYPE_BOOL: + # Booleans use a varint encoding, so convert it to true/false. + value = value > 0 + elif meta.proto_type == TYPE_ENUM: + # Convert enum ints to python enum instances + value = self._aristaproto.cls_by_field[field_name].try_value(value) + elif wire_type in (WIRE_FIXED_32, WIRE_FIXED_64): + fmt = _pack_fmt(meta.proto_type) + value = struct.unpack(fmt, value)[0] + elif wire_type == WIRE_LEN_DELIM: + if meta.proto_type == TYPE_STRING: + value = str(value, "utf-8") + elif meta.proto_type == TYPE_MESSAGE: + cls = self._aristaproto.cls_by_field[field_name] + + if cls == datetime: + value = _Timestamp().parse(value).to_datetime() + elif cls == timedelta: + value = _Duration().parse(value).to_timedelta() + elif meta.wraps: + # This is a Google wrapper value message around a single + # scalar type. + value = _get_wrapper(meta.wraps)().parse(value).value + else: + value = cls().parse(value) + value._serialized_on_wire = True + elif meta.proto_type == TYPE_MAP: + value = self._aristaproto.cls_by_field[field_name]().parse(value) + + return value + + def _include_default_value_for_oneof( + self, field_name: str, meta: FieldMetadata + ) -> bool: + return ( + meta.group is not None and self._group_current.get(meta.group) == field_name + ) + + def load( + self: T, + stream: "SupportsRead[bytes]", + size: Optional[int] = None, + ) -> T: + """ + Load the binary encoded Protobuf from a stream into this message instance. This + returns the instance itself and is therefore assignable and chainable. + + Parameters + ----------- + stream: :class:`bytes` + The stream to load the message from. + size: :class:`Optional[int]` + The size of the message in the stream. + Reads stream until EOF if ``None`` is given. + Reads based on a size delimiter prefix varint if SIZE_DELIMITED is given. + + Returns + -------- + :class:`Message` + The initialized message. + """ + # If the message is delimited, parse the message delimiter + if size == SIZE_DELIMITED: + size, _ = load_varint(stream) + + # Got some data over the wire + self._serialized_on_wire = True + proto_meta = self._aristaproto + read = 0 + for parsed in load_fields(stream): + field_name = proto_meta.field_name_by_number.get(parsed.number) + if not field_name: + self._unknown_fields += parsed.raw + continue + + meta = proto_meta.meta_by_field_name[field_name] + + value: Any + if parsed.wire_type == WIRE_LEN_DELIM and meta.proto_type in PACKED_TYPES: + # This is a packed repeated field. + pos = 0 + value = [] + while pos < len(parsed.value): + if meta.proto_type in (TYPE_FLOAT, TYPE_FIXED32, TYPE_SFIXED32): + decoded, pos = parsed.value[pos : pos + 4], pos + 4 + wire_type = WIRE_FIXED_32 + elif meta.proto_type in (TYPE_DOUBLE, TYPE_FIXED64, TYPE_SFIXED64): + decoded, pos = parsed.value[pos : pos + 8], pos + 8 + wire_type = WIRE_FIXED_64 + else: + decoded, pos = decode_varint(parsed.value, pos) + wire_type = WIRE_VARINT + decoded = self._postprocess_single( + wire_type, meta, field_name, decoded + ) + value.append(decoded) + else: + value = self._postprocess_single( + parsed.wire_type, meta, field_name, parsed.value + ) + + try: + current = getattr(self, field_name) + except AttributeError: + current = self._get_field_default(field_name) + setattr(self, field_name, current) + + if meta.proto_type == TYPE_MAP: + # Value represents a single key/value pair entry in the map. + current[value.key] = value.value + elif isinstance(current, list) and not isinstance(value, list): + current.append(value) + else: + setattr(self, field_name, value) + + # If we have now loaded the expected length of the message, stop + if size is not None: + prev = read + read += len(parsed.raw) + if read == size: + break + elif read > size: + raise ValueError( + f"Expected message of size {size}, can only read " + f"either {prev} or {read} bytes - there is no " + "message of the expected size in the stream." + ) + + if size is not None and read < size: + raise ValueError( + f"Expected message of size {size}, but was only able to " + f"read {read} bytes - the stream may have ended too soon," + " or the expected size may have been incorrect." + ) + + return self + + def parse(self: T, data: bytes) -> T: + """ + Parse the binary encoded Protobuf into this message instance. This + returns the instance itself and is therefore assignable and chainable. + + Parameters + ----------- + data: :class:`bytes` + The data to parse the message from. + + Returns + -------- + :class:`Message` + The initialized message. + """ + with BytesIO(data) as stream: + return self.load(stream) + + # For compatibility with other libraries. + @classmethod + def FromString(cls: Type[T], data: bytes) -> T: + """ + Parse the binary encoded Protobuf into this message instance. This + returns the instance itself and is therefore assignable and chainable. + + .. note:: + This is a method for compatibility with other libraries, + you should really use :meth:`parse`. + + + Parameters + ----------- + data: :class:`bytes` + The data to parse the protobuf from. + + Returns + -------- + :class:`Message` + The initialized message. + """ + return cls().parse(data) + + def to_dict( + self, casing: Casing = Casing.CAMEL, include_default_values: bool = False + ) -> Dict[str, Any]: + """ + Returns a JSON serializable dict representation of this object. + + Parameters + ----------- + casing: :class:`Casing` + The casing to use for key values. Default is :attr:`Casing.CAMEL` for + compatibility purposes. + include_default_values: :class:`bool` + If ``True`` will include the default values of fields. Default is ``False``. + E.g. an ``int32`` field will be included with a value of ``0`` if this is + set to ``True``, otherwise this would be ignored. + + Returns + -------- + Dict[:class:`str`, Any] + The JSON serializable dict representation of this object. + """ + output: Dict[str, Any] = {} + field_types = self._type_hints() + defaults = self._aristaproto.default_gen + for field_name, meta in self._aristaproto.meta_by_field_name.items(): + field_is_repeated = defaults[field_name] is list + try: + value = getattr(self, field_name) + except AttributeError: + value = self._get_field_default(field_name) + cased_name = casing(field_name).rstrip("_") # type: ignore + if meta.proto_type == TYPE_MESSAGE: + if isinstance(value, datetime): + if ( + value != DATETIME_ZERO + or include_default_values + or self._include_default_value_for_oneof( + field_name=field_name, meta=meta + ) + ): + output[cased_name] = _Timestamp.timestamp_to_json(value) + elif isinstance(value, timedelta): + if ( + value != timedelta(0) + or include_default_values + or self._include_default_value_for_oneof( + field_name=field_name, meta=meta + ) + ): + output[cased_name] = _Duration.delta_to_json(value) + elif meta.wraps: + if value is not None or include_default_values: + output[cased_name] = value + elif field_is_repeated: + # Convert each item. + cls = self._aristaproto.cls_by_field[field_name] + if cls == datetime: + value = [_Timestamp.timestamp_to_json(i) for i in value] + elif cls == timedelta: + value = [_Duration.delta_to_json(i) for i in value] + else: + value = [ + i.to_dict(casing, include_default_values) for i in value + ] + if value or include_default_values: + output[cased_name] = value + elif value is None: + if include_default_values: + output[cased_name] = value + elif ( + value._serialized_on_wire + or include_default_values + or self._include_default_value_for_oneof( + field_name=field_name, meta=meta + ) + ): + output[cased_name] = value.to_dict(casing, include_default_values) + elif meta.proto_type == TYPE_MAP: + output_map = {**value} + for k in value: + if hasattr(value[k], "to_dict"): + output_map[k] = value[k].to_dict(casing, include_default_values) + + if value or include_default_values: + output[cased_name] = output_map + elif ( + value != self._get_field_default(field_name) + or include_default_values + or self._include_default_value_for_oneof( + field_name=field_name, meta=meta + ) + ): + if meta.proto_type in INT_64_TYPES: + if field_is_repeated: + output[cased_name] = [str(n) for n in value] + elif value is None: + if include_default_values: + output[cased_name] = value + else: + output[cased_name] = str(value) + elif meta.proto_type == TYPE_BYTES: + if field_is_repeated: + output[cased_name] = [ + b64encode(b).decode("utf8") for b in value + ] + elif value is None and include_default_values: + output[cased_name] = value + else: + output[cased_name] = b64encode(value).decode("utf8") + elif meta.proto_type == TYPE_ENUM: + if field_is_repeated: + enum_class = field_types[field_name].__args__[0] + if isinstance(value, typing.Iterable) and not isinstance( + value, str + ): + output[cased_name] = [enum_class(el).name for el in value] + else: + # transparently upgrade single value to repeated + output[cased_name] = [enum_class(value).name] + elif value is None: + if include_default_values: + output[cased_name] = value + elif meta.optional: + enum_class = field_types[field_name].__args__[0] + output[cased_name] = enum_class(value).name + else: + enum_class = field_types[field_name] # noqa + output[cased_name] = enum_class(value).name + elif meta.proto_type in (TYPE_FLOAT, TYPE_DOUBLE): + if field_is_repeated: + output[cased_name] = [_dump_float(n) for n in value] + else: + output[cased_name] = _dump_float(value) + else: + output[cased_name] = value + return output + + @classmethod + def _from_dict_init(cls, mapping: Mapping[str, Any]) -> Mapping[str, Any]: + init_kwargs: Dict[str, Any] = {} + for key, value in mapping.items(): + field_name = safe_snake_case(key) + try: + meta = cls._aristaproto.meta_by_field_name[field_name] + except KeyError: + continue + if value is None: + continue + + if meta.proto_type == TYPE_MESSAGE: + sub_cls = cls._aristaproto.cls_by_field[field_name] + if sub_cls == datetime: + value = ( + [isoparse(item) for item in value] + if isinstance(value, list) + else isoparse(value) + ) + elif sub_cls == timedelta: + value = ( + [timedelta(seconds=float(item[:-1])) for item in value] + if isinstance(value, list) + else timedelta(seconds=float(value[:-1])) + ) + elif not meta.wraps: + value = ( + [sub_cls.from_dict(item) for item in value] + if isinstance(value, list) + else sub_cls.from_dict(value) + ) + elif meta.map_types and meta.map_types[1] == TYPE_MESSAGE: + sub_cls = cls._aristaproto.cls_by_field[f"{field_name}.value"] + value = {k: sub_cls.from_dict(v) for k, v in value.items()} + else: + if meta.proto_type in INT_64_TYPES: + value = ( + [int(n) for n in value] + if isinstance(value, list) + else int(value) + ) + elif meta.proto_type == TYPE_BYTES: + value = ( + [b64decode(n) for n in value] + if isinstance(value, list) + else b64decode(value) + ) + elif meta.proto_type == TYPE_ENUM: + enum_cls = cls._aristaproto.cls_by_field[field_name] + if isinstance(value, list): + value = [enum_cls.from_string(e) for e in value] + elif isinstance(value, str): + value = enum_cls.from_string(value) + elif meta.proto_type in (TYPE_FLOAT, TYPE_DOUBLE): + value = ( + [_parse_float(n) for n in value] + if isinstance(value, list) + else _parse_float(value) + ) + + init_kwargs[field_name] = value + return init_kwargs + + @hybridmethod + def from_dict(cls: type[Self], value: Mapping[str, Any]) -> Self: # type: ignore + """ + Parse the key/value pairs into the a new message instance. + + Parameters + ----------- + value: Dict[:class:`str`, Any] + The dictionary to parse from. + + Returns + -------- + :class:`Message` + The initialized message. + """ + self = cls(**cls._from_dict_init(value)) + self._serialized_on_wire = True + return self + + @from_dict.instancemethod + def from_dict(self, value: Mapping[str, Any]) -> Self: + """ + Parse the key/value pairs into the current message instance. This returns the + instance itself and is therefore assignable and chainable. + + Parameters + ----------- + value: Dict[:class:`str`, Any] + The dictionary to parse from. + + Returns + -------- + :class:`Message` + The initialized message. + """ + self._serialized_on_wire = True + for field, value in self._from_dict_init(value).items(): + setattr(self, field, value) + return self + + def to_json( + self, + indent: Union[None, int, str] = None, + include_default_values: bool = False, + casing: Casing = Casing.CAMEL, + ) -> str: + """A helper function to parse the message instance into its JSON + representation. + + This is equivalent to:: + + json.dumps(message.to_dict(), indent=indent) + + Parameters + ----------- + indent: Optional[Union[:class:`int`, :class:`str`]] + The indent to pass to :func:`json.dumps`. + + include_default_values: :class:`bool` + If ``True`` will include the default values of fields. Default is ``False``. + E.g. an ``int32`` field will be included with a value of ``0`` if this is + set to ``True``, otherwise this would be ignored. + + casing: :class:`Casing` + The casing to use for key values. Default is :attr:`Casing.CAMEL` for + compatibility purposes. + + Returns + -------- + :class:`str` + The JSON representation of the message. + """ + return json.dumps( + self.to_dict(include_default_values=include_default_values, casing=casing), + indent=indent, + ) + + def from_json(self: T, value: Union[str, bytes]) -> T: + """A helper function to return the message instance from its JSON + representation. This returns the instance itself and is therefore assignable + and chainable. + + This is equivalent to:: + + return message.from_dict(json.loads(value)) + + Parameters + ----------- + value: Union[:class:`str`, :class:`bytes`] + The value to pass to :func:`json.loads`. + + Returns + -------- + :class:`Message` + The initialized message. + """ + return self.from_dict(json.loads(value)) + + def to_pydict( + self, casing: Casing = Casing.CAMEL, include_default_values: bool = False + ) -> Dict[str, Any]: + """ + Returns a python dict representation of this object. + + Parameters + ----------- + casing: :class:`Casing` + The casing to use for key values. Default is :attr:`Casing.CAMEL` for + compatibility purposes. + include_default_values: :class:`bool` + If ``True`` will include the default values of fields. Default is ``False``. + E.g. an ``int32`` field will be included with a value of ``0`` if this is + set to ``True``, otherwise this would be ignored. + + Returns + -------- + Dict[:class:`str`, Any] + The python dict representation of this object. + """ + output: Dict[str, Any] = {} + defaults = self._aristaproto.default_gen + for field_name, meta in self._aristaproto.meta_by_field_name.items(): + field_is_repeated = defaults[field_name] is list + value = getattr(self, field_name) + cased_name = casing(field_name).rstrip("_") # type: ignore + if meta.proto_type == TYPE_MESSAGE: + if isinstance(value, datetime): + if ( + value != DATETIME_ZERO + or include_default_values + or self._include_default_value_for_oneof( + field_name=field_name, meta=meta + ) + ): + output[cased_name] = value + elif isinstance(value, timedelta): + if ( + value != timedelta(0) + or include_default_values + or self._include_default_value_for_oneof( + field_name=field_name, meta=meta + ) + ): + output[cased_name] = value + elif meta.wraps: + if value is not None or include_default_values: + output[cased_name] = value + elif field_is_repeated: + # Convert each item. + value = [i.to_pydict(casing, include_default_values) for i in value] + if value or include_default_values: + output[cased_name] = value + elif value is None: + if include_default_values: + output[cased_name] = None + elif ( + value._serialized_on_wire + or include_default_values + or self._include_default_value_for_oneof( + field_name=field_name, meta=meta + ) + ): + output[cased_name] = value.to_pydict(casing, include_default_values) + elif meta.proto_type == TYPE_MAP: + for k in value: + if hasattr(value[k], "to_pydict"): + value[k] = value[k].to_pydict(casing, include_default_values) + + if value or include_default_values: + output[cased_name] = value + elif ( + value != self._get_field_default(field_name) + or include_default_values + or self._include_default_value_for_oneof( + field_name=field_name, meta=meta + ) + ): + output[cased_name] = value + return output + + def from_pydict(self: T, value: Mapping[str, Any]) -> T: + """ + Parse the key/value pairs into the current message instance. This returns the + instance itself and is therefore assignable and chainable. + + Parameters + ----------- + value: Dict[:class:`str`, Any] + The dictionary to parse from. + + Returns + -------- + :class:`Message` + The initialized message. + """ + self._serialized_on_wire = True + for key in value: + field_name = safe_snake_case(key) + meta = self._aristaproto.meta_by_field_name.get(field_name) + if not meta: + continue + + if value[key] is not None: + if meta.proto_type == TYPE_MESSAGE: + v = getattr(self, field_name) + if isinstance(v, list): + cls = self._aristaproto.cls_by_field[field_name] + for item in value[key]: + v.append(cls().from_pydict(item)) + elif isinstance(v, datetime): + v = value[key] + elif isinstance(v, timedelta): + v = value[key] + elif meta.wraps: + v = value[key] + else: + # NOTE: `from_pydict` mutates the underlying message, so no + # assignment here is necessary. + v.from_pydict(value[key]) + elif meta.map_types and meta.map_types[1] == TYPE_MESSAGE: + v = getattr(self, field_name) + cls = self._aristaproto.cls_by_field[f"{field_name}.value"] + for k in value[key]: + v[k] = cls().from_pydict(value[key][k]) + else: + v = value[key] + + if v is not None: + setattr(self, field_name, v) + return self + + def is_set(self, name: str) -> bool: + """ + Check if field with the given name has been set. + + Parameters + ----------- + name: :class:`str` + The name of the field to check for. + + Returns + -------- + :class:`bool` + `True` if field has been set, otherwise `False`. + """ + default = ( + PLACEHOLDER + if not self._aristaproto.meta_by_field_name[name].optional + else None + ) + return self.__raw_get(name) is not default + + @classmethod + def _validate_field_groups(cls, values): + group_to_one_ofs = cls._aristaproto.oneof_field_by_group + field_name_to_meta = cls._aristaproto.meta_by_field_name + + for group, field_set in group_to_one_ofs.items(): + if len(field_set) == 1: + (field,) = field_set + field_name = field.name + meta = field_name_to_meta[field_name] + + # This is a synthetic oneof; we should ignore it's presence and not consider it as a oneof. + if meta.optional: + continue + + set_fields = [ + field.name for field in field_set if values[field.name] is not None + ] + + if not set_fields: + raise ValueError(f"Group {group} has no value; all fields are None") + elif len(set_fields) > 1: + set_fields_str = ", ".join(set_fields) + raise ValueError( + f"Group {group} has more than one value; fields {set_fields_str} are not None" + ) + + return values + + +Message.__annotations__ = {} # HACK to avoid typing.get_type_hints breaking :) + +# monkey patch (de-)serialization functions of class `Message` +# with functions from `betterproto-rust-codec` if available +try: + import betterproto_rust_codec + + def __parse_patch(self: T, data: bytes) -> T: + betterproto_rust_codec.deserialize(self, data) + return self + + def __bytes_patch(self) -> bytes: + return betterproto_rust_codec.serialize(self) + + Message.parse = __parse_patch + Message.__bytes__ = __bytes_patch +except ModuleNotFoundError: + pass + + +def serialized_on_wire(message: Message) -> bool: + """ + If this message was or should be serialized on the wire. This can be used to detect + presence (e.g. optional wrapper message) and is used internally during + parsing/serialization. + + Returns + -------- + :class:`bool` + Whether this message was or should be serialized on the wire. + """ + return message._serialized_on_wire + + +def which_one_of(message: Message, group_name: str) -> Tuple[str, Optional[Any]]: + """ + Return the name and value of a message's one-of field group. + + Returns + -------- + Tuple[:class:`str`, Any] + The field name and the value for that field. + """ + field_name = message._group_current.get(group_name) + if not field_name: + return "", None + return field_name, getattr(message, field_name) + + +# Circular import workaround: google.protobuf depends on base classes defined above. +from .lib.google.protobuf import ( # noqa + BoolValue, + BytesValue, + DoubleValue, + Duration, + EnumValue, + FloatValue, + Int32Value, + Int64Value, + StringValue, + Timestamp, + UInt32Value, + UInt64Value, +) + + +class _Duration(Duration): + @classmethod + def from_timedelta( + cls, delta: timedelta, *, _1_microsecond: timedelta = timedelta(microseconds=1) + ) -> "_Duration": + total_ms = delta // _1_microsecond + seconds = int(total_ms / 1e6) + nanos = int((total_ms % 1e6) * 1e3) + return cls(seconds, nanos) + + def to_timedelta(self) -> timedelta: + return timedelta(seconds=self.seconds, microseconds=self.nanos / 1e3) + + @staticmethod + def delta_to_json(delta: timedelta) -> str: + parts = str(delta.total_seconds()).split(".") + if len(parts) > 1: + while len(parts[1]) not in (3, 6, 9): + parts[1] = f"{parts[1]}0" + return f"{'.'.join(parts)}s" + + +class _Timestamp(Timestamp): + @classmethod + def from_datetime(cls, dt: datetime) -> "_Timestamp": + # manual epoch offset calulation to avoid rounding errors, + # to support negative timestamps (before 1970) and skirt + # around datetime bugs (apparently 0 isn't a year in [0, 9999]??) + offset = dt - DATETIME_ZERO + # below is the same as timedelta.total_seconds() but without dividing by 1e6 + # so we end up with microseconds as integers instead of seconds as float + offset_us = ( + offset.days * 24 * 60 * 60 + offset.seconds + ) * 10**6 + offset.microseconds + seconds, us = divmod(offset_us, 10**6) + # If ths given datetime is our subclass containing nanos from the original Timestamp + # We will prefer those nanos over the datetime micros. + if isinstance(dt, _DateTime) and dt.nanos: + return cls(seconds, dt.nanos) + return cls(seconds, us * 1000) + + def to_datetime(self) -> _DateTime: + # datetime.fromtimestamp() expects a timestamp in seconds, not microseconds + # if we pass it as a floating point number, we will run into rounding errors + # see also #407 + offset = timedelta(seconds=self.seconds, microseconds=self.nanos // 1000) + dt = DATETIME_ZERO + offset + # Store the original nanos in our subclass of datetime. + setattr(dt, "_nanos", self.nanos) + return dt + + @staticmethod + def timestamp_to_json(dt: datetime) -> str: + # If ths given datetime is our subclass containing nanos from the original Timestamp + # We will prefer those nanos over the datetime micros. + if isinstance(dt, _DateTime) and dt.nanos: + nanos = dt.nanos + else: + nanos = dt.microsecond * 1e3 + if dt.tzinfo is not None: + # change timezone aware datetime objects to utc + dt = dt.astimezone(timezone.utc) + copy = dt.replace(microsecond=0, tzinfo=None) + result = copy.isoformat() + if (nanos % 1e9) == 0: + # If there are 0 fractional digits, the fractional + # point '.' should be omitted when serializing. + return f"{result}Z" + if (nanos % 1e6) == 0: + # Serialize 3 fractional digits. + return f"{result}.{int(nanos // 1e6) :03d}Z" + if (nanos % 1e3) == 0: + # Serialize 6 fractional digits. + return f"{result}.{int(nanos // 1e3) :06d}Z" + # Serialize 9 fractional digits. + return f"{result}.{nanos:09d}" + + +def _get_wrapper(proto_type: str) -> Type: + """Get the wrapper message class for a wrapped type.""" + + # TODO: include ListValue and NullValue? + return { + TYPE_BOOL: BoolValue, + TYPE_BYTES: BytesValue, + TYPE_DOUBLE: DoubleValue, + TYPE_FLOAT: FloatValue, + TYPE_ENUM: EnumValue, + TYPE_INT32: Int32Value, + TYPE_INT64: Int64Value, + TYPE_STRING: StringValue, + TYPE_UINT32: UInt32Value, + TYPE_UINT64: UInt64Value, + }[proto_type] diff --git a/src/aristaproto/_types.py b/src/aristaproto/_types.py new file mode 100644 index 0000000..616d550 --- /dev/null +++ b/src/aristaproto/_types.py @@ -0,0 +1,14 @@ +from typing import ( + TYPE_CHECKING, + TypeVar, +) + + +if TYPE_CHECKING: + from grpclib._typing import IProtoMessage + + from . import Message + +# Bound type variable to allow methods to return `self` of subclasses +T = TypeVar("T", bound="Message") +ST = TypeVar("ST", bound="IProtoMessage") diff --git a/src/aristaproto/_version.py b/src/aristaproto/_version.py new file mode 100644 index 0000000..347a391 --- /dev/null +++ b/src/aristaproto/_version.py @@ -0,0 +1,4 @@ +from importlib import metadata + + +__version__ = metadata.version("aristaproto") diff --git a/src/aristaproto/casing.py b/src/aristaproto/casing.py new file mode 100644 index 0000000..f7d0832 --- /dev/null +++ b/src/aristaproto/casing.py @@ -0,0 +1,143 @@ +import keyword +import re + + +# Word delimiters and symbols that will not be preserved when re-casing. +# language=PythonRegExp +SYMBOLS = "[^a-zA-Z0-9]*" + +# Optionally capitalized word. +# language=PythonRegExp +WORD = "[A-Z]*[a-z]*[0-9]*" + +# Uppercase word, not followed by lowercase letters. +# language=PythonRegExp +WORD_UPPER = "[A-Z]+(?![a-z])[0-9]*" + + +def safe_snake_case(value: str) -> str: + """Snake case a value taking into account Python keywords.""" + value = snake_case(value) + value = sanitize_name(value) + return value + + +def snake_case(value: str, strict: bool = True) -> str: + """ + Join words with an underscore into lowercase and remove symbols. + + Parameters + ----------- + value: :class:`str` + The value to convert. + strict: :class:`bool` + Whether or not to force single underscores. + + Returns + -------- + :class:`str` + The value in snake_case. + """ + + def substitute_word(symbols: str, word: str, is_start: bool) -> str: + if not word: + return "" + if strict: + delimiter_count = 0 if is_start else 1 # Single underscore if strict. + elif is_start: + delimiter_count = len(symbols) + elif word.isupper() or word.islower(): + delimiter_count = max( + 1, len(symbols) + ) # Preserve all delimiters if not strict. + else: + delimiter_count = len(symbols) + 1 # Extra underscore for leading capital. + + return ("_" * delimiter_count) + word.lower() + + snake = re.sub( + f"(^)?({SYMBOLS})({WORD_UPPER}|{WORD})", + lambda groups: substitute_word(groups[2], groups[3], groups[1] is not None), + value, + ) + return snake + + +def pascal_case(value: str, strict: bool = True) -> str: + """ + Capitalize each word and remove symbols. + + Parameters + ----------- + value: :class:`str` + The value to convert. + strict: :class:`bool` + Whether or not to output only alphanumeric characters. + + Returns + -------- + :class:`str` + The value in PascalCase. + """ + + def substitute_word(symbols, word): + if strict: + return word.capitalize() # Remove all delimiters + + if word.islower(): + delimiter_length = len(symbols[:-1]) # Lose one delimiter + else: + delimiter_length = len(symbols) # Preserve all delimiters + + return ("_" * delimiter_length) + word.capitalize() + + return re.sub( + f"({SYMBOLS})({WORD_UPPER}|{WORD})", + lambda groups: substitute_word(groups[1], groups[2]), + value, + ) + + +def camel_case(value: str, strict: bool = True) -> str: + """ + Capitalize all words except first and remove symbols. + + Parameters + ----------- + value: :class:`str` + The value to convert. + strict: :class:`bool` + Whether or not to output only alphanumeric characters. + + Returns + -------- + :class:`str` + The value in camelCase. + """ + return lowercase_first(pascal_case(value, strict=strict)) + + +def lowercase_first(value: str) -> str: + """ + Lower cases the first character of the value. + + Parameters + ---------- + value: :class:`str` + The value to lower case. + + Returns + ------- + :class:`str` + The lower cased string. + """ + return value[0:1].lower() + value[1:] + + +def sanitize_name(value: str) -> str: + # https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles + if keyword.iskeyword(value): + return f"{value}_" + if not value.isidentifier(): + return f"_{value}" + return value diff --git a/src/aristaproto/compile/__init__.py b/src/aristaproto/compile/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/aristaproto/compile/__init__.py diff --git a/src/aristaproto/compile/importing.py b/src/aristaproto/compile/importing.py new file mode 100644 index 0000000..8486ddd --- /dev/null +++ b/src/aristaproto/compile/importing.py @@ -0,0 +1,176 @@ +import os +import re +from typing import ( + Dict, + List, + Set, + Tuple, + Type, +) + +from ..casing import safe_snake_case +from ..lib.google import protobuf as google_protobuf +from .naming import pythonize_class_name + + +WRAPPER_TYPES: Dict[str, Type] = { + ".google.protobuf.DoubleValue": google_protobuf.DoubleValue, + ".google.protobuf.FloatValue": google_protobuf.FloatValue, + ".google.protobuf.Int32Value": google_protobuf.Int32Value, + ".google.protobuf.Int64Value": google_protobuf.Int64Value, + ".google.protobuf.UInt32Value": google_protobuf.UInt32Value, + ".google.protobuf.UInt64Value": google_protobuf.UInt64Value, + ".google.protobuf.BoolValue": google_protobuf.BoolValue, + ".google.protobuf.StringValue": google_protobuf.StringValue, + ".google.protobuf.BytesValue": google_protobuf.BytesValue, +} + + +def parse_source_type_name(field_type_name: str) -> Tuple[str, str]: + """ + Split full source type name into package and type name. + E.g. 'root.package.Message' -> ('root.package', 'Message') + 'root.Message.SomeEnum' -> ('root', 'Message.SomeEnum') + """ + package_match = re.match(r"^\.?([^A-Z]+)\.(.+)", field_type_name) + if package_match: + package = package_match.group(1) + name = package_match.group(2) + else: + package = "" + name = field_type_name.lstrip(".") + return package, name + + +def get_type_reference( + *, + package: str, + imports: set, + source_type: str, + unwrap: bool = True, + pydantic: bool = False, +) -> str: + """ + Return a Python type name for a proto type reference. Adds the import if + necessary. Unwraps well known type if required. + """ + if unwrap: + if source_type in WRAPPER_TYPES: + wrapped_type = type(WRAPPER_TYPES[source_type]().value) + return f"Optional[{wrapped_type.__name__}]" + + if source_type == ".google.protobuf.Duration": + return "timedelta" + + elif source_type == ".google.protobuf.Timestamp": + return "datetime" + + source_package, source_type = parse_source_type_name(source_type) + + current_package: List[str] = package.split(".") if package else [] + py_package: List[str] = source_package.split(".") if source_package else [] + py_type: str = pythonize_class_name(source_type) + + compiling_google_protobuf = current_package == ["google", "protobuf"] + importing_google_protobuf = py_package == ["google", "protobuf"] + if importing_google_protobuf and not compiling_google_protobuf: + py_package = ( + ["aristaproto", "lib"] + (["pydantic"] if pydantic else []) + py_package + ) + + if py_package[:1] == ["aristaproto"]: + return reference_absolute(imports, py_package, py_type) + + if py_package == current_package: + return reference_sibling(py_type) + + if py_package[: len(current_package)] == current_package: + return reference_descendent(current_package, imports, py_package, py_type) + + if current_package[: len(py_package)] == py_package: + return reference_ancestor(current_package, imports, py_package, py_type) + + return reference_cousin(current_package, imports, py_package, py_type) + + +def reference_absolute(imports: Set[str], py_package: List[str], py_type: str) -> str: + """ + Returns a reference to a python type located in the root, i.e. sys.path. + """ + string_import = ".".join(py_package) + string_alias = safe_snake_case(string_import) + imports.add(f"import {string_import} as {string_alias}") + return f'"{string_alias}.{py_type}"' + + +def reference_sibling(py_type: str) -> str: + """ + Returns a reference to a python type within the same package as the current package. + """ + return f'"{py_type}"' + + +def reference_descendent( + current_package: List[str], imports: Set[str], py_package: List[str], py_type: str +) -> str: + """ + Returns a reference to a python type in a package that is a descendent of the + current package, and adds the required import that is aliased to avoid name + conflicts. + """ + importing_descendent = py_package[len(current_package) :] + string_from = ".".join(importing_descendent[:-1]) + string_import = importing_descendent[-1] + if string_from: + string_alias = "_".join(importing_descendent) + imports.add(f"from .{string_from} import {string_import} as {string_alias}") + return f'"{string_alias}.{py_type}"' + else: + imports.add(f"from . import {string_import}") + return f'"{string_import}.{py_type}"' + + +def reference_ancestor( + current_package: List[str], imports: Set[str], py_package: List[str], py_type: str +) -> str: + """ + Returns a reference to a python type in a package which is an ancestor to the + current package, and adds the required import that is aliased (if possible) to avoid + name conflicts. + + Adds trailing __ to avoid name mangling (python.org/dev/peps/pep-0008/#id34). + """ + distance_up = len(current_package) - len(py_package) + if py_package: + string_import = py_package[-1] + string_alias = f"_{'_' * distance_up}{string_import}__" + string_from = f"..{'.' * distance_up}" + imports.add(f"from {string_from} import {string_import} as {string_alias}") + return f'"{string_alias}.{py_type}"' + else: + string_alias = f"{'_' * distance_up}{py_type}__" + imports.add(f"from .{'.' * distance_up} import {py_type} as {string_alias}") + return f'"{string_alias}"' + + +def reference_cousin( + current_package: List[str], imports: Set[str], py_package: List[str], py_type: str +) -> str: + """ + Returns a reference to a python type in a package that is not descendent, ancestor + or sibling, and adds the required import that is aliased to avoid name conflicts. + """ + shared_ancestry = os.path.commonprefix([current_package, py_package]) # type: ignore + distance_up = len(current_package) - len(shared_ancestry) + string_from = f".{'.' * distance_up}" + ".".join( + py_package[len(shared_ancestry) : -1] + ) + string_import = py_package[-1] + # Add trailing __ to avoid name mangling (python.org/dev/peps/pep-0008/#id34) + string_alias = ( + f"{'_' * distance_up}" + + safe_snake_case(".".join(py_package[len(shared_ancestry) :])) + + "__" + ) + imports.add(f"from {string_from} import {string_import} as {string_alias}") + return f'"{string_alias}.{py_type}"' diff --git a/src/aristaproto/compile/naming.py b/src/aristaproto/compile/naming.py new file mode 100644 index 0000000..0c45dde --- /dev/null +++ b/src/aristaproto/compile/naming.py @@ -0,0 +1,21 @@ +from aristaproto import casing + + +def pythonize_class_name(name: str) -> str: + return casing.pascal_case(name) + + +def pythonize_field_name(name: str) -> str: + return casing.safe_snake_case(name) + + +def pythonize_method_name(name: str) -> str: + return casing.safe_snake_case(name) + + +def pythonize_enum_member_name(name: str, enum_name: str) -> str: + enum_name = casing.snake_case(enum_name).upper() + find = name.find(enum_name) + if find != -1: + name = name[find + len(enum_name) :].strip("_") + return casing.sanitize_name(name) diff --git a/src/aristaproto/enum.py b/src/aristaproto/enum.py new file mode 100644 index 0000000..8535e86 --- /dev/null +++ b/src/aristaproto/enum.py @@ -0,0 +1,195 @@ +from __future__ import annotations + +import sys +from enum import ( + EnumMeta, + IntEnum, +) +from types import MappingProxyType +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Optional, + Tuple, +) + + +if TYPE_CHECKING: + from collections.abc import ( + Generator, + Mapping, + ) + + from typing_extensions import ( + Never, + Self, + ) + + +def _is_descriptor(obj: object) -> bool: + return ( + hasattr(obj, "__get__") or hasattr(obj, "__set__") or hasattr(obj, "__delete__") + ) + + +class EnumType(EnumMeta if TYPE_CHECKING else type): + _value_map_: Mapping[int, Enum] + _member_map_: Mapping[str, Enum] + + def __new__( + mcs, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any] + ) -> Self: + value_map = {} + member_map = {} + + new_mcs = type( + f"{name}Type", + tuple( + dict.fromkeys( + [base.__class__ for base in bases if base.__class__ is not type] + + [EnumType, type] + ) + ), # reorder the bases so EnumType and type are last to avoid conflicts + {"_value_map_": value_map, "_member_map_": member_map}, + ) + + members = { + name: value + for name, value in namespace.items() + if not _is_descriptor(value) and not name.startswith("__") + } + + cls = type.__new__( + new_mcs, + name, + bases, + {key: value for key, value in namespace.items() if key not in members}, + ) + # this allows us to disallow member access from other members as + # members become proper class variables + + for name, value in members.items(): + member = value_map.get(value) + if member is None: + member = cls.__new__(cls, name=name, value=value) # type: ignore + value_map[value] = member + member_map[name] = member + type.__setattr__(new_mcs, name, member) + + return cls + + if not TYPE_CHECKING: + + def __call__(cls, value: int) -> Enum: + try: + return cls._value_map_[value] + except (KeyError, TypeError): + raise ValueError(f"{value!r} is not a valid {cls.__name__}") from None + + def __iter__(cls) -> Generator[Enum, None, None]: + yield from cls._member_map_.values() + + def __reversed__(cls) -> Generator[Enum, None, None]: + yield from reversed(cls._member_map_.values()) + + def __getitem__(cls, key: str) -> Enum: + return cls._member_map_[key] + + @property + def __members__(cls) -> MappingProxyType[str, Enum]: + return MappingProxyType(cls._member_map_) + + def __repr__(cls) -> str: + return f"<enum {cls.__name__!r}>" + + def __len__(cls) -> int: + return len(cls._member_map_) + + def __setattr__(cls, name: str, value: Any) -> Never: + raise AttributeError(f"{cls.__name__}: cannot reassign Enum members.") + + def __delattr__(cls, name: str) -> Never: + raise AttributeError(f"{cls.__name__}: cannot delete Enum members.") + + def __contains__(cls, member: object) -> bool: + return isinstance(member, cls) and member.name in cls._member_map_ + + +class Enum(IntEnum if TYPE_CHECKING else int, metaclass=EnumType): + """ + The base class for protobuf enumerations, all generated enumerations will + inherit from this. Emulates `enum.IntEnum`. + """ + + name: Optional[str] + value: int + + if not TYPE_CHECKING: + + def __new__(cls, *, name: Optional[str], value: int) -> Self: + self = super().__new__(cls, value) + super().__setattr__(self, "name", name) + super().__setattr__(self, "value", value) + return self + + def __str__(self) -> str: + return self.name or "None" + + def __repr__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" + + def __setattr__(self, key: str, value: Any) -> Never: + raise AttributeError( + f"{self.__class__.__name__} Cannot reassign a member's attributes." + ) + + def __delattr__(self, item: Any) -> Never: + raise AttributeError( + f"{self.__class__.__name__} Cannot delete a member's attributes." + ) + + def __copy__(self) -> Self: + return self + + def __deepcopy__(self, memo: Any) -> Self: + return self + + @classmethod + def try_value(cls, value: int = 0) -> Self: + """Return the value which corresponds to the value. + + Parameters + ----------- + value: :class:`int` + The value of the enum member to get. + + Returns + ------- + :class:`Enum` + The corresponding member or a new instance of the enum if + ``value`` isn't actually a member. + """ + try: + return cls._value_map_[value] + except (KeyError, TypeError): + return cls.__new__(cls, name=None, value=value) + + @classmethod + def from_string(cls, name: str) -> Self: + """Return the value which corresponds to the string name. + + Parameters + ----------- + name: :class:`str` + The name of the enum member to get. + + Raises + ------- + :exc:`ValueError` + The member was not found in the Enum. + """ + try: + return cls._member_map_[name] + except KeyError as e: + raise ValueError(f"Unknown value {name} for enum {cls.__name__}") from e diff --git a/src/aristaproto/grpc/__init__.py b/src/aristaproto/grpc/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/aristaproto/grpc/__init__.py diff --git a/src/aristaproto/grpc/grpclib_client.py b/src/aristaproto/grpc/grpclib_client.py new file mode 100644 index 0000000..b19e806 --- /dev/null +++ b/src/aristaproto/grpc/grpclib_client.py @@ -0,0 +1,177 @@ +import asyncio +from abc import ABC +from typing import ( + TYPE_CHECKING, + AsyncIterable, + AsyncIterator, + Collection, + Iterable, + Mapping, + Optional, + Tuple, + Type, + Union, +) + +import grpclib.const + + +if TYPE_CHECKING: + from grpclib.client import Channel + from grpclib.metadata import Deadline + + from .._types import ( + ST, + IProtoMessage, + Message, + T, + ) + + +Value = Union[str, bytes] +MetadataLike = Union[Mapping[str, Value], Collection[Tuple[str, Value]]] +MessageSource = Union[Iterable["IProtoMessage"], AsyncIterable["IProtoMessage"]] + + +class ServiceStub(ABC): + """ + Base class for async gRPC clients. + """ + + def __init__( + self, + channel: "Channel", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional[MetadataLike] = None, + ) -> None: + self.channel = channel + self.timeout = timeout + self.deadline = deadline + self.metadata = metadata + + def __resolve_request_kwargs( + self, + timeout: Optional[float], + deadline: Optional["Deadline"], + metadata: Optional[MetadataLike], + ): + return { + "timeout": self.timeout if timeout is None else timeout, + "deadline": self.deadline if deadline is None else deadline, + "metadata": self.metadata if metadata is None else metadata, + } + + async def _unary_unary( + self, + route: str, + request: "IProtoMessage", + response_type: Type["T"], + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional[MetadataLike] = None, + ) -> "T": + """Make a unary request and return the response.""" + async with self.channel.request( + route, + grpclib.const.Cardinality.UNARY_UNARY, + type(request), + response_type, + **self.__resolve_request_kwargs(timeout, deadline, metadata), + ) as stream: + await stream.send_message(request, end=True) + response = await stream.recv_message() + assert response is not None + return response + + async def _unary_stream( + self, + route: str, + request: "IProtoMessage", + response_type: Type["T"], + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional[MetadataLike] = None, + ) -> AsyncIterator["T"]: + """Make a unary request and return the stream response iterator.""" + async with self.channel.request( + route, + grpclib.const.Cardinality.UNARY_STREAM, + type(request), + response_type, + **self.__resolve_request_kwargs(timeout, deadline, metadata), + ) as stream: + await stream.send_message(request, end=True) + async for message in stream: + yield message + + async def _stream_unary( + self, + route: str, + request_iterator: MessageSource, + request_type: Type["IProtoMessage"], + response_type: Type["T"], + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional[MetadataLike] = None, + ) -> "T": + """Make a stream request and return the response.""" + async with self.channel.request( + route, + grpclib.const.Cardinality.STREAM_UNARY, + request_type, + response_type, + **self.__resolve_request_kwargs(timeout, deadline, metadata), + ) as stream: + await stream.send_request() + await self._send_messages(stream, request_iterator) + response = await stream.recv_message() + assert response is not None + return response + + async def _stream_stream( + self, + route: str, + request_iterator: MessageSource, + request_type: Type["IProtoMessage"], + response_type: Type["T"], + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional[MetadataLike] = None, + ) -> AsyncIterator["T"]: + """ + Make a stream request and return an AsyncIterator to iterate over response + messages. + """ + async with self.channel.request( + route, + grpclib.const.Cardinality.STREAM_STREAM, + request_type, + response_type, + **self.__resolve_request_kwargs(timeout, deadline, metadata), + ) as stream: + await stream.send_request() + sending_task = asyncio.ensure_future( + self._send_messages(stream, request_iterator) + ) + try: + async for response in stream: + yield response + except: + sending_task.cancel() + raise + + @staticmethod + async def _send_messages(stream, messages: MessageSource): + if isinstance(messages, AsyncIterable): + async for message in messages: + await stream.send_message(message) + else: + for message in messages: + await stream.send_message(message) + await stream.end() diff --git a/src/aristaproto/grpc/grpclib_server.py b/src/aristaproto/grpc/grpclib_server.py new file mode 100644 index 0000000..3e28031 --- /dev/null +++ b/src/aristaproto/grpc/grpclib_server.py @@ -0,0 +1,33 @@ +from abc import ABC +from collections.abc import AsyncIterable +from typing import ( + Any, + Callable, + Dict, +) + +import grpclib +import grpclib.server + + +class ServiceBase(ABC): + """ + Base class for async gRPC servers. + """ + + async def _call_rpc_handler_server_stream( + self, + handler: Callable, + stream: grpclib.server.Stream, + request: Any, + ) -> None: + response_iter = handler(request) + # check if response is actually an AsyncIterator + # this might be false if the method just returns without + # yielding at least once + # in that case, we just interpret it as an empty iterator + if isinstance(response_iter, AsyncIterable): + async for response_message in response_iter: + await stream.send_message(response_message) + else: + response_iter.close() diff --git a/src/aristaproto/grpc/util/__init__.py b/src/aristaproto/grpc/util/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/aristaproto/grpc/util/__init__.py diff --git a/src/aristaproto/grpc/util/async_channel.py b/src/aristaproto/grpc/util/async_channel.py new file mode 100644 index 0000000..9f18dbf --- /dev/null +++ b/src/aristaproto/grpc/util/async_channel.py @@ -0,0 +1,193 @@ +import asyncio +from typing import ( + AsyncIterable, + AsyncIterator, + Iterable, + Optional, + TypeVar, + Union, +) + + +T = TypeVar("T") + + +class ChannelClosed(Exception): + """ + An exception raised on an attempt to send through a closed channel + """ + + +class ChannelDone(Exception): + """ + An exception raised on an attempt to send receive from a channel that is both closed + and empty. + """ + + +class AsyncChannel(AsyncIterable[T]): + """ + A buffered async channel for sending items between coroutines with FIFO ordering. + + This makes decoupled bidirectional steaming gRPC requests easy if used like: + + .. code-block:: python + client = GeneratedStub(grpclib_chan) + request_channel = await AsyncChannel() + # We can start be sending all the requests we already have + await request_channel.send_from([RequestObject(...), RequestObject(...)]) + async for response in client.rpc_call(request_channel): + # The response iterator will remain active until the connection is closed + ... + # More items can be sent at any time + await request_channel.send(RequestObject(...)) + ... + # The channel must be closed to complete the gRPC connection + request_channel.close() + + Items can be sent through the channel by either: + - providing an iterable to the send_from method + - passing them to the send method one at a time + + Items can be received from the channel by either: + - iterating over the channel with a for loop to get all items + - calling the receive method to get one item at a time + + If the channel is empty then receivers will wait until either an item appears or the + channel is closed. + + Once the channel is closed then subsequent attempt to send through the channel will + fail with a ChannelClosed exception. + + When th channel is closed and empty then it is done, and further attempts to receive + from it will fail with a ChannelDone exception + + If multiple coroutines receive from the channel concurrently, each item sent will be + received by only one of the receivers. + + :param source: + An optional iterable will items that should be sent through the channel + immediately. + :param buffer_limit: + Limit the number of items that can be buffered in the channel, A value less than + 1 implies no limit. If the channel is full then attempts to send more items will + result in the sender waiting until an item is received from the channel. + :param close: + If set to True then the channel will automatically close after exhausting source + or immediately if no source is provided. + """ + + def __init__(self, *, buffer_limit: int = 0, close: bool = False): + self._queue: asyncio.Queue[T] = asyncio.Queue(buffer_limit) + self._closed = False + self._waiting_receivers: int = 0 + # Track whether flush has been invoked so it can only happen once + self._flushed = False + + def __aiter__(self) -> AsyncIterator[T]: + return self + + async def __anext__(self) -> T: + if self.done(): + raise StopAsyncIteration + self._waiting_receivers += 1 + try: + result = await self._queue.get() + if result is self.__flush: + raise StopAsyncIteration + return result + finally: + self._waiting_receivers -= 1 + self._queue.task_done() + + def closed(self) -> bool: + """ + Returns True if this channel is closed and no-longer accepting new items + """ + return self._closed + + def done(self) -> bool: + """ + Check if this channel is done. + + :return: True if this channel is closed and and has been drained of items in + which case any further attempts to receive an item from this channel will raise + a ChannelDone exception. + """ + # After close the channel is not yet done until there is at least one waiting + # receiver per enqueued item. + return self._closed and self._queue.qsize() <= self._waiting_receivers + + async def send_from( + self, source: Union[Iterable[T], AsyncIterable[T]], close: bool = False + ) -> "AsyncChannel[T]": + """ + Iterates the given [Async]Iterable and sends all the resulting items. + If close is set to True then subsequent send calls will be rejected with a + ChannelClosed exception. + :param source: an iterable of items to send + :param close: + if True then the channel will be closed after the source has been exhausted + + """ + if self._closed: + raise ChannelClosed("Cannot send through a closed channel") + if isinstance(source, AsyncIterable): + async for item in source: + await self._queue.put(item) + else: + for item in source: + await self._queue.put(item) + if close: + # Complete the closing process + self.close() + return self + + async def send(self, item: T) -> "AsyncChannel[T]": + """ + Send a single item over this channel. + :param item: The item to send + """ + if self._closed: + raise ChannelClosed("Cannot send through a closed channel") + await self._queue.put(item) + return self + + async def receive(self) -> Optional[T]: + """ + Returns the next item from this channel when it becomes available, + or None if the channel is closed before another item is sent. + :return: An item from the channel + """ + if self.done(): + raise ChannelDone("Cannot receive from a closed channel") + self._waiting_receivers += 1 + try: + result = await self._queue.get() + if result is self.__flush: + return None + return result + finally: + self._waiting_receivers -= 1 + self._queue.task_done() + + def close(self): + """ + Close this channel to new items + """ + self._closed = True + asyncio.ensure_future(self._flush_queue()) + + async def _flush_queue(self): + """ + To be called after the channel is closed. Pushes a number of self.__flush + objects to the queue to ensure no waiting consumers get deadlocked. + """ + if not self._flushed: + self._flushed = True + deadlocked_receivers = max(0, self._waiting_receivers - self._queue.qsize()) + for _ in range(deadlocked_receivers): + await self._queue.put(self.__flush) + + # A special signal object for flushing the queue when the channel is closed + __flush = object() diff --git a/src/aristaproto/lib/__init__.py b/src/aristaproto/lib/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/aristaproto/lib/__init__.py diff --git a/src/aristaproto/lib/google/__init__.py b/src/aristaproto/lib/google/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/aristaproto/lib/google/__init__.py diff --git a/src/aristaproto/lib/google/protobuf/__init__.py b/src/aristaproto/lib/google/protobuf/__init__.py new file mode 100644 index 0000000..f8aad1e --- /dev/null +++ b/src/aristaproto/lib/google/protobuf/__init__.py @@ -0,0 +1 @@ +from aristaproto.lib.std.google.protobuf import * diff --git a/src/aristaproto/lib/google/protobuf/compiler/__init__.py b/src/aristaproto/lib/google/protobuf/compiler/__init__.py new file mode 100644 index 0000000..cfa3855 --- /dev/null +++ b/src/aristaproto/lib/google/protobuf/compiler/__init__.py @@ -0,0 +1 @@ +from aristaproto.lib.std.google.protobuf.compiler import * diff --git a/src/aristaproto/lib/pydantic/__init__.py b/src/aristaproto/lib/pydantic/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/aristaproto/lib/pydantic/__init__.py diff --git a/src/aristaproto/lib/pydantic/google/__init__.py b/src/aristaproto/lib/pydantic/google/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/aristaproto/lib/pydantic/google/__init__.py diff --git a/src/aristaproto/lib/pydantic/google/protobuf/__init__.py b/src/aristaproto/lib/pydantic/google/protobuf/__init__.py new file mode 100644 index 0000000..3a7e8ac --- /dev/null +++ b/src/aristaproto/lib/pydantic/google/protobuf/__init__.py @@ -0,0 +1,2589 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: google/protobuf/any.proto, google/protobuf/api.proto, google/protobuf/descriptor.proto, google/protobuf/duration.proto, google/protobuf/empty.proto, google/protobuf/field_mask.proto, google/protobuf/source_context.proto, google/protobuf/struct.proto, google/protobuf/timestamp.proto, google/protobuf/type.proto, google/protobuf/wrappers.proto +# plugin: python-aristaproto + +import warnings +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from dataclasses import dataclass +else: + from pydantic.dataclasses import dataclass + +from typing import ( + Dict, + List, + Mapping, + Optional, +) + +from pydantic import root_validator +from typing_extensions import Self + +import aristaproto +from aristaproto.utils import hybridmethod + + +class Syntax(aristaproto.Enum): + """The syntax in which a protocol buffer element is defined.""" + + PROTO2 = 0 + """Syntax `proto2`.""" + + PROTO3 = 1 + """Syntax `proto3`.""" + + EDITIONS = 2 + """Syntax `editions`.""" + + +class FieldKind(aristaproto.Enum): + """Basic field types.""" + + TYPE_UNKNOWN = 0 + """Field type unknown.""" + + TYPE_DOUBLE = 1 + """Field type double.""" + + TYPE_FLOAT = 2 + """Field type float.""" + + TYPE_INT64 = 3 + """Field type int64.""" + + TYPE_UINT64 = 4 + """Field type uint64.""" + + TYPE_INT32 = 5 + """Field type int32.""" + + TYPE_FIXED64 = 6 + """Field type fixed64.""" + + TYPE_FIXED32 = 7 + """Field type fixed32.""" + + TYPE_BOOL = 8 + """Field type bool.""" + + TYPE_STRING = 9 + """Field type string.""" + + TYPE_GROUP = 10 + """Field type group. Proto2 syntax only, and deprecated.""" + + TYPE_MESSAGE = 11 + """Field type message.""" + + TYPE_BYTES = 12 + """Field type bytes.""" + + TYPE_UINT32 = 13 + """Field type uint32.""" + + TYPE_ENUM = 14 + """Field type enum.""" + + TYPE_SFIXED32 = 15 + """Field type sfixed32.""" + + TYPE_SFIXED64 = 16 + """Field type sfixed64.""" + + TYPE_SINT32 = 17 + """Field type sint32.""" + + TYPE_SINT64 = 18 + """Field type sint64.""" + + +class FieldCardinality(aristaproto.Enum): + """Whether a field is optional, required, or repeated.""" + + CARDINALITY_UNKNOWN = 0 + """For fields with unknown cardinality.""" + + CARDINALITY_OPTIONAL = 1 + """For optional fields.""" + + CARDINALITY_REQUIRED = 2 + """For required fields. Proto2 syntax only.""" + + CARDINALITY_REPEATED = 3 + """For repeated fields.""" + + +class Edition(aristaproto.Enum): + """The full set of known editions.""" + + UNKNOWN = 0 + """A placeholder for an unknown edition value.""" + + PROTO2 = 998 + """ + Legacy syntax "editions". These pre-date editions, but behave much like + distinct editions. These can't be used to specify the edition of proto + files, but feature definitions must supply proto2/proto3 defaults for + backwards compatibility. + """ + + PROTO3 = 999 + _2023 = 1000 + """ + Editions that have been released. The specific values are arbitrary and + should not be depended on, but they will always be time-ordered for easy + comparison. + """ + + _2024 = 1001 + _1_TEST_ONLY = 1 + """ + Placeholder editions for testing feature resolution. These should not be + used or relyed on outside of tests. + """ + + _2_TEST_ONLY = 2 + _99997_TEST_ONLY = 99997 + _99998_TEST_ONLY = 99998 + _99999_TEST_ONLY = 99999 + MAX = 2147483647 + """ + Placeholder for specifying unbounded edition support. This should only + ever be used by plugins that can expect to never require any changes to + support a new edition. + """ + + +class ExtensionRangeOptionsVerificationState(aristaproto.Enum): + """The verification state of the extension range.""" + + DECLARATION = 0 + """All the extensions of the range must be declared.""" + + UNVERIFIED = 1 + + +class FieldDescriptorProtoType(aristaproto.Enum): + TYPE_DOUBLE = 1 + """ + 0 is reserved for errors. + Order is weird for historical reasons. + """ + + TYPE_FLOAT = 2 + TYPE_INT64 = 3 + """ + Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if + negative values are likely. + """ + + TYPE_UINT64 = 4 + TYPE_INT32 = 5 + """ + Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if + negative values are likely. + """ + + TYPE_FIXED64 = 6 + TYPE_FIXED32 = 7 + TYPE_BOOL = 8 + TYPE_STRING = 9 + TYPE_GROUP = 10 + """ + Tag-delimited aggregate. + Group type is deprecated and not supported after google.protobuf. However, Proto3 + implementations should still be able to parse the group wire format and + treat group fields as unknown fields. In Editions, the group wire format + can be enabled via the `message_encoding` feature. + """ + + TYPE_MESSAGE = 11 + TYPE_BYTES = 12 + """New in version 2.""" + + TYPE_UINT32 = 13 + TYPE_ENUM = 14 + TYPE_SFIXED32 = 15 + TYPE_SFIXED64 = 16 + TYPE_SINT32 = 17 + TYPE_SINT64 = 18 + + +class FieldDescriptorProtoLabel(aristaproto.Enum): + LABEL_OPTIONAL = 1 + """0 is reserved for errors""" + + LABEL_REPEATED = 3 + LABEL_REQUIRED = 2 + """ + The required label is only allowed in google.protobuf. In proto3 and Editions + it's explicitly prohibited. In Editions, the `field_presence` feature + can be used to get this behavior. + """ + + +class FileOptionsOptimizeMode(aristaproto.Enum): + """Generated classes can be optimized for speed or code size.""" + + SPEED = 1 + CODE_SIZE = 2 + """etc.""" + + LITE_RUNTIME = 3 + + +class FieldOptionsCType(aristaproto.Enum): + STRING = 0 + """Default mode.""" + + CORD = 1 + """ + The option [ctype=CORD] may be applied to a non-repeated field of type + "bytes". It indicates that in C++, the data should be stored in a Cord + instead of a string. For very large strings, this may reduce memory + fragmentation. It may also allow better performance when parsing from a + Cord, or when parsing with aliasing enabled, as the parsed Cord may then + alias the original buffer. + """ + + STRING_PIECE = 2 + + +class FieldOptionsJsType(aristaproto.Enum): + JS_NORMAL = 0 + """Use the default type.""" + + JS_STRING = 1 + """Use JavaScript strings.""" + + JS_NUMBER = 2 + """Use JavaScript numbers.""" + + +class FieldOptionsOptionRetention(aristaproto.Enum): + """ + If set to RETENTION_SOURCE, the option will be omitted from the binary. + Note: as of January 2023, support for this is in progress and does not yet + have an effect (b/264593489). + """ + + RETENTION_UNKNOWN = 0 + RETENTION_RUNTIME = 1 + RETENTION_SOURCE = 2 + + +class FieldOptionsOptionTargetType(aristaproto.Enum): + """ + This indicates the types of entities that the field may apply to when used + as an option. If it is unset, then the field may be freely used as an + option on any kind of entity. Note: as of January 2023, support for this is + in progress and does not yet have an effect (b/264593489). + """ + + TARGET_TYPE_UNKNOWN = 0 + TARGET_TYPE_FILE = 1 + TARGET_TYPE_EXTENSION_RANGE = 2 + TARGET_TYPE_MESSAGE = 3 + TARGET_TYPE_FIELD = 4 + TARGET_TYPE_ONEOF = 5 + TARGET_TYPE_ENUM = 6 + TARGET_TYPE_ENUM_ENTRY = 7 + TARGET_TYPE_SERVICE = 8 + TARGET_TYPE_METHOD = 9 + + +class MethodOptionsIdempotencyLevel(aristaproto.Enum): + """ + Is this method side-effect-free (or safe in HTTP parlance), or idempotent, + or neither? HTTP based RPC implementation may choose GET verb for safe + methods, and PUT verb for idempotent methods instead of the default POST. + """ + + IDEMPOTENCY_UNKNOWN = 0 + NO_SIDE_EFFECTS = 1 + IDEMPOTENT = 2 + + +class FeatureSetFieldPresence(aristaproto.Enum): + FIELD_PRESENCE_UNKNOWN = 0 + EXPLICIT = 1 + IMPLICIT = 2 + LEGACY_REQUIRED = 3 + + +class FeatureSetEnumType(aristaproto.Enum): + ENUM_TYPE_UNKNOWN = 0 + OPEN = 1 + CLOSED = 2 + + +class FeatureSetRepeatedFieldEncoding(aristaproto.Enum): + REPEATED_FIELD_ENCODING_UNKNOWN = 0 + PACKED = 1 + EXPANDED = 2 + + +class FeatureSetUtf8Validation(aristaproto.Enum): + UTF8_VALIDATION_UNKNOWN = 0 + VERIFY = 2 + NONE = 3 + + +class FeatureSetMessageEncoding(aristaproto.Enum): + MESSAGE_ENCODING_UNKNOWN = 0 + LENGTH_PREFIXED = 1 + DELIMITED = 2 + + +class FeatureSetJsonFormat(aristaproto.Enum): + JSON_FORMAT_UNKNOWN = 0 + ALLOW = 1 + LEGACY_BEST_EFFORT = 2 + + +class GeneratedCodeInfoAnnotationSemantic(aristaproto.Enum): + """ + Represents the identified object's effect on the element in the original + .proto file. + """ + + NONE = 0 + """There is no effect or the effect is indescribable.""" + + SET = 1 + """The element is set or otherwise mutated.""" + + ALIAS = 2 + """An alias to the element is returned.""" + + +class NullValue(aristaproto.Enum): + """ + `NullValue` is a singleton enumeration to represent the null value for the + `Value` type union. + + The JSON representation for `NullValue` is JSON `null`. + """ + + _ = 0 + """Null value.""" + + +@dataclass(eq=False, repr=False) +class Any(aristaproto.Message): + """ + `Any` contains an arbitrary serialized protocol buffer message along with a + URL that describes the type of the serialized message. + + Protobuf library provides support to pack/unpack Any values in the form + of utility functions or additional generated methods of the Any type. + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + 'type.googleapis.com/full.type.name' as the type URL and the unpack + methods only use the fully qualified type name after the last '/' + in the type URL, for example "foo.bar.com/x/y.z" will yield type + name "y.z". + + JSON + ==== + The JSON representation of an `Any` value uses the regular + representation of the deserialized, embedded message, with an + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": <string>, + "lastName": <string> + } + + If the embedded message type is well-known and has a custom JSON + representation, that representation will be embedded adding a field + `value` which holds the custom JSON in addition to the `@type` + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + """ + + type_url: str = aristaproto.string_field(1) + """ + A URL/resource name that uniquely identifies the type of the serialized + protocol buffer message. This string must contain at least + one "/" character. The last segment of the URL's path must represent + the fully qualified name of the type (as in + `path/google.protobuf.Duration`). The name should be in a canonical form + (e.g., leading "." is not accepted). + + In practice, teams usually precompile into the binary all types that they + expect it to use in the context of Any. However, for URLs which use the + scheme `http`, `https`, or no scheme, one can optionally set up a type + server that maps type URLs to message definitions as follows: + + * If no scheme is provided, `https` is assumed. + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the official + protobuf release, and it is not used for type URLs beginning with + type.googleapis.com. As of May 2023, there are no widely used type server + implementations and no plans to implement one. + + Schemes other than `http`, `https` (or the empty scheme) might be + used with implementation specific semantics. + """ + + value: bytes = aristaproto.bytes_field(2) + """ + Must be a valid serialized protocol buffer of the above specified type. + """ + + +@dataclass(eq=False, repr=False) +class SourceContext(aristaproto.Message): + """ + `SourceContext` represents information about the source of a + protobuf element, like the file in which it is defined. + """ + + file_name: str = aristaproto.string_field(1) + """ + The path-qualified name of the .proto file that contained the associated + protobuf element. For example: `"google/protobuf/source_context.proto"`. + """ + + +@dataclass(eq=False, repr=False) +class Type(aristaproto.Message): + """A protocol buffer message type.""" + + name: str = aristaproto.string_field(1) + """The fully qualified message name.""" + + fields: List["Field"] = aristaproto.message_field(2) + """The list of fields.""" + + oneofs: List[str] = aristaproto.string_field(3) + """The list of types appearing in `oneof` definitions in this type.""" + + options: List["Option"] = aristaproto.message_field(4) + """The protocol buffer options.""" + + source_context: "SourceContext" = aristaproto.message_field(5) + """The source context.""" + + syntax: "Syntax" = aristaproto.enum_field(6) + """The source syntax.""" + + edition: str = aristaproto.string_field(7) + """ + The source edition string, only valid when syntax is SYNTAX_EDITIONS. + """ + + +@dataclass(eq=False, repr=False) +class Field(aristaproto.Message): + """A single field of a message type.""" + + kind: "FieldKind" = aristaproto.enum_field(1) + """The field type.""" + + cardinality: "FieldCardinality" = aristaproto.enum_field(2) + """The field cardinality.""" + + number: int = aristaproto.int32_field(3) + """The field number.""" + + name: str = aristaproto.string_field(4) + """The field name.""" + + type_url: str = aristaproto.string_field(6) + """ + The field type URL, without the scheme, for message or enumeration + types. Example: `"type.googleapis.com/google.protobuf.Timestamp"`. + """ + + oneof_index: int = aristaproto.int32_field(7) + """ + The index of the field type in `Type.oneofs`, for message or enumeration + types. The first type has index 1; zero means the type is not in the list. + """ + + packed: bool = aristaproto.bool_field(8) + """Whether to use alternative packed wire representation.""" + + options: List["Option"] = aristaproto.message_field(9) + """The protocol buffer options.""" + + json_name: str = aristaproto.string_field(10) + """The field JSON name.""" + + default_value: str = aristaproto.string_field(11) + """ + The string value of the default value of this field. Proto2 syntax only. + """ + + +@dataclass(eq=False, repr=False) +class Enum(aristaproto.Message): + """Enum type definition.""" + + name: str = aristaproto.string_field(1) + """Enum type name.""" + + enumvalue: List["EnumValue"] = aristaproto.message_field( + 2, wraps=aristaproto.TYPE_ENUM + ) + """Enum value definitions.""" + + options: List["Option"] = aristaproto.message_field(3) + """Protocol buffer options.""" + + source_context: "SourceContext" = aristaproto.message_field(4) + """The source context.""" + + syntax: "Syntax" = aristaproto.enum_field(5) + """The source syntax.""" + + edition: str = aristaproto.string_field(6) + """ + The source edition string, only valid when syntax is SYNTAX_EDITIONS. + """ + + +@dataclass(eq=False, repr=False) +class EnumValue(aristaproto.Message): + """Enum value definition.""" + + name: str = aristaproto.string_field(1) + """Enum value name.""" + + number: int = aristaproto.int32_field(2) + """Enum value number.""" + + options: List["Option"] = aristaproto.message_field(3) + """Protocol buffer options.""" + + +@dataclass(eq=False, repr=False) +class Option(aristaproto.Message): + """ + A protocol buffer option, which can be attached to a message, field, + enumeration, etc. + """ + + name: str = aristaproto.string_field(1) + """ + The option's name. For protobuf built-in options (options defined in + descriptor.proto), this is the short name. For example, `"map_entry"`. + For custom options, it should be the fully-qualified name. For example, + `"google.api.http"`. + """ + + value: "Any" = aristaproto.message_field(2) + """ + The option's value packed in an Any message. If the value is a primitive, + the corresponding wrapper type defined in google/protobuf/wrappers.proto + should be used. If the value is an enum, it should be stored as an int32 + value using the google.protobuf.Int32Value type. + """ + + +@dataclass(eq=False, repr=False) +class Api(aristaproto.Message): + """ + Api is a light-weight descriptor for an API Interface. + + Interfaces are also described as "protocol buffer services" in some contexts, + such as by the "service" keyword in a .proto file, but they are different + from API Services, which represent a concrete implementation of an interface + as opposed to simply a description of methods and bindings. They are also + sometimes simply referred to as "APIs" in other contexts, such as the name of + this message itself. See https://cloud.google.com/apis/design/glossary for + detailed terminology. + """ + + name: str = aristaproto.string_field(1) + """ + The fully qualified name of this interface, including package name + followed by the interface's simple name. + """ + + methods: List["Method"] = aristaproto.message_field(2) + """The methods of this interface, in unspecified order.""" + + options: List["Option"] = aristaproto.message_field(3) + """Any metadata attached to the interface.""" + + version: str = aristaproto.string_field(4) + """ + A version string for this interface. If specified, must have the form + `major-version.minor-version`, as in `1.10`. If the minor version is + omitted, it defaults to zero. If the entire version field is empty, the + major version is derived from the package name, as outlined below. If the + field is not empty, the version in the package name will be verified to be + consistent with what is provided here. + + The versioning schema uses [semantic + versioning](http://semver.org) where the major version number + indicates a breaking change and the minor version an additive, + non-breaking change. Both version numbers are signals to users + what to expect from different versions, and should be carefully + chosen based on the product plan. + + The major version is also reflected in the package name of the + interface, which must end in `v<major-version>`, as in + `google.feature.v1`. For major versions 0 and 1, the suffix can + be omitted. Zero major versions must only be used for + experimental, non-GA interfaces. + """ + + source_context: "SourceContext" = aristaproto.message_field(5) + """ + Source context for the protocol buffer service represented by this + message. + """ + + mixins: List["Mixin"] = aristaproto.message_field(6) + """Included interfaces. See [Mixin][].""" + + syntax: "Syntax" = aristaproto.enum_field(7) + """The source syntax of the service.""" + + +@dataclass(eq=False, repr=False) +class Method(aristaproto.Message): + """Method represents a method of an API interface.""" + + name: str = aristaproto.string_field(1) + """The simple name of this method.""" + + request_type_url: str = aristaproto.string_field(2) + """A URL of the input message type.""" + + request_streaming: bool = aristaproto.bool_field(3) + """If true, the request is streamed.""" + + response_type_url: str = aristaproto.string_field(4) + """The URL of the output message type.""" + + response_streaming: bool = aristaproto.bool_field(5) + """If true, the response is streamed.""" + + options: List["Option"] = aristaproto.message_field(6) + """Any metadata attached to the method.""" + + syntax: "Syntax" = aristaproto.enum_field(7) + """The source syntax of this method.""" + + +@dataclass(eq=False, repr=False) +class Mixin(aristaproto.Message): + """ + Declares an API Interface to be included in this interface. The including + interface must redeclare all the methods from the included interface, but + documentation and options are inherited as follows: + + - If after comment and whitespace stripping, the documentation + string of the redeclared method is empty, it will be inherited + from the original method. + + - Each annotation belonging to the service config (http, + visibility) which is not set in the redeclared method will be + inherited. + + - If an http annotation is inherited, the path pattern will be + modified as follows. Any version prefix will be replaced by the + version of the including interface plus the [root][] path if + specified. + + Example of a simple mixin: + + package google.acl.v1; + service AccessControl { + // Get the underlying ACL object. + rpc GetAcl(GetAclRequest) returns (Acl) { + option (google.api.http).get = "/v1/{resource=**}:getAcl"; + } + } + + package google.storage.v2; + service Storage { + rpc GetAcl(GetAclRequest) returns (Acl); + + // Get a data record. + rpc GetData(GetDataRequest) returns (Data) { + option (google.api.http).get = "/v2/{resource=**}"; + } + } + + Example of a mixin configuration: + + apis: + - name: google.storage.v2.Storage + mixins: + - name: google.acl.v1.AccessControl + + The mixin construct implies that all methods in `AccessControl` are + also declared with same name and request/response types in + `Storage`. A documentation generator or annotation processor will + see the effective `Storage.GetAcl` method after inherting + documentation and annotations as follows: + + service Storage { + // Get the underlying ACL object. + rpc GetAcl(GetAclRequest) returns (Acl) { + option (google.api.http).get = "/v2/{resource=**}:getAcl"; + } + ... + } + + Note how the version in the path pattern changed from `v1` to `v2`. + + If the `root` field in the mixin is specified, it should be a + relative path under which inherited HTTP paths are placed. Example: + + apis: + - name: google.storage.v2.Storage + mixins: + - name: google.acl.v1.AccessControl + root: acls + + This implies the following inherited HTTP annotation: + + service Storage { + // Get the underlying ACL object. + rpc GetAcl(GetAclRequest) returns (Acl) { + option (google.api.http).get = "/v2/acls/{resource=**}:getAcl"; + } + ... + } + """ + + name: str = aristaproto.string_field(1) + """The fully qualified name of the interface which is included.""" + + root: str = aristaproto.string_field(2) + """ + If non-empty specifies a path under which inherited HTTP paths + are rooted. + """ + + +@dataclass(eq=False, repr=False) +class FileDescriptorSet(aristaproto.Message): + """ + The protocol compiler can output a FileDescriptorSet containing the .proto + files it parses. + """ + + file: List["FileDescriptorProto"] = aristaproto.message_field(1) + + +@dataclass(eq=False, repr=False) +class FileDescriptorProto(aristaproto.Message): + """Describes a complete .proto file.""" + + name: str = aristaproto.string_field(1) + package: str = aristaproto.string_field(2) + dependency: List[str] = aristaproto.string_field(3) + """Names of files imported by this file.""" + + public_dependency: List[int] = aristaproto.int32_field(10) + """Indexes of the public imported files in the dependency list above.""" + + weak_dependency: List[int] = aristaproto.int32_field(11) + """ + Indexes of the weak imported files in the dependency list. + For Google-internal migration only. Do not use. + """ + + message_type: List["DescriptorProto"] = aristaproto.message_field(4) + """All top-level definitions in this file.""" + + enum_type: List["EnumDescriptorProto"] = aristaproto.message_field(5) + service: List["ServiceDescriptorProto"] = aristaproto.message_field(6) + extension: List["FieldDescriptorProto"] = aristaproto.message_field(7) + options: "FileOptions" = aristaproto.message_field(8) + source_code_info: "SourceCodeInfo" = aristaproto.message_field(9) + """ + This field contains optional information about the original source code. + You may safely remove this entire field without harming runtime + functionality of the descriptors -- the information is needed only by + development tools. + """ + + syntax: str = aristaproto.string_field(12) + """ + The syntax of the proto file. + The supported values are "proto2", "proto3", and "editions". + + If `edition` is present, this value must be "editions". + """ + + edition: "Edition" = aristaproto.enum_field(14) + """The edition of the proto file.""" + + +@dataclass(eq=False, repr=False) +class DescriptorProto(aristaproto.Message): + """Describes a message type.""" + + name: str = aristaproto.string_field(1) + field: List["FieldDescriptorProto"] = aristaproto.message_field(2) + extension: List["FieldDescriptorProto"] = aristaproto.message_field(6) + nested_type: List["DescriptorProto"] = aristaproto.message_field(3) + enum_type: List["EnumDescriptorProto"] = aristaproto.message_field(4) + extension_range: List["DescriptorProtoExtensionRange"] = aristaproto.message_field( + 5 + ) + oneof_decl: List["OneofDescriptorProto"] = aristaproto.message_field(8) + options: "MessageOptions" = aristaproto.message_field(7) + reserved_range: List["DescriptorProtoReservedRange"] = aristaproto.message_field(9) + reserved_name: List[str] = aristaproto.string_field(10) + """ + Reserved field names, which may not be used by fields in the same message. + A given name may only be reserved once. + """ + + +@dataclass(eq=False, repr=False) +class DescriptorProtoExtensionRange(aristaproto.Message): + start: int = aristaproto.int32_field(1) + end: int = aristaproto.int32_field(2) + options: "ExtensionRangeOptions" = aristaproto.message_field(3) + + +@dataclass(eq=False, repr=False) +class DescriptorProtoReservedRange(aristaproto.Message): + """ + Range of reserved tag numbers. Reserved tag numbers may not be used by + fields or extension ranges in the same message. Reserved ranges may + not overlap. + """ + + start: int = aristaproto.int32_field(1) + end: int = aristaproto.int32_field(2) + + +@dataclass(eq=False, repr=False) +class ExtensionRangeOptions(aristaproto.Message): + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + declaration: List["ExtensionRangeOptionsDeclaration"] = aristaproto.message_field(2) + """ + For external users: DO NOT USE. We are in the process of open sourcing + extension declaration and executing internal cleanups before it can be + used externally. + """ + + features: "FeatureSet" = aristaproto.message_field(50) + """Any features defined in the specific edition.""" + + verification: "ExtensionRangeOptionsVerificationState" = aristaproto.enum_field(3) + """ + The verification state of the range. + TODO: flip the default to DECLARATION once all empty ranges + are marked as UNVERIFIED. + """ + + +@dataclass(eq=False, repr=False) +class ExtensionRangeOptionsDeclaration(aristaproto.Message): + number: int = aristaproto.int32_field(1) + """The extension number declared within the extension range.""" + + full_name: str = aristaproto.string_field(2) + """ + The fully-qualified name of the extension field. There must be a leading + dot in front of the full name. + """ + + type: str = aristaproto.string_field(3) + """ + The fully-qualified type name of the extension field. Unlike + Metadata.type, Declaration.type must have a leading dot for messages + and enums. + """ + + reserved: bool = aristaproto.bool_field(5) + """ + If true, indicates that the number is reserved in the extension range, + and any extension field with the number will fail to compile. Set this + when a declared extension field is deleted. + """ + + repeated: bool = aristaproto.bool_field(6) + """ + If true, indicates that the extension must be defined as repeated. + Otherwise the extension must be defined as optional. + """ + + +@dataclass(eq=False, repr=False) +class FieldDescriptorProto(aristaproto.Message): + """Describes a field within a message.""" + + name: str = aristaproto.string_field(1) + number: int = aristaproto.int32_field(3) + label: "FieldDescriptorProtoLabel" = aristaproto.enum_field(4) + type: "FieldDescriptorProtoType" = aristaproto.enum_field(5) + """ + If type_name is set, this need not be set. If both this and type_name + are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. + """ + + type_name: str = aristaproto.string_field(6) + """ + For message and enum types, this is the name of the type. If the name + starts with a '.', it is fully-qualified. Otherwise, C++-like scoping + rules are used to find the type (i.e. first the nested types within this + message are searched, then within the parent, on up to the root + namespace). + """ + + extendee: str = aristaproto.string_field(2) + """ + For extensions, this is the name of the type being extended. It is + resolved in the same manner as type_name. + """ + + default_value: str = aristaproto.string_field(7) + """ + For numeric types, contains the original text representation of the value. + For booleans, "true" or "false". + For strings, contains the default text contents (not escaped in any way). + For bytes, contains the C escaped value. All bytes >= 128 are escaped. + """ + + oneof_index: int = aristaproto.int32_field(9) + """ + If set, gives the index of a oneof in the containing type's oneof_decl + list. This field is a member of that oneof. + """ + + json_name: str = aristaproto.string_field(10) + """ + JSON name of this field. The value is set by protocol compiler. If the + user has set a "json_name" option on this field, that option's value + will be used. Otherwise, it's deduced from the field's name by converting + it to camelCase. + """ + + options: "FieldOptions" = aristaproto.message_field(8) + proto3_optional: bool = aristaproto.bool_field(17) + """ + If true, this is a proto3 "optional". When a proto3 field is optional, it + tracks presence regardless of field type. + + When proto3_optional is true, this field must belong to a oneof to signal + to old proto3 clients that presence is tracked for this field. This oneof + is known as a "synthetic" oneof, and this field must be its sole member + (each proto3 optional field gets its own synthetic oneof). Synthetic oneofs + exist in the descriptor only, and do not generate any API. Synthetic oneofs + must be ordered after all "real" oneofs. + + For message fields, proto3_optional doesn't create any semantic change, + since non-repeated message fields always track presence. However it still + indicates the semantic detail of whether the user wrote "optional" or not. + This can be useful for round-tripping the .proto file. For consistency we + give message fields a synthetic oneof also, even though it is not required + to track presence. This is especially important because the parser can't + tell if a field is a message or an enum, so it must always create a + synthetic oneof. + + Proto2 optional fields do not set this flag, because they already indicate + optional with `LABEL_OPTIONAL`. + """ + + +@dataclass(eq=False, repr=False) +class OneofDescriptorProto(aristaproto.Message): + """Describes a oneof.""" + + name: str = aristaproto.string_field(1) + options: "OneofOptions" = aristaproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class EnumDescriptorProto(aristaproto.Message): + """Describes an enum type.""" + + name: str = aristaproto.string_field(1) + value: List["EnumValueDescriptorProto"] = aristaproto.message_field(2) + options: "EnumOptions" = aristaproto.message_field(3) + reserved_range: List[ + "EnumDescriptorProtoEnumReservedRange" + ] = aristaproto.message_field(4) + """ + Range of reserved numeric values. Reserved numeric values may not be used + by enum values in the same enum declaration. Reserved ranges may not + overlap. + """ + + reserved_name: List[str] = aristaproto.string_field(5) + """ + Reserved enum value names, which may not be reused. A given name may only + be reserved once. + """ + + +@dataclass(eq=False, repr=False) +class EnumDescriptorProtoEnumReservedRange(aristaproto.Message): + """ + Range of reserved numeric values. Reserved values may not be used by + entries in the same enum. Reserved ranges may not overlap. + + Note that this is distinct from DescriptorProto.ReservedRange in that it + is inclusive such that it can appropriately represent the entire int32 + domain. + """ + + start: int = aristaproto.int32_field(1) + end: int = aristaproto.int32_field(2) + + +@dataclass(eq=False, repr=False) +class EnumValueDescriptorProto(aristaproto.Message): + """Describes a value within an enum.""" + + name: str = aristaproto.string_field(1) + number: int = aristaproto.int32_field(2) + options: "EnumValueOptions" = aristaproto.message_field(3) + + +@dataclass(eq=False, repr=False) +class ServiceDescriptorProto(aristaproto.Message): + """Describes a service.""" + + name: str = aristaproto.string_field(1) + method: List["MethodDescriptorProto"] = aristaproto.message_field(2) + options: "ServiceOptions" = aristaproto.message_field(3) + + +@dataclass(eq=False, repr=False) +class MethodDescriptorProto(aristaproto.Message): + """Describes a method of a service.""" + + name: str = aristaproto.string_field(1) + input_type: str = aristaproto.string_field(2) + """ + Input and output type names. These are resolved in the same way as + FieldDescriptorProto.type_name, but must refer to a message type. + """ + + output_type: str = aristaproto.string_field(3) + options: "MethodOptions" = aristaproto.message_field(4) + client_streaming: bool = aristaproto.bool_field(5) + """Identifies if client streams multiple client messages""" + + server_streaming: bool = aristaproto.bool_field(6) + """Identifies if server streams multiple server messages""" + + +@dataclass(eq=False, repr=False) +class FileOptions(aristaproto.Message): + java_package: str = aristaproto.string_field(1) + """ + Sets the Java package where classes generated from this .proto will be + placed. By default, the proto package is used, but this is often + inappropriate because proto packages do not normally start with backwards + domain names. + """ + + java_outer_classname: str = aristaproto.string_field(8) + """ + Controls the name of the wrapper Java class generated for the .proto file. + That class will always contain the .proto file's getDescriptor() method as + well as any top-level extensions defined in the .proto file. + If java_multiple_files is disabled, then all the other classes from the + .proto file will be nested inside the single wrapper outer class. + """ + + java_multiple_files: bool = aristaproto.bool_field(10) + """ + If enabled, then the Java code generator will generate a separate .java + file for each top-level message, enum, and service defined in the .proto + file. Thus, these types will *not* be nested inside the wrapper class + named by java_outer_classname. However, the wrapper class will still be + generated to contain the file's getDescriptor() method as well as any + top-level extensions defined in the file. + """ + + java_generate_equals_and_hash: bool = aristaproto.bool_field(20) + """This option does nothing.""" + + java_string_check_utf8: bool = aristaproto.bool_field(27) + """ + A proto2 file can set this to true to opt in to UTF-8 checking for Java, + which will throw an exception if invalid UTF-8 is parsed from the wire or + assigned to a string field. + + TODO: clarify exactly what kinds of field types this option + applies to, and update these docs accordingly. + + Proto3 files already perform these checks. Setting the option explicitly to + false has no effect: it cannot be used to opt proto3 files out of UTF-8 + checks. + """ + + optimize_for: "FileOptionsOptimizeMode" = aristaproto.enum_field(9) + go_package: str = aristaproto.string_field(11) + """ + Sets the Go package where structs generated from this .proto will be + placed. If omitted, the Go package will be derived from the following: + - The basename of the package import path, if provided. + - Otherwise, the package statement in the .proto file, if present. + - Otherwise, the basename of the .proto file, without extension. + """ + + cc_generic_services: bool = aristaproto.bool_field(16) + """ + Should generic services be generated in each language? "Generic" services + are not specific to any particular RPC system. They are generated by the + main code generators in each language (without additional plugins). + Generic services were the only kind of service generation supported by + early versions of google.protobuf. + + Generic services are now considered deprecated in favor of using plugins + that generate code specific to your particular RPC system. Therefore, + these default to false. Old code which depends on generic services should + explicitly set them to true. + """ + + java_generic_services: bool = aristaproto.bool_field(17) + py_generic_services: bool = aristaproto.bool_field(18) + deprecated: bool = aristaproto.bool_field(23) + """ + Is this file deprecated? + Depending on the target platform, this can emit Deprecated annotations + for everything in the file, or it will be completely ignored; in the very + least, this is a formalization for deprecating files. + """ + + cc_enable_arenas: bool = aristaproto.bool_field(31) + """ + Enables the use of arenas for the proto messages in this file. This applies + only to generated classes for C++. + """ + + objc_class_prefix: str = aristaproto.string_field(36) + """ + Sets the objective c class prefix which is prepended to all objective c + generated classes from this .proto. There is no default. + """ + + csharp_namespace: str = aristaproto.string_field(37) + """Namespace for generated classes; defaults to the package.""" + + swift_prefix: str = aristaproto.string_field(39) + """ + By default Swift generators will take the proto package and CamelCase it + replacing '.' with underscore and use that to prefix the types/symbols + defined. When this options is provided, they will use this value instead + to prefix the types/symbols defined. + """ + + php_class_prefix: str = aristaproto.string_field(40) + """ + Sets the php class prefix which is prepended to all php generated classes + from this .proto. Default is empty. + """ + + php_namespace: str = aristaproto.string_field(41) + """ + Use this option to change the namespace of php generated classes. Default + is empty. When this option is empty, the package name will be used for + determining the namespace. + """ + + php_metadata_namespace: str = aristaproto.string_field(44) + """ + Use this option to change the namespace of php generated metadata classes. + Default is empty. When this option is empty, the proto file name will be + used for determining the namespace. + """ + + ruby_package: str = aristaproto.string_field(45) + """ + Use this option to change the package of ruby generated classes. Default + is empty. When this option is not set, the package name will be used for + determining the ruby package. + """ + + features: "FeatureSet" = aristaproto.message_field(50) + """Any features defined in the specific edition.""" + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """ + The parser stores options it doesn't recognize here. + See the documentation for the "Options" section above. + """ + + def __post_init__(self) -> None: + super().__post_init__() + if self.is_set("java_generate_equals_and_hash"): + warnings.warn( + "FileOptions.java_generate_equals_and_hash is deprecated", + DeprecationWarning, + ) + + +@dataclass(eq=False, repr=False) +class MessageOptions(aristaproto.Message): + message_set_wire_format: bool = aristaproto.bool_field(1) + """ + Set true to use the old proto1 MessageSet wire format for extensions. + This is provided for backwards-compatibility with the MessageSet wire + format. You should not use this for any other reason: It's less + efficient, has fewer features, and is more complicated. + + The message must be defined exactly as follows: + message Foo { + option message_set_wire_format = true; + extensions 4 to max; + } + Note that the message cannot have any defined fields; MessageSets only + have extensions. + + All extensions of your type must be singular messages; e.g. they cannot + be int32s, enums, or repeated messages. + + Because this is an option, the above two restrictions are not enforced by + the protocol compiler. + """ + + no_standard_descriptor_accessor: bool = aristaproto.bool_field(2) + """ + Disables the generation of the standard "descriptor()" accessor, which can + conflict with a field of the same name. This is meant to make migration + from proto1 easier; new code should avoid fields named "descriptor". + """ + + deprecated: bool = aristaproto.bool_field(3) + """ + Is this message deprecated? + Depending on the target platform, this can emit Deprecated annotations + for the message, or it will be completely ignored; in the very least, + this is a formalization for deprecating messages. + """ + + map_entry: bool = aristaproto.bool_field(7) + """ + Whether the message is an automatically generated map entry type for the + maps field. + + For maps fields: + map<KeyType, ValueType> map_field = 1; + The parsed descriptor looks like: + message MapFieldEntry { + option map_entry = true; + optional KeyType key = 1; + optional ValueType value = 2; + } + repeated MapFieldEntry map_field = 1; + + Implementations may choose not to generate the map_entry=true message, but + use a native map in the target language to hold the keys and values. + The reflection APIs in such implementations still need to work as + if the field is a repeated message field. + + NOTE: Do not set the option in .proto files. Always use the maps syntax + instead. The option should only be implicitly set by the proto compiler + parser. + """ + + deprecated_legacy_json_field_conflicts: bool = aristaproto.bool_field(11) + """ + Enable the legacy handling of JSON field name conflicts. This lowercases + and strips underscored from the fields before comparison in proto3 only. + The new behavior takes `json_name` into account and applies to proto2 as + well. + + This should only be used as a temporary measure against broken builds due + to the change in behavior for JSON field name conflicts. + + TODO This is legacy behavior we plan to remove once downstream + teams have had time to migrate. + """ + + features: "FeatureSet" = aristaproto.message_field(12) + """Any features defined in the specific edition.""" + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + def __post_init__(self) -> None: + super().__post_init__() + if self.is_set("deprecated_legacy_json_field_conflicts"): + warnings.warn( + "MessageOptions.deprecated_legacy_json_field_conflicts is deprecated", + DeprecationWarning, + ) + + +@dataclass(eq=False, repr=False) +class FieldOptions(aristaproto.Message): + ctype: "FieldOptionsCType" = aristaproto.enum_field(1) + """ + The ctype option instructs the C++ code generator to use a different + representation of the field than it normally would. See the specific + options below. This option is only implemented to support use of + [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of + type "bytes" in the open source release -- sorry, we'll try to include + other types in a future version! + """ + + packed: bool = aristaproto.bool_field(2) + """ + The packed option can be enabled for repeated primitive fields to enable + a more efficient representation on the wire. Rather than repeatedly + writing the tag and type for each element, the entire array is encoded as + a single length-delimited blob. In proto3, only explicit setting it to + false will avoid using packed encoding. This option is prohibited in + Editions, but the `repeated_field_encoding` feature can be used to control + the behavior. + """ + + jstype: "FieldOptionsJsType" = aristaproto.enum_field(6) + """ + The jstype option determines the JavaScript type used for values of the + field. The option is permitted only for 64 bit integral and fixed types + (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING + is represented as JavaScript string, which avoids loss of precision that + can happen when a large value is converted to a floating point JavaScript. + Specifying JS_NUMBER for the jstype causes the generated JavaScript code to + use the JavaScript "number" type. The behavior of the default option + JS_NORMAL is implementation dependent. + + This option is an enum to permit additional types to be added, e.g. + goog.math.Integer. + """ + + lazy: bool = aristaproto.bool_field(5) + """ + Should this field be parsed lazily? Lazy applies only to message-type + fields. It means that when the outer message is initially parsed, the + inner message's contents will not be parsed but instead stored in encoded + form. The inner message will actually be parsed when it is first accessed. + + This is only a hint. Implementations are free to choose whether to use + eager or lazy parsing regardless of the value of this option. However, + setting this option true suggests that the protocol author believes that + using lazy parsing on this field is worth the additional bookkeeping + overhead typically needed to implement it. + + This option does not affect the public interface of any generated code; + all method signatures remain the same. Furthermore, thread-safety of the + interface is not affected by this option; const methods remain safe to + call from multiple threads concurrently, while non-const methods continue + to require exclusive access. + + Note that lazy message fields are still eagerly verified to check + ill-formed wireformat or missing required fields. Calling IsInitialized() + on the outer message would fail if the inner message has missing required + fields. Failed verification would result in parsing failure (except when + uninitialized messages are acceptable). + """ + + unverified_lazy: bool = aristaproto.bool_field(15) + """ + unverified_lazy does no correctness checks on the byte stream. This should + only be used where lazy with verification is prohibitive for performance + reasons. + """ + + deprecated: bool = aristaproto.bool_field(3) + """ + Is this field deprecated? + Depending on the target platform, this can emit Deprecated annotations + for accessors, or it will be completely ignored; in the very least, this + is a formalization for deprecating fields. + """ + + weak: bool = aristaproto.bool_field(10) + """For Google-internal migration only. Do not use.""" + + debug_redact: bool = aristaproto.bool_field(16) + """ + Indicate that the field value should not be printed out when using debug + formats, e.g. when the field contains sensitive credentials. + """ + + retention: "FieldOptionsOptionRetention" = aristaproto.enum_field(17) + targets: List["FieldOptionsOptionTargetType"] = aristaproto.enum_field(19) + edition_defaults: List["FieldOptionsEditionDefault"] = aristaproto.message_field(20) + features: "FeatureSet" = aristaproto.message_field(21) + """Any features defined in the specific edition.""" + + feature_support: "FieldOptionsFeatureSupport" = aristaproto.message_field(22) + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + +@dataclass(eq=False, repr=False) +class FieldOptionsEditionDefault(aristaproto.Message): + edition: "Edition" = aristaproto.enum_field(3) + value: str = aristaproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class FieldOptionsFeatureSupport(aristaproto.Message): + """Information about the support window of a feature.""" + + edition_introduced: "Edition" = aristaproto.enum_field(1) + """ + The edition that this feature was first available in. In editions + earlier than this one, the default assigned to EDITION_LEGACY will be + used, and proto files will not be able to override it. + """ + + edition_deprecated: "Edition" = aristaproto.enum_field(2) + """ + The edition this feature becomes deprecated in. Using this after this + edition may trigger warnings. + """ + + deprecation_warning: str = aristaproto.string_field(3) + """ + The deprecation warning text if this feature is used after the edition it + was marked deprecated in. + """ + + edition_removed: "Edition" = aristaproto.enum_field(4) + """ + The edition this feature is no longer available in. In editions after + this one, the last default assigned will be used, and proto files will + not be able to override it. + """ + + +@dataclass(eq=False, repr=False) +class OneofOptions(aristaproto.Message): + features: "FeatureSet" = aristaproto.message_field(1) + """Any features defined in the specific edition.""" + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + +@dataclass(eq=False, repr=False) +class EnumOptions(aristaproto.Message): + allow_alias: bool = aristaproto.bool_field(2) + """ + Set this option to true to allow mapping different tag names to the same + value. + """ + + deprecated: bool = aristaproto.bool_field(3) + """ + Is this enum deprecated? + Depending on the target platform, this can emit Deprecated annotations + for the enum, or it will be completely ignored; in the very least, this + is a formalization for deprecating enums. + """ + + deprecated_legacy_json_field_conflicts: bool = aristaproto.bool_field(6) + """ + Enable the legacy handling of JSON field name conflicts. This lowercases + and strips underscored from the fields before comparison in proto3 only. + The new behavior takes `json_name` into account and applies to proto2 as + well. + TODO Remove this legacy behavior once downstream teams have + had time to migrate. + """ + + features: "FeatureSet" = aristaproto.message_field(7) + """Any features defined in the specific edition.""" + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + def __post_init__(self) -> None: + super().__post_init__() + if self.is_set("deprecated_legacy_json_field_conflicts"): + warnings.warn( + "EnumOptions.deprecated_legacy_json_field_conflicts is deprecated", + DeprecationWarning, + ) + + +@dataclass(eq=False, repr=False) +class EnumValueOptions(aristaproto.Message): + deprecated: bool = aristaproto.bool_field(1) + """ + Is this enum value deprecated? + Depending on the target platform, this can emit Deprecated annotations + for the enum value, or it will be completely ignored; in the very least, + this is a formalization for deprecating enum values. + """ + + features: "FeatureSet" = aristaproto.message_field(2) + """Any features defined in the specific edition.""" + + debug_redact: bool = aristaproto.bool_field(3) + """ + Indicate that fields annotated with this enum value should not be printed + out when using debug formats, e.g. when the field contains sensitive + credentials. + """ + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + +@dataclass(eq=False, repr=False) +class ServiceOptions(aristaproto.Message): + features: "FeatureSet" = aristaproto.message_field(34) + """Any features defined in the specific edition.""" + + deprecated: bool = aristaproto.bool_field(33) + """ + Is this service deprecated? + Depending on the target platform, this can emit Deprecated annotations + for the service, or it will be completely ignored; in the very least, + this is a formalization for deprecating services. + """ + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + +@dataclass(eq=False, repr=False) +class MethodOptions(aristaproto.Message): + deprecated: bool = aristaproto.bool_field(33) + """ + Is this method deprecated? + Depending on the target platform, this can emit Deprecated annotations + for the method, or it will be completely ignored; in the very least, + this is a formalization for deprecating methods. + """ + + idempotency_level: "MethodOptionsIdempotencyLevel" = aristaproto.enum_field(34) + features: "FeatureSet" = aristaproto.message_field(35) + """Any features defined in the specific edition.""" + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + +@dataclass(eq=False, repr=False) +class UninterpretedOption(aristaproto.Message): + """ + A message representing a option the parser does not recognize. This only + appears in options protos created by the compiler::Parser class. + DescriptorPool resolves these when building Descriptor objects. Therefore, + options protos in descriptor objects (e.g. returned by Descriptor::options(), + or produced by Descriptor::CopyTo()) will never have UninterpretedOptions + in them. + """ + + name: List["UninterpretedOptionNamePart"] = aristaproto.message_field(2) + identifier_value: str = aristaproto.string_field(3) + """ + The value of the uninterpreted option, in whatever type the tokenizer + identified it as during parsing. Exactly one of these should be set. + """ + + positive_int_value: int = aristaproto.uint64_field(4) + negative_int_value: int = aristaproto.int64_field(5) + double_value: float = aristaproto.double_field(6) + string_value: bytes = aristaproto.bytes_field(7) + aggregate_value: str = aristaproto.string_field(8) + + +@dataclass(eq=False, repr=False) +class UninterpretedOptionNamePart(aristaproto.Message): + """ + The name of the uninterpreted option. Each string represents a segment in + a dot-separated name. is_extension is true iff a segment represents an + extension (denoted with parentheses in options specs in .proto files). + E.g.,{ ["foo", false], ["bar.baz", true], ["moo", false] } represents + "foo.(bar.baz).moo". + """ + + name_part: str = aristaproto.string_field(1) + is_extension: bool = aristaproto.bool_field(2) + + +@dataclass(eq=False, repr=False) +class FeatureSet(aristaproto.Message): + """ + TODO Enums in C++ gencode (and potentially other languages) are + not well scoped. This means that each of the feature enums below can clash + with each other. The short names we've chosen maximize call-site + readability, but leave us very open to this scenario. A future feature will + be designed and implemented to handle this, hopefully before we ever hit a + conflict here. + """ + + field_presence: "FeatureSetFieldPresence" = aristaproto.enum_field(1) + enum_type: "FeatureSetEnumType" = aristaproto.enum_field(2) + repeated_field_encoding: "FeatureSetRepeatedFieldEncoding" = aristaproto.enum_field( + 3 + ) + utf8_validation: "FeatureSetUtf8Validation" = aristaproto.enum_field(4) + message_encoding: "FeatureSetMessageEncoding" = aristaproto.enum_field(5) + json_format: "FeatureSetJsonFormat" = aristaproto.enum_field(6) + + +@dataclass(eq=False, repr=False) +class FeatureSetDefaults(aristaproto.Message): + """ + A compiled specification for the defaults of a set of features. These + messages are generated from FeatureSet extensions and can be used to seed + feature resolution. The resolution with this object becomes a simple search + for the closest matching edition, followed by proto merges. + """ + + defaults: List[ + "FeatureSetDefaultsFeatureSetEditionDefault" + ] = aristaproto.message_field(1) + minimum_edition: "Edition" = aristaproto.enum_field(4) + """ + The minimum supported edition (inclusive) when this was constructed. + Editions before this will not have defaults. + """ + + maximum_edition: "Edition" = aristaproto.enum_field(5) + """ + The maximum known edition (inclusive) when this was constructed. Editions + after this will not have reliable defaults. + """ + + +@dataclass(eq=False, repr=False) +class FeatureSetDefaultsFeatureSetEditionDefault(aristaproto.Message): + """ + A map from every known edition with a unique set of defaults to its + defaults. Not all editions may be contained here. For a given edition, + the defaults at the closest matching edition ordered at or before it should + be used. This field must be in strict ascending order by edition. + """ + + edition: "Edition" = aristaproto.enum_field(3) + overridable_features: "FeatureSet" = aristaproto.message_field(4) + """Defaults of features that can be overridden in this edition.""" + + fixed_features: "FeatureSet" = aristaproto.message_field(5) + """Defaults of features that can't be overridden in this edition.""" + + features: "FeatureSet" = aristaproto.message_field(2) + """ + TODO Deprecate and remove this field, which is just the + above two merged. + """ + + +@dataclass(eq=False, repr=False) +class SourceCodeInfo(aristaproto.Message): + """ + Encapsulates information about the original source file from which a + FileDescriptorProto was generated. + """ + + location: List["SourceCodeInfoLocation"] = aristaproto.message_field(1) + """ + A Location identifies a piece of source code in a .proto file which + corresponds to a particular definition. This information is intended + to be useful to IDEs, code indexers, documentation generators, and similar + tools. + + For example, say we have a file like: + message Foo { + optional string foo = 1; + } + Let's look at just the field definition: + optional string foo = 1; + ^ ^^ ^^ ^ ^^^ + a bc de f ghi + We have the following locations: + span path represents + [a,i) [ 4, 0, 2, 0 ] The whole field definition. + [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). + [c,d) [ 4, 0, 2, 0, 5 ] The type (string). + [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). + [g,h) [ 4, 0, 2, 0, 3 ] The number (1). + + Notes: + - A location may refer to a repeated field itself (i.e. not to any + particular index within it). This is used whenever a set of elements are + logically enclosed in a single code segment. For example, an entire + extend block (possibly containing multiple extension definitions) will + have an outer location whose path refers to the "extensions" repeated + field without an index. + - Multiple locations may have the same path. This happens when a single + logical declaration is spread out across multiple places. The most + obvious example is the "extend" block again -- there may be multiple + extend blocks in the same scope, each of which will have the same path. + - A location's span is not always a subset of its parent's span. For + example, the "extendee" of an extension declaration appears at the + beginning of the "extend" block and is shared by all extensions within + the block. + - Just because a location's span is a subset of some other location's span + does not mean that it is a descendant. For example, a "group" defines + both a type and a field in a single declaration. Thus, the locations + corresponding to the type and field and their components will overlap. + - Code which tries to interpret locations should probably be designed to + ignore those that it doesn't understand, as more types of locations could + be recorded in the future. + """ + + +@dataclass(eq=False, repr=False) +class SourceCodeInfoLocation(aristaproto.Message): + path: List[int] = aristaproto.int32_field(1) + """ + Identifies which part of the FileDescriptorProto was defined at this + location. + + Each element is a field number or an index. They form a path from + the root FileDescriptorProto to the place where the definition appears. + For example, this path: + [ 4, 3, 2, 7, 1 ] + refers to: + file.message_type(3) // 4, 3 + .field(7) // 2, 7 + .name() // 1 + This is because FileDescriptorProto.message_type has field number 4: + repeated DescriptorProto message_type = 4; + and DescriptorProto.field has field number 2: + repeated FieldDescriptorProto field = 2; + and FieldDescriptorProto.name has field number 1: + optional string name = 1; + + Thus, the above path gives the location of a field name. If we removed + the last element: + [ 4, 3, 2, 7 ] + this path refers to the whole field declaration (from the beginning + of the label to the terminating semicolon). + """ + + span: List[int] = aristaproto.int32_field(2) + """ + Always has exactly three or four elements: start line, start column, + end line (optional, otherwise assumed same as start line), end column. + These are packed into a single field for efficiency. Note that line + and column numbers are zero-based -- typically you will want to add + 1 to each before displaying to a user. + """ + + leading_comments: str = aristaproto.string_field(3) + """ + If this SourceCodeInfo represents a complete declaration, these are any + comments appearing before and after the declaration which appear to be + attached to the declaration. + + A series of line comments appearing on consecutive lines, with no other + tokens appearing on those lines, will be treated as a single comment. + + leading_detached_comments will keep paragraphs of comments that appear + before (but not connected to) the current element. Each paragraph, + separated by empty lines, will be one comment element in the repeated + field. + + Only the comment content is provided; comment markers (e.g. //) are + stripped out. For block comments, leading whitespace and an asterisk + will be stripped from the beginning of each line other than the first. + Newlines are included in the output. + + Examples: + + optional int32 foo = 1; // Comment attached to foo. + // Comment attached to bar. + optional int32 bar = 2; + + optional string baz = 3; + // Comment attached to baz. + // Another line attached to baz. + + // Comment attached to moo. + // + // Another line attached to moo. + optional double moo = 4; + + // Detached comment for corge. This is not leading or trailing comments + // to moo or corge because there are blank lines separating it from + // both. + + // Detached comment for corge paragraph 2. + + optional string corge = 5; + /* Block comment attached + * to corge. Leading asterisks + * will be removed. */ + /* Block comment attached to + * grault. */ + optional int32 grault = 6; + + // ignored detached comments. + """ + + trailing_comments: str = aristaproto.string_field(4) + leading_detached_comments: List[str] = aristaproto.string_field(6) + + +@dataclass(eq=False, repr=False) +class GeneratedCodeInfo(aristaproto.Message): + """ + Describes the relationship between generated code and its original source + file. A GeneratedCodeInfo message is associated with only one generated + source file, but may contain references to different source .proto files. + """ + + annotation: List["GeneratedCodeInfoAnnotation"] = aristaproto.message_field(1) + """ + An Annotation connects some span of text in generated code to an element + of its generating .proto file. + """ + + +@dataclass(eq=False, repr=False) +class GeneratedCodeInfoAnnotation(aristaproto.Message): + path: List[int] = aristaproto.int32_field(1) + """ + Identifies the element in the original source .proto file. This field + is formatted the same as SourceCodeInfo.Location.path. + """ + + source_file: str = aristaproto.string_field(2) + """Identifies the filesystem path to the original source .proto.""" + + begin: int = aristaproto.int32_field(3) + """ + Identifies the starting offset in bytes in the generated code + that relates to the identified object. + """ + + end: int = aristaproto.int32_field(4) + """ + Identifies the ending offset in bytes in the generated code that + relates to the identified object. The end offset should be one past + the last relevant byte (so the length of the text = end - begin). + """ + + semantic: "GeneratedCodeInfoAnnotationSemantic" = aristaproto.enum_field(5) + + +@dataclass(eq=False, repr=False) +class Duration(aristaproto.Message): + """ + A Duration represents a signed, fixed-length span of time represented + as a count of seconds and fractions of seconds at nanosecond + resolution. It is independent of any calendar and concepts like "day" + or "month". It is related to Timestamp in that the difference between + two Timestamp values is a Duration and it can be added or subtracted + from a Timestamp. Range is approximately +-10,000 years. + + # Examples + + Example 1: Compute Duration from two Timestamps in pseudo code. + + Timestamp start = ...; + Timestamp end = ...; + Duration duration = ...; + + duration.seconds = end.seconds - start.seconds; + duration.nanos = end.nanos - start.nanos; + + if (duration.seconds < 0 && duration.nanos > 0) { + duration.seconds += 1; + duration.nanos -= 1000000000; + } else if (duration.seconds > 0 && duration.nanos < 0) { + duration.seconds -= 1; + duration.nanos += 1000000000; + } + + Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. + + Timestamp start = ...; + Duration duration = ...; + Timestamp end = ...; + + end.seconds = start.seconds + duration.seconds; + end.nanos = start.nanos + duration.nanos; + + if (end.nanos < 0) { + end.seconds -= 1; + end.nanos += 1000000000; + } else if (end.nanos >= 1000000000) { + end.seconds += 1; + end.nanos -= 1000000000; + } + + Example 3: Compute Duration from datetime.timedelta in Python. + + td = datetime.timedelta(days=3, minutes=10) + duration = Duration() + duration.FromTimedelta(td) + + # JSON Mapping + + In JSON format, the Duration type is encoded as a string rather than an + object, where the string ends in the suffix "s" (indicating seconds) and + is preceded by the number of seconds, with nanoseconds expressed as + fractional seconds. For example, 3 seconds with 0 nanoseconds should be + encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should + be expressed in JSON format as "3.000000001s", and 3 seconds and 1 + microsecond should be expressed in JSON format as "3.000001s". + """ + + seconds: int = aristaproto.int64_field(1) + """ + Signed seconds of the span of time. Must be from -315,576,000,000 + to +315,576,000,000 inclusive. Note: these bounds are computed from: + 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + """ + + nanos: int = aristaproto.int32_field(2) + """ + Signed fractions of a second at nanosecond resolution of the span + of time. Durations less than one second are represented with a 0 + `seconds` field and a positive or negative `nanos` field. For durations + of one second or more, a non-zero value for the `nanos` field must be + of the same sign as the `seconds` field. Must be from -999,999,999 + to +999,999,999 inclusive. + """ + + +@dataclass(eq=False, repr=False) +class Empty(aristaproto.Message): + """ + A generic empty message that you can re-use to avoid defining duplicated + empty messages in your APIs. A typical example is to use it as the request + or the response type of an API method. For instance: + + service Foo { + rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); + } + """ + + pass + + +@dataclass(eq=False, repr=False) +class FieldMask(aristaproto.Message): + """ + `FieldMask` represents a set of symbolic field paths, for example: + + paths: "f.a" + paths: "f.b.d" + + Here `f` represents a field in some root message, `a` and `b` + fields in the message found in `f`, and `d` a field found in the + message in `f.b`. + + Field masks are used to specify a subset of fields that should be + returned by a get operation or modified by an update operation. + Field masks also have a custom JSON encoding (see below). + + # Field Masks in Projections + + When used in the context of a projection, a response message or + sub-message is filtered by the API to only contain those fields as + specified in the mask. For example, if the mask in the previous + example is applied to a response message as follows: + + f { + a : 22 + b { + d : 1 + x : 2 + } + y : 13 + } + z: 8 + + The result will not contain specific values for fields x,y and z + (their value will be set to the default, and omitted in proto text + output): + + + f { + a : 22 + b { + d : 1 + } + } + + A repeated field is not allowed except at the last position of a + paths string. + + If a FieldMask object is not present in a get operation, the + operation applies to all fields (as if a FieldMask of all fields + had been specified). + + Note that a field mask does not necessarily apply to the + top-level response message. In case of a REST get operation, the + field mask applies directly to the response, but in case of a REST + list operation, the mask instead applies to each individual message + in the returned resource list. In case of a REST custom method, + other definitions may be used. Where the mask applies will be + clearly documented together with its declaration in the API. In + any case, the effect on the returned resource/resources is required + behavior for APIs. + + # Field Masks in Update Operations + + A field mask in update operations specifies which fields of the + targeted resource are going to be updated. The API is required + to only change the values of the fields as specified in the mask + and leave the others untouched. If a resource is passed in to + describe the updated values, the API ignores the values of all + fields not covered by the mask. + + If a repeated field is specified for an update operation, new values will + be appended to the existing repeated field in the target resource. Note that + a repeated field is only allowed in the last position of a `paths` string. + + If a sub-message is specified in the last position of the field mask for an + update operation, then new value will be merged into the existing sub-message + in the target resource. + + For example, given the target message: + + f { + b { + d: 1 + x: 2 + } + c: [1] + } + + And an update message: + + f { + b { + d: 10 + } + c: [2] + } + + then if the field mask is: + + paths: ["f.b", "f.c"] + + then the result will be: + + f { + b { + d: 10 + x: 2 + } + c: [1, 2] + } + + An implementation may provide options to override this default behavior for + repeated and message fields. + + In order to reset a field's value to the default, the field must + be in the mask and set to the default value in the provided resource. + Hence, in order to reset all fields of a resource, provide a default + instance of the resource and set all fields in the mask, or do + not provide a mask as described below. + + If a field mask is not present on update, the operation applies to + all fields (as if a field mask of all fields has been specified). + Note that in the presence of schema evolution, this may mean that + fields the client does not know and has therefore not filled into + the request will be reset to their default. If this is unwanted + behavior, a specific service may require a client to always specify + a field mask, producing an error if not. + + As with get operations, the location of the resource which + describes the updated values in the request message depends on the + operation kind. In any case, the effect of the field mask is + required to be honored by the API. + + ## Considerations for HTTP REST + + The HTTP kind of an update operation which uses a field mask must + be set to PATCH instead of PUT in order to satisfy HTTP semantics + (PUT must only be used for full updates). + + # JSON Encoding of Field Masks + + In JSON, a field mask is encoded as a single string where paths are + separated by a comma. Fields name in each path are converted + to/from lower-camel naming conventions. + + As an example, consider the following message declarations: + + message Profile { + User user = 1; + Photo photo = 2; + } + message User { + string display_name = 1; + string address = 2; + } + + In proto a field mask for `Profile` may look as such: + + mask { + paths: "user.display_name" + paths: "photo" + } + + In JSON, the same mask is represented as below: + + { + mask: "user.displayName,photo" + } + + # Field Masks and Oneof Fields + + Field masks treat fields in oneofs just as regular fields. Consider the + following message: + + message SampleMessage { + oneof test_oneof { + string name = 4; + SubMessage sub_message = 9; + } + } + + The field mask can be: + + mask { + paths: "name" + } + + Or: + + mask { + paths: "sub_message" + } + + Note that oneof type names ("test_oneof" in this case) cannot be used in + paths. + + ## Field Mask Verification + + The implementation of any API method which has a FieldMask type field in the + request should verify the included field paths, and return an + `INVALID_ARGUMENT` error if any path is unmappable. + """ + + paths: List[str] = aristaproto.string_field(1) + """The set of field mask paths.""" + + +@dataclass(eq=False, repr=False) +class Struct(aristaproto.Message): + """ + `Struct` represents a structured data value, consisting of fields + which map to dynamically typed values. In some languages, `Struct` + might be supported by a native representation. For example, in + scripting languages like JS a struct is represented as an + object. The details of that representation are described together + with the proto support for the language. + + The JSON representation for `Struct` is JSON object. + """ + + fields: Dict[str, "Value"] = aristaproto.map_field( + 1, aristaproto.TYPE_STRING, aristaproto.TYPE_MESSAGE + ) + """Unordered map of dynamically typed values.""" + + @hybridmethod + def from_dict(cls: "type[Self]", value: Mapping[str, Any]) -> Self: # type: ignore + self = cls() + return self.from_dict(value) + + @from_dict.instancemethod + def from_dict(self, value: Mapping[str, Any]) -> Self: + fields = {**value} + for k in fields: + if hasattr(fields[k], "from_dict"): + fields[k] = fields[k].from_dict() + + self.fields = fields + return self + + def to_dict( + self, + casing: aristaproto.Casing = aristaproto.Casing.CAMEL, + include_default_values: bool = False, + ) -> Dict[str, Any]: + output = {**self.fields} + for k in self.fields: + if hasattr(self.fields[k], "to_dict"): + output[k] = self.fields[k].to_dict(casing, include_default_values) + return output + + +@dataclass(eq=False, repr=False) +class Value(aristaproto.Message): + """ + `Value` represents a dynamically typed value which can be either + null, a number, a string, a boolean, a recursive struct value, or a + list of values. A producer of value is expected to set one of these + variants. Absence of any variant indicates an error. + + The JSON representation for `Value` is JSON value. + """ + + null_value: Optional["NullValue"] = aristaproto.enum_field( + 1, optional=True, group="kind" + ) + """Represents a null value.""" + + number_value: Optional[float] = aristaproto.double_field( + 2, optional=True, group="kind" + ) + """Represents a double value.""" + + string_value: Optional[str] = aristaproto.string_field( + 3, optional=True, group="kind" + ) + """Represents a string value.""" + + bool_value: Optional[bool] = aristaproto.bool_field(4, optional=True, group="kind") + """Represents a boolean value.""" + + struct_value: Optional["Struct"] = aristaproto.message_field( + 5, optional=True, group="kind" + ) + """Represents a structured value.""" + + list_value: Optional["ListValue"] = aristaproto.message_field( + 6, optional=True, group="kind" + ) + """Represents a repeated `Value`.""" + + @root_validator() + def check_oneof(cls, values): + return cls._validate_field_groups(values) + + +@dataclass(eq=False, repr=False) +class ListValue(aristaproto.Message): + """ + `ListValue` is a wrapper around a repeated field of values. + + The JSON representation for `ListValue` is JSON array. + """ + + values: List["Value"] = aristaproto.message_field(1) + """Repeated field of dynamically typed values.""" + + +@dataclass(eq=False, repr=False) +class Timestamp(aristaproto.Message): + """ + A Timestamp represents a point in time independent of any time zone or local + calendar, encoded as a count of seconds and fractions of seconds at + nanosecond resolution. The count is relative to an epoch at UTC midnight on + January 1, 1970, in the proleptic Gregorian calendar which extends the + Gregorian calendar backwards to year one. + + All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap + second table is needed for interpretation, using a [24-hour linear + smear](https://developers.google.com/time/smear). + + The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By + restricting to that range, we ensure that we can convert to and from [RFC + 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. + + # Examples + + Example 1: Compute Timestamp from POSIX `time()`. + + Timestamp timestamp; + timestamp.set_seconds(time(NULL)); + timestamp.set_nanos(0); + + Example 2: Compute Timestamp from POSIX `gettimeofday()`. + + struct timeval tv; + gettimeofday(&tv, NULL); + + Timestamp timestamp; + timestamp.set_seconds(tv.tv_sec); + timestamp.set_nanos(tv.tv_usec * 1000); + + Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. + + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + + // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z + // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. + Timestamp timestamp; + timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); + timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); + + Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. + + long millis = System.currentTimeMillis(); + + Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) + .setNanos((int) ((millis % 1000) * 1000000)).build(); + + Example 5: Compute Timestamp from Java `Instant.now()`. + + Instant now = Instant.now(); + + Timestamp timestamp = + Timestamp.newBuilder().setSeconds(now.getEpochSecond()) + .setNanos(now.getNano()).build(); + + Example 6: Compute Timestamp from current time in Python. + + timestamp = Timestamp() + timestamp.GetCurrentTime() + + # JSON Mapping + + In JSON format, the Timestamp type is encoded as a string in the + [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the + format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" + where {year} is always expressed using four digits while {month}, {day}, + {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional + seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), + are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone + is required. A proto3 JSON serializer should always use UTC (as indicated by + "Z") when printing the Timestamp type and a proto3 JSON parser should be + able to accept both UTC and other timezones (as indicated by an offset). + + For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past + 01:30 UTC on January 15, 2017. + + In JavaScript, one can convert a Date object to this format using the + standard + [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) + method. In Python, a standard `datetime.datetime` object can be converted + to this format using + [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with + the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use + the Joda Time's [`ISODateTimeFormat.dateTime()`]( + http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime() + ) to obtain a formatter capable of generating timestamps in this format. + """ + + seconds: int = aristaproto.int64_field(1) + """ + Represents seconds of UTC time since Unix epoch + 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + 9999-12-31T23:59:59Z inclusive. + """ + + nanos: int = aristaproto.int32_field(2) + """ + Non-negative fractions of a second at nanosecond resolution. Negative + second values with fractions must still have non-negative nanos values + that count forward in time. Must be from 0 to 999,999,999 + inclusive. + """ + + +@dataclass(eq=False, repr=False) +class DoubleValue(aristaproto.Message): + """ + Wrapper message for `double`. + + The JSON representation for `DoubleValue` is JSON number. + """ + + value: float = aristaproto.double_field(1) + """The double value.""" + + +@dataclass(eq=False, repr=False) +class FloatValue(aristaproto.Message): + """ + Wrapper message for `float`. + + The JSON representation for `FloatValue` is JSON number. + """ + + value: float = aristaproto.float_field(1) + """The float value.""" + + +@dataclass(eq=False, repr=False) +class Int64Value(aristaproto.Message): + """ + Wrapper message for `int64`. + + The JSON representation for `Int64Value` is JSON string. + """ + + value: int = aristaproto.int64_field(1) + """The int64 value.""" + + +@dataclass(eq=False, repr=False) +class UInt64Value(aristaproto.Message): + """ + Wrapper message for `uint64`. + + The JSON representation for `UInt64Value` is JSON string. + """ + + value: int = aristaproto.uint64_field(1) + """The uint64 value.""" + + +@dataclass(eq=False, repr=False) +class Int32Value(aristaproto.Message): + """ + Wrapper message for `int32`. + + The JSON representation for `Int32Value` is JSON number. + """ + + value: int = aristaproto.int32_field(1) + """The int32 value.""" + + +@dataclass(eq=False, repr=False) +class UInt32Value(aristaproto.Message): + """ + Wrapper message for `uint32`. + + The JSON representation for `UInt32Value` is JSON number. + """ + + value: int = aristaproto.uint32_field(1) + """The uint32 value.""" + + +@dataclass(eq=False, repr=False) +class BoolValue(aristaproto.Message): + """ + Wrapper message for `bool`. + + The JSON representation for `BoolValue` is JSON `true` and `false`. + """ + + value: bool = aristaproto.bool_field(1) + """The bool value.""" + + +@dataclass(eq=False, repr=False) +class StringValue(aristaproto.Message): + """ + Wrapper message for `string`. + + The JSON representation for `StringValue` is JSON string. + """ + + value: str = aristaproto.string_field(1) + """The string value.""" + + +@dataclass(eq=False, repr=False) +class BytesValue(aristaproto.Message): + """ + Wrapper message for `bytes`. + + The JSON representation for `BytesValue` is JSON string. + """ + + value: bytes = aristaproto.bytes_field(1) + """The bytes value.""" + + +Type.__pydantic_model__.update_forward_refs() # type: ignore +Field.__pydantic_model__.update_forward_refs() # type: ignore +Enum.__pydantic_model__.update_forward_refs() # type: ignore +EnumValue.__pydantic_model__.update_forward_refs() # type: ignore +Option.__pydantic_model__.update_forward_refs() # type: ignore +Api.__pydantic_model__.update_forward_refs() # type: ignore +Method.__pydantic_model__.update_forward_refs() # type: ignore +FileDescriptorSet.__pydantic_model__.update_forward_refs() # type: ignore +FileDescriptorProto.__pydantic_model__.update_forward_refs() # type: ignore +DescriptorProto.__pydantic_model__.update_forward_refs() # type: ignore +DescriptorProtoExtensionRange.__pydantic_model__.update_forward_refs() # type: ignore +ExtensionRangeOptions.__pydantic_model__.update_forward_refs() # type: ignore +FieldDescriptorProto.__pydantic_model__.update_forward_refs() # type: ignore +OneofDescriptorProto.__pydantic_model__.update_forward_refs() # type: ignore +EnumDescriptorProto.__pydantic_model__.update_forward_refs() # type: ignore +EnumValueDescriptorProto.__pydantic_model__.update_forward_refs() # type: ignore +ServiceDescriptorProto.__pydantic_model__.update_forward_refs() # type: ignore +MethodDescriptorProto.__pydantic_model__.update_forward_refs() # type: ignore +FileOptions.__pydantic_model__.update_forward_refs() # type: ignore +MessageOptions.__pydantic_model__.update_forward_refs() # type: ignore +FieldOptions.__pydantic_model__.update_forward_refs() # type: ignore +FieldOptionsEditionDefault.__pydantic_model__.update_forward_refs() # type: ignore +FieldOptionsFeatureSupport.__pydantic_model__.update_forward_refs() # type: ignore +OneofOptions.__pydantic_model__.update_forward_refs() # type: ignore +EnumOptions.__pydantic_model__.update_forward_refs() # type: ignore +EnumValueOptions.__pydantic_model__.update_forward_refs() # type: ignore +ServiceOptions.__pydantic_model__.update_forward_refs() # type: ignore +MethodOptions.__pydantic_model__.update_forward_refs() # type: ignore +UninterpretedOption.__pydantic_model__.update_forward_refs() # type: ignore +FeatureSet.__pydantic_model__.update_forward_refs() # type: ignore +FeatureSetDefaults.__pydantic_model__.update_forward_refs() # type: ignore +FeatureSetDefaultsFeatureSetEditionDefault.__pydantic_model__.update_forward_refs() # type: ignore +SourceCodeInfo.__pydantic_model__.update_forward_refs() # type: ignore +GeneratedCodeInfo.__pydantic_model__.update_forward_refs() # type: ignore +GeneratedCodeInfoAnnotation.__pydantic_model__.update_forward_refs() # type: ignore +Struct.__pydantic_model__.update_forward_refs() # type: ignore +Value.__pydantic_model__.update_forward_refs() # type: ignore +ListValue.__pydantic_model__.update_forward_refs() # type: ignore diff --git a/src/aristaproto/lib/pydantic/google/protobuf/compiler/__init__.py b/src/aristaproto/lib/pydantic/google/protobuf/compiler/__init__.py new file mode 100644 index 0000000..495c555 --- /dev/null +++ b/src/aristaproto/lib/pydantic/google/protobuf/compiler/__init__.py @@ -0,0 +1,210 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: google/protobuf/compiler/plugin.proto +# plugin: python-aristaproto +# This file has been @generated + +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from dataclasses import dataclass +else: + from pydantic.dataclasses import dataclass + +from typing import List + +import aristaproto +import aristaproto.lib.pydantic.google.protobuf as aristaproto_lib_pydantic_google_protobuf + + +class CodeGeneratorResponseFeature(aristaproto.Enum): + """Sync with code_generator.h.""" + + FEATURE_NONE = 0 + FEATURE_PROTO3_OPTIONAL = 1 + FEATURE_SUPPORTS_EDITIONS = 2 + + +@dataclass(eq=False, repr=False) +class Version(aristaproto.Message): + """The version number of protocol compiler.""" + + major: int = aristaproto.int32_field(1) + minor: int = aristaproto.int32_field(2) + patch: int = aristaproto.int32_field(3) + suffix: str = aristaproto.string_field(4) + """ + A suffix for alpha, beta or rc release, e.g., "alpha-1", "rc2". It should + be empty for mainline stable releases. + """ + + +@dataclass(eq=False, repr=False) +class CodeGeneratorRequest(aristaproto.Message): + """An encoded CodeGeneratorRequest is written to the plugin's stdin.""" + + file_to_generate: List[str] = aristaproto.string_field(1) + """ + The .proto files that were explicitly listed on the command-line. The + code generator should generate code only for these files. Each file's + descriptor will be included in proto_file, below. + """ + + parameter: str = aristaproto.string_field(2) + """The generator parameter passed on the command-line.""" + + proto_file: List[ + "aristaproto_lib_pydantic_google_protobuf.FileDescriptorProto" + ] = aristaproto.message_field(15) + """ + FileDescriptorProtos for all files in files_to_generate and everything + they import. The files will appear in topological order, so each file + appears before any file that imports it. + + Note: the files listed in files_to_generate will include runtime-retention + options only, but all other files will include source-retention options. + The source_file_descriptors field below is available in case you need + source-retention options for files_to_generate. + + protoc guarantees that all proto_files will be written after + the fields above, even though this is not technically guaranteed by the + protobuf wire format. This theoretically could allow a plugin to stream + in the FileDescriptorProtos and handle them one by one rather than read + the entire set into memory at once. However, as of this writing, this + is not similarly optimized on protoc's end -- it will store all fields in + memory at once before sending them to the plugin. + + Type names of fields and extensions in the FileDescriptorProto are always + fully qualified. + """ + + source_file_descriptors: List[ + "aristaproto_lib_pydantic_google_protobuf.FileDescriptorProto" + ] = aristaproto.message_field(17) + """ + File descriptors with all options, including source-retention options. + These descriptors are only provided for the files listed in + files_to_generate. + """ + + compiler_version: "Version" = aristaproto.message_field(3) + """The version number of protocol compiler.""" + + +@dataclass(eq=False, repr=False) +class CodeGeneratorResponse(aristaproto.Message): + """The plugin writes an encoded CodeGeneratorResponse to stdout.""" + + error: str = aristaproto.string_field(1) + """ + Error message. If non-empty, code generation failed. The plugin process + should exit with status code zero even if it reports an error in this way. + + This should be used to indicate errors in .proto files which prevent the + code generator from generating correct code. Errors which indicate a + problem in protoc itself -- such as the input CodeGeneratorRequest being + unparseable -- should be reported by writing a message to stderr and + exiting with a non-zero status code. + """ + + supported_features: int = aristaproto.uint64_field(2) + """ + A bitmask of supported features that the code generator supports. + This is a bitwise "or" of values from the Feature enum. + """ + + minimum_edition: int = aristaproto.int32_field(3) + """ + The minimum edition this plugin supports. This will be treated as an + Edition enum, but we want to allow unknown values. It should be specified + according the edition enum value, *not* the edition number. Only takes + effect for plugins that have FEATURE_SUPPORTS_EDITIONS set. + """ + + maximum_edition: int = aristaproto.int32_field(4) + """ + The maximum edition this plugin supports. This will be treated as an + Edition enum, but we want to allow unknown values. It should be specified + according the edition enum value, *not* the edition number. Only takes + effect for plugins that have FEATURE_SUPPORTS_EDITIONS set. + """ + + file: List["CodeGeneratorResponseFile"] = aristaproto.message_field(15) + + +@dataclass(eq=False, repr=False) +class CodeGeneratorResponseFile(aristaproto.Message): + """Represents a single generated file.""" + + name: str = aristaproto.string_field(1) + """ + The file name, relative to the output directory. The name must not + contain "." or ".." components and must be relative, not be absolute (so, + the file cannot lie outside the output directory). "/" must be used as + the path separator, not "\". + + If the name is omitted, the content will be appended to the previous + file. This allows the generator to break large files into small chunks, + and allows the generated text to be streamed back to protoc so that large + files need not reside completely in memory at one time. Note that as of + this writing protoc does not optimize for this -- it will read the entire + CodeGeneratorResponse before writing files to disk. + """ + + insertion_point: str = aristaproto.string_field(2) + """ + If non-empty, indicates that the named file should already exist, and the + content here is to be inserted into that file at a defined insertion + point. This feature allows a code generator to extend the output + produced by another code generator. The original generator may provide + insertion points by placing special annotations in the file that look + like: + @@protoc_insertion_point(NAME) + The annotation can have arbitrary text before and after it on the line, + which allows it to be placed in a comment. NAME should be replaced with + an identifier naming the point -- this is what other generators will use + as the insertion_point. Code inserted at this point will be placed + immediately above the line containing the insertion point (thus multiple + insertions to the same point will come out in the order they were added). + The double-@ is intended to make it unlikely that the generated code + could contain things that look like insertion points by accident. + + For example, the C++ code generator places the following line in the + .pb.h files that it generates: + // @@protoc_insertion_point(namespace_scope) + This line appears within the scope of the file's package namespace, but + outside of any particular class. Another plugin can then specify the + insertion_point "namespace_scope" to generate additional classes or + other declarations that should be placed in this scope. + + Note that if the line containing the insertion point begins with + whitespace, the same whitespace will be added to every line of the + inserted text. This is useful for languages like Python, where + indentation matters. In these languages, the insertion point comment + should be indented the same amount as any inserted code will need to be + in order to work correctly in that context. + + The code generator that generates the initial file and the one which + inserts into it must both run as part of a single invocation of protoc. + Code generators are executed in the order in which they appear on the + command line. + + If |insertion_point| is present, |name| must also be present. + """ + + content: str = aristaproto.string_field(15) + """The file contents.""" + + generated_code_info: ( + "aristaproto_lib_pydantic_google_protobuf.GeneratedCodeInfo" + ) = aristaproto.message_field(16) + """ + Information describing the file content being inserted. If an insertion + point is used, this information will be appropriately offset and inserted + into the code generation metadata for the generated files. + """ + + +CodeGeneratorRequest.__pydantic_model__.update_forward_refs() # type: ignore +CodeGeneratorResponse.__pydantic_model__.update_forward_refs() # type: ignore +CodeGeneratorResponseFile.__pydantic_model__.update_forward_refs() # type: ignore diff --git a/src/aristaproto/lib/std/__init__.py b/src/aristaproto/lib/std/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/aristaproto/lib/std/__init__.py diff --git a/src/aristaproto/lib/std/google/__init__.py b/src/aristaproto/lib/std/google/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/aristaproto/lib/std/google/__init__.py diff --git a/src/aristaproto/lib/std/google/protobuf/__init__.py b/src/aristaproto/lib/std/google/protobuf/__init__.py new file mode 100644 index 0000000..783676a --- /dev/null +++ b/src/aristaproto/lib/std/google/protobuf/__init__.py @@ -0,0 +1,2526 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: google/protobuf/any.proto, google/protobuf/api.proto, google/protobuf/descriptor.proto, google/protobuf/duration.proto, google/protobuf/empty.proto, google/protobuf/field_mask.proto, google/protobuf/source_context.proto, google/protobuf/struct.proto, google/protobuf/timestamp.proto, google/protobuf/type.proto, google/protobuf/wrappers.proto +# plugin: python-aristaproto + +import warnings +from dataclasses import dataclass +from typing import ( + Dict, + List, + Mapping, +) + +from typing_extensions import Self + +import aristaproto +from aristaproto.utils import hybridmethod + + +class Syntax(aristaproto.Enum): + """The syntax in which a protocol buffer element is defined.""" + + PROTO2 = 0 + """Syntax `proto2`.""" + + PROTO3 = 1 + """Syntax `proto3`.""" + + EDITIONS = 2 + """Syntax `editions`.""" + + +class FieldKind(aristaproto.Enum): + """Basic field types.""" + + TYPE_UNKNOWN = 0 + """Field type unknown.""" + + TYPE_DOUBLE = 1 + """Field type double.""" + + TYPE_FLOAT = 2 + """Field type float.""" + + TYPE_INT64 = 3 + """Field type int64.""" + + TYPE_UINT64 = 4 + """Field type uint64.""" + + TYPE_INT32 = 5 + """Field type int32.""" + + TYPE_FIXED64 = 6 + """Field type fixed64.""" + + TYPE_FIXED32 = 7 + """Field type fixed32.""" + + TYPE_BOOL = 8 + """Field type bool.""" + + TYPE_STRING = 9 + """Field type string.""" + + TYPE_GROUP = 10 + """Field type group. Proto2 syntax only, and deprecated.""" + + TYPE_MESSAGE = 11 + """Field type message.""" + + TYPE_BYTES = 12 + """Field type bytes.""" + + TYPE_UINT32 = 13 + """Field type uint32.""" + + TYPE_ENUM = 14 + """Field type enum.""" + + TYPE_SFIXED32 = 15 + """Field type sfixed32.""" + + TYPE_SFIXED64 = 16 + """Field type sfixed64.""" + + TYPE_SINT32 = 17 + """Field type sint32.""" + + TYPE_SINT64 = 18 + """Field type sint64.""" + + +class FieldCardinality(aristaproto.Enum): + """Whether a field is optional, required, or repeated.""" + + CARDINALITY_UNKNOWN = 0 + """For fields with unknown cardinality.""" + + CARDINALITY_OPTIONAL = 1 + """For optional fields.""" + + CARDINALITY_REQUIRED = 2 + """For required fields. Proto2 syntax only.""" + + CARDINALITY_REPEATED = 3 + """For repeated fields.""" + + +class Edition(aristaproto.Enum): + """The full set of known editions.""" + + UNKNOWN = 0 + """A placeholder for an unknown edition value.""" + + PROTO2 = 998 + """ + Legacy syntax "editions". These pre-date editions, but behave much like + distinct editions. These can't be used to specify the edition of proto + files, but feature definitions must supply proto2/proto3 defaults for + backwards compatibility. + """ + + PROTO3 = 999 + _2023 = 1000 + """ + Editions that have been released. The specific values are arbitrary and + should not be depended on, but they will always be time-ordered for easy + comparison. + """ + + _2024 = 1001 + _1_TEST_ONLY = 1 + """ + Placeholder editions for testing feature resolution. These should not be + used or relyed on outside of tests. + """ + + _2_TEST_ONLY = 2 + _99997_TEST_ONLY = 99997 + _99998_TEST_ONLY = 99998 + _99999_TEST_ONLY = 99999 + MAX = 2147483647 + """ + Placeholder for specifying unbounded edition support. This should only + ever be used by plugins that can expect to never require any changes to + support a new edition. + """ + + +class ExtensionRangeOptionsVerificationState(aristaproto.Enum): + """The verification state of the extension range.""" + + DECLARATION = 0 + """All the extensions of the range must be declared.""" + + UNVERIFIED = 1 + + +class FieldDescriptorProtoType(aristaproto.Enum): + TYPE_DOUBLE = 1 + """ + 0 is reserved for errors. + Order is weird for historical reasons. + """ + + TYPE_FLOAT = 2 + TYPE_INT64 = 3 + """ + Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if + negative values are likely. + """ + + TYPE_UINT64 = 4 + TYPE_INT32 = 5 + """ + Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if + negative values are likely. + """ + + TYPE_FIXED64 = 6 + TYPE_FIXED32 = 7 + TYPE_BOOL = 8 + TYPE_STRING = 9 + TYPE_GROUP = 10 + """ + Tag-delimited aggregate. + Group type is deprecated and not supported after google.protobuf. However, Proto3 + implementations should still be able to parse the group wire format and + treat group fields as unknown fields. In Editions, the group wire format + can be enabled via the `message_encoding` feature. + """ + + TYPE_MESSAGE = 11 + TYPE_BYTES = 12 + """New in version 2.""" + + TYPE_UINT32 = 13 + TYPE_ENUM = 14 + TYPE_SFIXED32 = 15 + TYPE_SFIXED64 = 16 + TYPE_SINT32 = 17 + TYPE_SINT64 = 18 + + +class FieldDescriptorProtoLabel(aristaproto.Enum): + LABEL_OPTIONAL = 1 + """0 is reserved for errors""" + + LABEL_REPEATED = 3 + LABEL_REQUIRED = 2 + """ + The required label is only allowed in google.protobuf. In proto3 and Editions + it's explicitly prohibited. In Editions, the `field_presence` feature + can be used to get this behavior. + """ + + +class FileOptionsOptimizeMode(aristaproto.Enum): + """Generated classes can be optimized for speed or code size.""" + + SPEED = 1 + CODE_SIZE = 2 + """etc.""" + + LITE_RUNTIME = 3 + + +class FieldOptionsCType(aristaproto.Enum): + STRING = 0 + """Default mode.""" + + CORD = 1 + """ + The option [ctype=CORD] may be applied to a non-repeated field of type + "bytes". It indicates that in C++, the data should be stored in a Cord + instead of a string. For very large strings, this may reduce memory + fragmentation. It may also allow better performance when parsing from a + Cord, or when parsing with aliasing enabled, as the parsed Cord may then + alias the original buffer. + """ + + STRING_PIECE = 2 + + +class FieldOptionsJsType(aristaproto.Enum): + JS_NORMAL = 0 + """Use the default type.""" + + JS_STRING = 1 + """Use JavaScript strings.""" + + JS_NUMBER = 2 + """Use JavaScript numbers.""" + + +class FieldOptionsOptionRetention(aristaproto.Enum): + """ + If set to RETENTION_SOURCE, the option will be omitted from the binary. + Note: as of January 2023, support for this is in progress and does not yet + have an effect (b/264593489). + """ + + RETENTION_UNKNOWN = 0 + RETENTION_RUNTIME = 1 + RETENTION_SOURCE = 2 + + +class FieldOptionsOptionTargetType(aristaproto.Enum): + """ + This indicates the types of entities that the field may apply to when used + as an option. If it is unset, then the field may be freely used as an + option on any kind of entity. Note: as of January 2023, support for this is + in progress and does not yet have an effect (b/264593489). + """ + + TARGET_TYPE_UNKNOWN = 0 + TARGET_TYPE_FILE = 1 + TARGET_TYPE_EXTENSION_RANGE = 2 + TARGET_TYPE_MESSAGE = 3 + TARGET_TYPE_FIELD = 4 + TARGET_TYPE_ONEOF = 5 + TARGET_TYPE_ENUM = 6 + TARGET_TYPE_ENUM_ENTRY = 7 + TARGET_TYPE_SERVICE = 8 + TARGET_TYPE_METHOD = 9 + + +class MethodOptionsIdempotencyLevel(aristaproto.Enum): + """ + Is this method side-effect-free (or safe in HTTP parlance), or idempotent, + or neither? HTTP based RPC implementation may choose GET verb for safe + methods, and PUT verb for idempotent methods instead of the default POST. + """ + + IDEMPOTENCY_UNKNOWN = 0 + NO_SIDE_EFFECTS = 1 + IDEMPOTENT = 2 + + +class FeatureSetFieldPresence(aristaproto.Enum): + FIELD_PRESENCE_UNKNOWN = 0 + EXPLICIT = 1 + IMPLICIT = 2 + LEGACY_REQUIRED = 3 + + +class FeatureSetEnumType(aristaproto.Enum): + ENUM_TYPE_UNKNOWN = 0 + OPEN = 1 + CLOSED = 2 + + +class FeatureSetRepeatedFieldEncoding(aristaproto.Enum): + REPEATED_FIELD_ENCODING_UNKNOWN = 0 + PACKED = 1 + EXPANDED = 2 + + +class FeatureSetUtf8Validation(aristaproto.Enum): + UTF8_VALIDATION_UNKNOWN = 0 + VERIFY = 2 + NONE = 3 + + +class FeatureSetMessageEncoding(aristaproto.Enum): + MESSAGE_ENCODING_UNKNOWN = 0 + LENGTH_PREFIXED = 1 + DELIMITED = 2 + + +class FeatureSetJsonFormat(aristaproto.Enum): + JSON_FORMAT_UNKNOWN = 0 + ALLOW = 1 + LEGACY_BEST_EFFORT = 2 + + +class GeneratedCodeInfoAnnotationSemantic(aristaproto.Enum): + """ + Represents the identified object's effect on the element in the original + .proto file. + """ + + NONE = 0 + """There is no effect or the effect is indescribable.""" + + SET = 1 + """The element is set or otherwise mutated.""" + + ALIAS = 2 + """An alias to the element is returned.""" + + +class NullValue(aristaproto.Enum): + """ + `NullValue` is a singleton enumeration to represent the null value for the + `Value` type union. + + The JSON representation for `NullValue` is JSON `null`. + """ + + _ = 0 + """Null value.""" + + +@dataclass(eq=False, repr=False) +class Any(aristaproto.Message): + """ + `Any` contains an arbitrary serialized protocol buffer message along with a + URL that describes the type of the serialized message. + + Protobuf library provides support to pack/unpack Any values in the form + of utility functions or additional generated methods of the Any type. + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + 'type.googleapis.com/full.type.name' as the type URL and the unpack + methods only use the fully qualified type name after the last '/' + in the type URL, for example "foo.bar.com/x/y.z" will yield type + name "y.z". + + JSON + ==== + The JSON representation of an `Any` value uses the regular + representation of the deserialized, embedded message, with an + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": <string>, + "lastName": <string> + } + + If the embedded message type is well-known and has a custom JSON + representation, that representation will be embedded adding a field + `value` which holds the custom JSON in addition to the `@type` + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + """ + + type_url: str = aristaproto.string_field(1) + """ + A URL/resource name that uniquely identifies the type of the serialized + protocol buffer message. This string must contain at least + one "/" character. The last segment of the URL's path must represent + the fully qualified name of the type (as in + `path/google.protobuf.Duration`). The name should be in a canonical form + (e.g., leading "." is not accepted). + + In practice, teams usually precompile into the binary all types that they + expect it to use in the context of Any. However, for URLs which use the + scheme `http`, `https`, or no scheme, one can optionally set up a type + server that maps type URLs to message definitions as follows: + + * If no scheme is provided, `https` is assumed. + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the official + protobuf release, and it is not used for type URLs beginning with + type.googleapis.com. As of May 2023, there are no widely used type server + implementations and no plans to implement one. + + Schemes other than `http`, `https` (or the empty scheme) might be + used with implementation specific semantics. + """ + + value: bytes = aristaproto.bytes_field(2) + """ + Must be a valid serialized protocol buffer of the above specified type. + """ + + +@dataclass(eq=False, repr=False) +class SourceContext(aristaproto.Message): + """ + `SourceContext` represents information about the source of a + protobuf element, like the file in which it is defined. + """ + + file_name: str = aristaproto.string_field(1) + """ + The path-qualified name of the .proto file that contained the associated + protobuf element. For example: `"google/protobuf/source_context.proto"`. + """ + + +@dataclass(eq=False, repr=False) +class Type(aristaproto.Message): + """A protocol buffer message type.""" + + name: str = aristaproto.string_field(1) + """The fully qualified message name.""" + + fields: List["Field"] = aristaproto.message_field(2) + """The list of fields.""" + + oneofs: List[str] = aristaproto.string_field(3) + """The list of types appearing in `oneof` definitions in this type.""" + + options: List["Option"] = aristaproto.message_field(4) + """The protocol buffer options.""" + + source_context: "SourceContext" = aristaproto.message_field(5) + """The source context.""" + + syntax: "Syntax" = aristaproto.enum_field(6) + """The source syntax.""" + + edition: str = aristaproto.string_field(7) + """ + The source edition string, only valid when syntax is SYNTAX_EDITIONS. + """ + + +@dataclass(eq=False, repr=False) +class Field(aristaproto.Message): + """A single field of a message type.""" + + kind: "FieldKind" = aristaproto.enum_field(1) + """The field type.""" + + cardinality: "FieldCardinality" = aristaproto.enum_field(2) + """The field cardinality.""" + + number: int = aristaproto.int32_field(3) + """The field number.""" + + name: str = aristaproto.string_field(4) + """The field name.""" + + type_url: str = aristaproto.string_field(6) + """ + The field type URL, without the scheme, for message or enumeration + types. Example: `"type.googleapis.com/google.protobuf.Timestamp"`. + """ + + oneof_index: int = aristaproto.int32_field(7) + """ + The index of the field type in `Type.oneofs`, for message or enumeration + types. The first type has index 1; zero means the type is not in the list. + """ + + packed: bool = aristaproto.bool_field(8) + """Whether to use alternative packed wire representation.""" + + options: List["Option"] = aristaproto.message_field(9) + """The protocol buffer options.""" + + json_name: str = aristaproto.string_field(10) + """The field JSON name.""" + + default_value: str = aristaproto.string_field(11) + """ + The string value of the default value of this field. Proto2 syntax only. + """ + + +@dataclass(eq=False, repr=False) +class Enum(aristaproto.Message): + """Enum type definition.""" + + name: str = aristaproto.string_field(1) + """Enum type name.""" + + enumvalue: List["EnumValue"] = aristaproto.message_field( + 2, wraps=aristaproto.TYPE_ENUM + ) + """Enum value definitions.""" + + options: List["Option"] = aristaproto.message_field(3) + """Protocol buffer options.""" + + source_context: "SourceContext" = aristaproto.message_field(4) + """The source context.""" + + syntax: "Syntax" = aristaproto.enum_field(5) + """The source syntax.""" + + edition: str = aristaproto.string_field(6) + """ + The source edition string, only valid when syntax is SYNTAX_EDITIONS. + """ + + +@dataclass(eq=False, repr=False) +class EnumValue(aristaproto.Message): + """Enum value definition.""" + + name: str = aristaproto.string_field(1) + """Enum value name.""" + + number: int = aristaproto.int32_field(2) + """Enum value number.""" + + options: List["Option"] = aristaproto.message_field(3) + """Protocol buffer options.""" + + +@dataclass(eq=False, repr=False) +class Option(aristaproto.Message): + """ + A protocol buffer option, which can be attached to a message, field, + enumeration, etc. + """ + + name: str = aristaproto.string_field(1) + """ + The option's name. For protobuf built-in options (options defined in + descriptor.proto), this is the short name. For example, `"map_entry"`. + For custom options, it should be the fully-qualified name. For example, + `"google.api.http"`. + """ + + value: "Any" = aristaproto.message_field(2) + """ + The option's value packed in an Any message. If the value is a primitive, + the corresponding wrapper type defined in google/protobuf/wrappers.proto + should be used. If the value is an enum, it should be stored as an int32 + value using the google.protobuf.Int32Value type. + """ + + +@dataclass(eq=False, repr=False) +class Api(aristaproto.Message): + """ + Api is a light-weight descriptor for an API Interface. + + Interfaces are also described as "protocol buffer services" in some contexts, + such as by the "service" keyword in a .proto file, but they are different + from API Services, which represent a concrete implementation of an interface + as opposed to simply a description of methods and bindings. They are also + sometimes simply referred to as "APIs" in other contexts, such as the name of + this message itself. See https://cloud.google.com/apis/design/glossary for + detailed terminology. + """ + + name: str = aristaproto.string_field(1) + """ + The fully qualified name of this interface, including package name + followed by the interface's simple name. + """ + + methods: List["Method"] = aristaproto.message_field(2) + """The methods of this interface, in unspecified order.""" + + options: List["Option"] = aristaproto.message_field(3) + """Any metadata attached to the interface.""" + + version: str = aristaproto.string_field(4) + """ + A version string for this interface. If specified, must have the form + `major-version.minor-version`, as in `1.10`. If the minor version is + omitted, it defaults to zero. If the entire version field is empty, the + major version is derived from the package name, as outlined below. If the + field is not empty, the version in the package name will be verified to be + consistent with what is provided here. + + The versioning schema uses [semantic + versioning](http://semver.org) where the major version number + indicates a breaking change and the minor version an additive, + non-breaking change. Both version numbers are signals to users + what to expect from different versions, and should be carefully + chosen based on the product plan. + + The major version is also reflected in the package name of the + interface, which must end in `v<major-version>`, as in + `google.feature.v1`. For major versions 0 and 1, the suffix can + be omitted. Zero major versions must only be used for + experimental, non-GA interfaces. + """ + + source_context: "SourceContext" = aristaproto.message_field(5) + """ + Source context for the protocol buffer service represented by this + message. + """ + + mixins: List["Mixin"] = aristaproto.message_field(6) + """Included interfaces. See [Mixin][].""" + + syntax: "Syntax" = aristaproto.enum_field(7) + """The source syntax of the service.""" + + +@dataclass(eq=False, repr=False) +class Method(aristaproto.Message): + """Method represents a method of an API interface.""" + + name: str = aristaproto.string_field(1) + """The simple name of this method.""" + + request_type_url: str = aristaproto.string_field(2) + """A URL of the input message type.""" + + request_streaming: bool = aristaproto.bool_field(3) + """If true, the request is streamed.""" + + response_type_url: str = aristaproto.string_field(4) + """The URL of the output message type.""" + + response_streaming: bool = aristaproto.bool_field(5) + """If true, the response is streamed.""" + + options: List["Option"] = aristaproto.message_field(6) + """Any metadata attached to the method.""" + + syntax: "Syntax" = aristaproto.enum_field(7) + """The source syntax of this method.""" + + +@dataclass(eq=False, repr=False) +class Mixin(aristaproto.Message): + """ + Declares an API Interface to be included in this interface. The including + interface must redeclare all the methods from the included interface, but + documentation and options are inherited as follows: + + - If after comment and whitespace stripping, the documentation + string of the redeclared method is empty, it will be inherited + from the original method. + + - Each annotation belonging to the service config (http, + visibility) which is not set in the redeclared method will be + inherited. + + - If an http annotation is inherited, the path pattern will be + modified as follows. Any version prefix will be replaced by the + version of the including interface plus the [root][] path if + specified. + + Example of a simple mixin: + + package google.acl.v1; + service AccessControl { + // Get the underlying ACL object. + rpc GetAcl(GetAclRequest) returns (Acl) { + option (google.api.http).get = "/v1/{resource=**}:getAcl"; + } + } + + package google.storage.v2; + service Storage { + rpc GetAcl(GetAclRequest) returns (Acl); + + // Get a data record. + rpc GetData(GetDataRequest) returns (Data) { + option (google.api.http).get = "/v2/{resource=**}"; + } + } + + Example of a mixin configuration: + + apis: + - name: google.storage.v2.Storage + mixins: + - name: google.acl.v1.AccessControl + + The mixin construct implies that all methods in `AccessControl` are + also declared with same name and request/response types in + `Storage`. A documentation generator or annotation processor will + see the effective `Storage.GetAcl` method after inherting + documentation and annotations as follows: + + service Storage { + // Get the underlying ACL object. + rpc GetAcl(GetAclRequest) returns (Acl) { + option (google.api.http).get = "/v2/{resource=**}:getAcl"; + } + ... + } + + Note how the version in the path pattern changed from `v1` to `v2`. + + If the `root` field in the mixin is specified, it should be a + relative path under which inherited HTTP paths are placed. Example: + + apis: + - name: google.storage.v2.Storage + mixins: + - name: google.acl.v1.AccessControl + root: acls + + This implies the following inherited HTTP annotation: + + service Storage { + // Get the underlying ACL object. + rpc GetAcl(GetAclRequest) returns (Acl) { + option (google.api.http).get = "/v2/acls/{resource=**}:getAcl"; + } + ... + } + """ + + name: str = aristaproto.string_field(1) + """The fully qualified name of the interface which is included.""" + + root: str = aristaproto.string_field(2) + """ + If non-empty specifies a path under which inherited HTTP paths + are rooted. + """ + + +@dataclass(eq=False, repr=False) +class FileDescriptorSet(aristaproto.Message): + """ + The protocol compiler can output a FileDescriptorSet containing the .proto + files it parses. + """ + + file: List["FileDescriptorProto"] = aristaproto.message_field(1) + + +@dataclass(eq=False, repr=False) +class FileDescriptorProto(aristaproto.Message): + """Describes a complete .proto file.""" + + name: str = aristaproto.string_field(1) + package: str = aristaproto.string_field(2) + dependency: List[str] = aristaproto.string_field(3) + """Names of files imported by this file.""" + + public_dependency: List[int] = aristaproto.int32_field(10) + """Indexes of the public imported files in the dependency list above.""" + + weak_dependency: List[int] = aristaproto.int32_field(11) + """ + Indexes of the weak imported files in the dependency list. + For Google-internal migration only. Do not use. + """ + + message_type: List["DescriptorProto"] = aristaproto.message_field(4) + """All top-level definitions in this file.""" + + enum_type: List["EnumDescriptorProto"] = aristaproto.message_field(5) + service: List["ServiceDescriptorProto"] = aristaproto.message_field(6) + extension: List["FieldDescriptorProto"] = aristaproto.message_field(7) + options: "FileOptions" = aristaproto.message_field(8) + source_code_info: "SourceCodeInfo" = aristaproto.message_field(9) + """ + This field contains optional information about the original source code. + You may safely remove this entire field without harming runtime + functionality of the descriptors -- the information is needed only by + development tools. + """ + + syntax: str = aristaproto.string_field(12) + """ + The syntax of the proto file. + The supported values are "proto2", "proto3", and "editions". + + If `edition` is present, this value must be "editions". + """ + + edition: "Edition" = aristaproto.enum_field(14) + """The edition of the proto file.""" + + +@dataclass(eq=False, repr=False) +class DescriptorProto(aristaproto.Message): + """Describes a message type.""" + + name: str = aristaproto.string_field(1) + field: List["FieldDescriptorProto"] = aristaproto.message_field(2) + extension: List["FieldDescriptorProto"] = aristaproto.message_field(6) + nested_type: List["DescriptorProto"] = aristaproto.message_field(3) + enum_type: List["EnumDescriptorProto"] = aristaproto.message_field(4) + extension_range: List["DescriptorProtoExtensionRange"] = aristaproto.message_field( + 5 + ) + oneof_decl: List["OneofDescriptorProto"] = aristaproto.message_field(8) + options: "MessageOptions" = aristaproto.message_field(7) + reserved_range: List["DescriptorProtoReservedRange"] = aristaproto.message_field(9) + reserved_name: List[str] = aristaproto.string_field(10) + """ + Reserved field names, which may not be used by fields in the same message. + A given name may only be reserved once. + """ + + +@dataclass(eq=False, repr=False) +class DescriptorProtoExtensionRange(aristaproto.Message): + start: int = aristaproto.int32_field(1) + end: int = aristaproto.int32_field(2) + options: "ExtensionRangeOptions" = aristaproto.message_field(3) + + +@dataclass(eq=False, repr=False) +class DescriptorProtoReservedRange(aristaproto.Message): + """ + Range of reserved tag numbers. Reserved tag numbers may not be used by + fields or extension ranges in the same message. Reserved ranges may + not overlap. + """ + + start: int = aristaproto.int32_field(1) + end: int = aristaproto.int32_field(2) + + +@dataclass(eq=False, repr=False) +class ExtensionRangeOptions(aristaproto.Message): + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + declaration: List["ExtensionRangeOptionsDeclaration"] = aristaproto.message_field(2) + """ + For external users: DO NOT USE. We are in the process of open sourcing + extension declaration and executing internal cleanups before it can be + used externally. + """ + + features: "FeatureSet" = aristaproto.message_field(50) + """Any features defined in the specific edition.""" + + verification: "ExtensionRangeOptionsVerificationState" = aristaproto.enum_field(3) + """ + The verification state of the range. + TODO: flip the default to DECLARATION once all empty ranges + are marked as UNVERIFIED. + """ + + +@dataclass(eq=False, repr=False) +class ExtensionRangeOptionsDeclaration(aristaproto.Message): + number: int = aristaproto.int32_field(1) + """The extension number declared within the extension range.""" + + full_name: str = aristaproto.string_field(2) + """ + The fully-qualified name of the extension field. There must be a leading + dot in front of the full name. + """ + + type: str = aristaproto.string_field(3) + """ + The fully-qualified type name of the extension field. Unlike + Metadata.type, Declaration.type must have a leading dot for messages + and enums. + """ + + reserved: bool = aristaproto.bool_field(5) + """ + If true, indicates that the number is reserved in the extension range, + and any extension field with the number will fail to compile. Set this + when a declared extension field is deleted. + """ + + repeated: bool = aristaproto.bool_field(6) + """ + If true, indicates that the extension must be defined as repeated. + Otherwise the extension must be defined as optional. + """ + + +@dataclass(eq=False, repr=False) +class FieldDescriptorProto(aristaproto.Message): + """Describes a field within a message.""" + + name: str = aristaproto.string_field(1) + number: int = aristaproto.int32_field(3) + label: "FieldDescriptorProtoLabel" = aristaproto.enum_field(4) + type: "FieldDescriptorProtoType" = aristaproto.enum_field(5) + """ + If type_name is set, this need not be set. If both this and type_name + are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. + """ + + type_name: str = aristaproto.string_field(6) + """ + For message and enum types, this is the name of the type. If the name + starts with a '.', it is fully-qualified. Otherwise, C++-like scoping + rules are used to find the type (i.e. first the nested types within this + message are searched, then within the parent, on up to the root + namespace). + """ + + extendee: str = aristaproto.string_field(2) + """ + For extensions, this is the name of the type being extended. It is + resolved in the same manner as type_name. + """ + + default_value: str = aristaproto.string_field(7) + """ + For numeric types, contains the original text representation of the value. + For booleans, "true" or "false". + For strings, contains the default text contents (not escaped in any way). + For bytes, contains the C escaped value. All bytes >= 128 are escaped. + """ + + oneof_index: int = aristaproto.int32_field(9) + """ + If set, gives the index of a oneof in the containing type's oneof_decl + list. This field is a member of that oneof. + """ + + json_name: str = aristaproto.string_field(10) + """ + JSON name of this field. The value is set by protocol compiler. If the + user has set a "json_name" option on this field, that option's value + will be used. Otherwise, it's deduced from the field's name by converting + it to camelCase. + """ + + options: "FieldOptions" = aristaproto.message_field(8) + proto3_optional: bool = aristaproto.bool_field(17) + """ + If true, this is a proto3 "optional". When a proto3 field is optional, it + tracks presence regardless of field type. + + When proto3_optional is true, this field must belong to a oneof to signal + to old proto3 clients that presence is tracked for this field. This oneof + is known as a "synthetic" oneof, and this field must be its sole member + (each proto3 optional field gets its own synthetic oneof). Synthetic oneofs + exist in the descriptor only, and do not generate any API. Synthetic oneofs + must be ordered after all "real" oneofs. + + For message fields, proto3_optional doesn't create any semantic change, + since non-repeated message fields always track presence. However it still + indicates the semantic detail of whether the user wrote "optional" or not. + This can be useful for round-tripping the .proto file. For consistency we + give message fields a synthetic oneof also, even though it is not required + to track presence. This is especially important because the parser can't + tell if a field is a message or an enum, so it must always create a + synthetic oneof. + + Proto2 optional fields do not set this flag, because they already indicate + optional with `LABEL_OPTIONAL`. + """ + + +@dataclass(eq=False, repr=False) +class OneofDescriptorProto(aristaproto.Message): + """Describes a oneof.""" + + name: str = aristaproto.string_field(1) + options: "OneofOptions" = aristaproto.message_field(2) + + +@dataclass(eq=False, repr=False) +class EnumDescriptorProto(aristaproto.Message): + """Describes an enum type.""" + + name: str = aristaproto.string_field(1) + value: List["EnumValueDescriptorProto"] = aristaproto.message_field(2) + options: "EnumOptions" = aristaproto.message_field(3) + reserved_range: List[ + "EnumDescriptorProtoEnumReservedRange" + ] = aristaproto.message_field(4) + """ + Range of reserved numeric values. Reserved numeric values may not be used + by enum values in the same enum declaration. Reserved ranges may not + overlap. + """ + + reserved_name: List[str] = aristaproto.string_field(5) + """ + Reserved enum value names, which may not be reused. A given name may only + be reserved once. + """ + + +@dataclass(eq=False, repr=False) +class EnumDescriptorProtoEnumReservedRange(aristaproto.Message): + """ + Range of reserved numeric values. Reserved values may not be used by + entries in the same enum. Reserved ranges may not overlap. + + Note that this is distinct from DescriptorProto.ReservedRange in that it + is inclusive such that it can appropriately represent the entire int32 + domain. + """ + + start: int = aristaproto.int32_field(1) + end: int = aristaproto.int32_field(2) + + +@dataclass(eq=False, repr=False) +class EnumValueDescriptorProto(aristaproto.Message): + """Describes a value within an enum.""" + + name: str = aristaproto.string_field(1) + number: int = aristaproto.int32_field(2) + options: "EnumValueOptions" = aristaproto.message_field(3) + + +@dataclass(eq=False, repr=False) +class ServiceDescriptorProto(aristaproto.Message): + """Describes a service.""" + + name: str = aristaproto.string_field(1) + method: List["MethodDescriptorProto"] = aristaproto.message_field(2) + options: "ServiceOptions" = aristaproto.message_field(3) + + +@dataclass(eq=False, repr=False) +class MethodDescriptorProto(aristaproto.Message): + """Describes a method of a service.""" + + name: str = aristaproto.string_field(1) + input_type: str = aristaproto.string_field(2) + """ + Input and output type names. These are resolved in the same way as + FieldDescriptorProto.type_name, but must refer to a message type. + """ + + output_type: str = aristaproto.string_field(3) + options: "MethodOptions" = aristaproto.message_field(4) + client_streaming: bool = aristaproto.bool_field(5) + """Identifies if client streams multiple client messages""" + + server_streaming: bool = aristaproto.bool_field(6) + """Identifies if server streams multiple server messages""" + + +@dataclass(eq=False, repr=False) +class FileOptions(aristaproto.Message): + java_package: str = aristaproto.string_field(1) + """ + Sets the Java package where classes generated from this .proto will be + placed. By default, the proto package is used, but this is often + inappropriate because proto packages do not normally start with backwards + domain names. + """ + + java_outer_classname: str = aristaproto.string_field(8) + """ + Controls the name of the wrapper Java class generated for the .proto file. + That class will always contain the .proto file's getDescriptor() method as + well as any top-level extensions defined in the .proto file. + If java_multiple_files is disabled, then all the other classes from the + .proto file will be nested inside the single wrapper outer class. + """ + + java_multiple_files: bool = aristaproto.bool_field(10) + """ + If enabled, then the Java code generator will generate a separate .java + file for each top-level message, enum, and service defined in the .proto + file. Thus, these types will *not* be nested inside the wrapper class + named by java_outer_classname. However, the wrapper class will still be + generated to contain the file's getDescriptor() method as well as any + top-level extensions defined in the file. + """ + + java_generate_equals_and_hash: bool = aristaproto.bool_field(20) + """This option does nothing.""" + + java_string_check_utf8: bool = aristaproto.bool_field(27) + """ + A proto2 file can set this to true to opt in to UTF-8 checking for Java, + which will throw an exception if invalid UTF-8 is parsed from the wire or + assigned to a string field. + + TODO: clarify exactly what kinds of field types this option + applies to, and update these docs accordingly. + + Proto3 files already perform these checks. Setting the option explicitly to + false has no effect: it cannot be used to opt proto3 files out of UTF-8 + checks. + """ + + optimize_for: "FileOptionsOptimizeMode" = aristaproto.enum_field(9) + go_package: str = aristaproto.string_field(11) + """ + Sets the Go package where structs generated from this .proto will be + placed. If omitted, the Go package will be derived from the following: + - The basename of the package import path, if provided. + - Otherwise, the package statement in the .proto file, if present. + - Otherwise, the basename of the .proto file, without extension. + """ + + cc_generic_services: bool = aristaproto.bool_field(16) + """ + Should generic services be generated in each language? "Generic" services + are not specific to any particular RPC system. They are generated by the + main code generators in each language (without additional plugins). + Generic services were the only kind of service generation supported by + early versions of google.protobuf. + + Generic services are now considered deprecated in favor of using plugins + that generate code specific to your particular RPC system. Therefore, + these default to false. Old code which depends on generic services should + explicitly set them to true. + """ + + java_generic_services: bool = aristaproto.bool_field(17) + py_generic_services: bool = aristaproto.bool_field(18) + deprecated: bool = aristaproto.bool_field(23) + """ + Is this file deprecated? + Depending on the target platform, this can emit Deprecated annotations + for everything in the file, or it will be completely ignored; in the very + least, this is a formalization for deprecating files. + """ + + cc_enable_arenas: bool = aristaproto.bool_field(31) + """ + Enables the use of arenas for the proto messages in this file. This applies + only to generated classes for C++. + """ + + objc_class_prefix: str = aristaproto.string_field(36) + """ + Sets the objective c class prefix which is prepended to all objective c + generated classes from this .proto. There is no default. + """ + + csharp_namespace: str = aristaproto.string_field(37) + """Namespace for generated classes; defaults to the package.""" + + swift_prefix: str = aristaproto.string_field(39) + """ + By default Swift generators will take the proto package and CamelCase it + replacing '.' with underscore and use that to prefix the types/symbols + defined. When this options is provided, they will use this value instead + to prefix the types/symbols defined. + """ + + php_class_prefix: str = aristaproto.string_field(40) + """ + Sets the php class prefix which is prepended to all php generated classes + from this .proto. Default is empty. + """ + + php_namespace: str = aristaproto.string_field(41) + """ + Use this option to change the namespace of php generated classes. Default + is empty. When this option is empty, the package name will be used for + determining the namespace. + """ + + php_metadata_namespace: str = aristaproto.string_field(44) + """ + Use this option to change the namespace of php generated metadata classes. + Default is empty. When this option is empty, the proto file name will be + used for determining the namespace. + """ + + ruby_package: str = aristaproto.string_field(45) + """ + Use this option to change the package of ruby generated classes. Default + is empty. When this option is not set, the package name will be used for + determining the ruby package. + """ + + features: "FeatureSet" = aristaproto.message_field(50) + """Any features defined in the specific edition.""" + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """ + The parser stores options it doesn't recognize here. + See the documentation for the "Options" section above. + """ + + def __post_init__(self) -> None: + super().__post_init__() + if self.is_set("java_generate_equals_and_hash"): + warnings.warn( + "FileOptions.java_generate_equals_and_hash is deprecated", + DeprecationWarning, + ) + + +@dataclass(eq=False, repr=False) +class MessageOptions(aristaproto.Message): + message_set_wire_format: bool = aristaproto.bool_field(1) + """ + Set true to use the old proto1 MessageSet wire format for extensions. + This is provided for backwards-compatibility with the MessageSet wire + format. You should not use this for any other reason: It's less + efficient, has fewer features, and is more complicated. + + The message must be defined exactly as follows: + message Foo { + option message_set_wire_format = true; + extensions 4 to max; + } + Note that the message cannot have any defined fields; MessageSets only + have extensions. + + All extensions of your type must be singular messages; e.g. they cannot + be int32s, enums, or repeated messages. + + Because this is an option, the above two restrictions are not enforced by + the protocol compiler. + """ + + no_standard_descriptor_accessor: bool = aristaproto.bool_field(2) + """ + Disables the generation of the standard "descriptor()" accessor, which can + conflict with a field of the same name. This is meant to make migration + from proto1 easier; new code should avoid fields named "descriptor". + """ + + deprecated: bool = aristaproto.bool_field(3) + """ + Is this message deprecated? + Depending on the target platform, this can emit Deprecated annotations + for the message, or it will be completely ignored; in the very least, + this is a formalization for deprecating messages. + """ + + map_entry: bool = aristaproto.bool_field(7) + """ + Whether the message is an automatically generated map entry type for the + maps field. + + For maps fields: + map<KeyType, ValueType> map_field = 1; + The parsed descriptor looks like: + message MapFieldEntry { + option map_entry = true; + optional KeyType key = 1; + optional ValueType value = 2; + } + repeated MapFieldEntry map_field = 1; + + Implementations may choose not to generate the map_entry=true message, but + use a native map in the target language to hold the keys and values. + The reflection APIs in such implementations still need to work as + if the field is a repeated message field. + + NOTE: Do not set the option in .proto files. Always use the maps syntax + instead. The option should only be implicitly set by the proto compiler + parser. + """ + + deprecated_legacy_json_field_conflicts: bool = aristaproto.bool_field(11) + """ + Enable the legacy handling of JSON field name conflicts. This lowercases + and strips underscored from the fields before comparison in proto3 only. + The new behavior takes `json_name` into account and applies to proto2 as + well. + + This should only be used as a temporary measure against broken builds due + to the change in behavior for JSON field name conflicts. + + TODO This is legacy behavior we plan to remove once downstream + teams have had time to migrate. + """ + + features: "FeatureSet" = aristaproto.message_field(12) + """Any features defined in the specific edition.""" + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + def __post_init__(self) -> None: + super().__post_init__() + if self.is_set("deprecated_legacy_json_field_conflicts"): + warnings.warn( + "MessageOptions.deprecated_legacy_json_field_conflicts is deprecated", + DeprecationWarning, + ) + + +@dataclass(eq=False, repr=False) +class FieldOptions(aristaproto.Message): + ctype: "FieldOptionsCType" = aristaproto.enum_field(1) + """ + The ctype option instructs the C++ code generator to use a different + representation of the field than it normally would. See the specific + options below. This option is only implemented to support use of + [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of + type "bytes" in the open source release -- sorry, we'll try to include + other types in a future version! + """ + + packed: bool = aristaproto.bool_field(2) + """ + The packed option can be enabled for repeated primitive fields to enable + a more efficient representation on the wire. Rather than repeatedly + writing the tag and type for each element, the entire array is encoded as + a single length-delimited blob. In proto3, only explicit setting it to + false will avoid using packed encoding. This option is prohibited in + Editions, but the `repeated_field_encoding` feature can be used to control + the behavior. + """ + + jstype: "FieldOptionsJsType" = aristaproto.enum_field(6) + """ + The jstype option determines the JavaScript type used for values of the + field. The option is permitted only for 64 bit integral and fixed types + (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING + is represented as JavaScript string, which avoids loss of precision that + can happen when a large value is converted to a floating point JavaScript. + Specifying JS_NUMBER for the jstype causes the generated JavaScript code to + use the JavaScript "number" type. The behavior of the default option + JS_NORMAL is implementation dependent. + + This option is an enum to permit additional types to be added, e.g. + goog.math.Integer. + """ + + lazy: bool = aristaproto.bool_field(5) + """ + Should this field be parsed lazily? Lazy applies only to message-type + fields. It means that when the outer message is initially parsed, the + inner message's contents will not be parsed but instead stored in encoded + form. The inner message will actually be parsed when it is first accessed. + + This is only a hint. Implementations are free to choose whether to use + eager or lazy parsing regardless of the value of this option. However, + setting this option true suggests that the protocol author believes that + using lazy parsing on this field is worth the additional bookkeeping + overhead typically needed to implement it. + + This option does not affect the public interface of any generated code; + all method signatures remain the same. Furthermore, thread-safety of the + interface is not affected by this option; const methods remain safe to + call from multiple threads concurrently, while non-const methods continue + to require exclusive access. + + Note that lazy message fields are still eagerly verified to check + ill-formed wireformat or missing required fields. Calling IsInitialized() + on the outer message would fail if the inner message has missing required + fields. Failed verification would result in parsing failure (except when + uninitialized messages are acceptable). + """ + + unverified_lazy: bool = aristaproto.bool_field(15) + """ + unverified_lazy does no correctness checks on the byte stream. This should + only be used where lazy with verification is prohibitive for performance + reasons. + """ + + deprecated: bool = aristaproto.bool_field(3) + """ + Is this field deprecated? + Depending on the target platform, this can emit Deprecated annotations + for accessors, or it will be completely ignored; in the very least, this + is a formalization for deprecating fields. + """ + + weak: bool = aristaproto.bool_field(10) + """For Google-internal migration only. Do not use.""" + + debug_redact: bool = aristaproto.bool_field(16) + """ + Indicate that the field value should not be printed out when using debug + formats, e.g. when the field contains sensitive credentials. + """ + + retention: "FieldOptionsOptionRetention" = aristaproto.enum_field(17) + targets: List["FieldOptionsOptionTargetType"] = aristaproto.enum_field(19) + edition_defaults: List["FieldOptionsEditionDefault"] = aristaproto.message_field(20) + features: "FeatureSet" = aristaproto.message_field(21) + """Any features defined in the specific edition.""" + + feature_support: "FieldOptionsFeatureSupport" = aristaproto.message_field(22) + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + +@dataclass(eq=False, repr=False) +class FieldOptionsEditionDefault(aristaproto.Message): + edition: "Edition" = aristaproto.enum_field(3) + value: str = aristaproto.string_field(2) + + +@dataclass(eq=False, repr=False) +class FieldOptionsFeatureSupport(aristaproto.Message): + """Information about the support window of a feature.""" + + edition_introduced: "Edition" = aristaproto.enum_field(1) + """ + The edition that this feature was first available in. In editions + earlier than this one, the default assigned to EDITION_LEGACY will be + used, and proto files will not be able to override it. + """ + + edition_deprecated: "Edition" = aristaproto.enum_field(2) + """ + The edition this feature becomes deprecated in. Using this after this + edition may trigger warnings. + """ + + deprecation_warning: str = aristaproto.string_field(3) + """ + The deprecation warning text if this feature is used after the edition it + was marked deprecated in. + """ + + edition_removed: "Edition" = aristaproto.enum_field(4) + """ + The edition this feature is no longer available in. In editions after + this one, the last default assigned will be used, and proto files will + not be able to override it. + """ + + +@dataclass(eq=False, repr=False) +class OneofOptions(aristaproto.Message): + features: "FeatureSet" = aristaproto.message_field(1) + """Any features defined in the specific edition.""" + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + +@dataclass(eq=False, repr=False) +class EnumOptions(aristaproto.Message): + allow_alias: bool = aristaproto.bool_field(2) + """ + Set this option to true to allow mapping different tag names to the same + value. + """ + + deprecated: bool = aristaproto.bool_field(3) + """ + Is this enum deprecated? + Depending on the target platform, this can emit Deprecated annotations + for the enum, or it will be completely ignored; in the very least, this + is a formalization for deprecating enums. + """ + + deprecated_legacy_json_field_conflicts: bool = aristaproto.bool_field(6) + """ + Enable the legacy handling of JSON field name conflicts. This lowercases + and strips underscored from the fields before comparison in proto3 only. + The new behavior takes `json_name` into account and applies to proto2 as + well. + TODO Remove this legacy behavior once downstream teams have + had time to migrate. + """ + + features: "FeatureSet" = aristaproto.message_field(7) + """Any features defined in the specific edition.""" + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + def __post_init__(self) -> None: + super().__post_init__() + if self.is_set("deprecated_legacy_json_field_conflicts"): + warnings.warn( + "EnumOptions.deprecated_legacy_json_field_conflicts is deprecated", + DeprecationWarning, + ) + + +@dataclass(eq=False, repr=False) +class EnumValueOptions(aristaproto.Message): + deprecated: bool = aristaproto.bool_field(1) + """ + Is this enum value deprecated? + Depending on the target platform, this can emit Deprecated annotations + for the enum value, or it will be completely ignored; in the very least, + this is a formalization for deprecating enum values. + """ + + features: "FeatureSet" = aristaproto.message_field(2) + """Any features defined in the specific edition.""" + + debug_redact: bool = aristaproto.bool_field(3) + """ + Indicate that fields annotated with this enum value should not be printed + out when using debug formats, e.g. when the field contains sensitive + credentials. + """ + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + +@dataclass(eq=False, repr=False) +class ServiceOptions(aristaproto.Message): + features: "FeatureSet" = aristaproto.message_field(34) + """Any features defined in the specific edition.""" + + deprecated: bool = aristaproto.bool_field(33) + """ + Is this service deprecated? + Depending on the target platform, this can emit Deprecated annotations + for the service, or it will be completely ignored; in the very least, + this is a formalization for deprecating services. + """ + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + +@dataclass(eq=False, repr=False) +class MethodOptions(aristaproto.Message): + deprecated: bool = aristaproto.bool_field(33) + """ + Is this method deprecated? + Depending on the target platform, this can emit Deprecated annotations + for the method, or it will be completely ignored; in the very least, + this is a formalization for deprecating methods. + """ + + idempotency_level: "MethodOptionsIdempotencyLevel" = aristaproto.enum_field(34) + features: "FeatureSet" = aristaproto.message_field(35) + """Any features defined in the specific edition.""" + + uninterpreted_option: List["UninterpretedOption"] = aristaproto.message_field(999) + """The parser stores options it doesn't recognize here. See above.""" + + +@dataclass(eq=False, repr=False) +class UninterpretedOption(aristaproto.Message): + """ + A message representing a option the parser does not recognize. This only + appears in options protos created by the compiler::Parser class. + DescriptorPool resolves these when building Descriptor objects. Therefore, + options protos in descriptor objects (e.g. returned by Descriptor::options(), + or produced by Descriptor::CopyTo()) will never have UninterpretedOptions + in them. + """ + + name: List["UninterpretedOptionNamePart"] = aristaproto.message_field(2) + identifier_value: str = aristaproto.string_field(3) + """ + The value of the uninterpreted option, in whatever type the tokenizer + identified it as during parsing. Exactly one of these should be set. + """ + + positive_int_value: int = aristaproto.uint64_field(4) + negative_int_value: int = aristaproto.int64_field(5) + double_value: float = aristaproto.double_field(6) + string_value: bytes = aristaproto.bytes_field(7) + aggregate_value: str = aristaproto.string_field(8) + + +@dataclass(eq=False, repr=False) +class UninterpretedOptionNamePart(aristaproto.Message): + """ + The name of the uninterpreted option. Each string represents a segment in + a dot-separated name. is_extension is true iff a segment represents an + extension (denoted with parentheses in options specs in .proto files). + E.g.,{ ["foo", false], ["bar.baz", true], ["moo", false] } represents + "foo.(bar.baz).moo". + """ + + name_part: str = aristaproto.string_field(1) + is_extension: bool = aristaproto.bool_field(2) + + +@dataclass(eq=False, repr=False) +class FeatureSet(aristaproto.Message): + """ + TODO Enums in C++ gencode (and potentially other languages) are + not well scoped. This means that each of the feature enums below can clash + with each other. The short names we've chosen maximize call-site + readability, but leave us very open to this scenario. A future feature will + be designed and implemented to handle this, hopefully before we ever hit a + conflict here. + """ + + field_presence: "FeatureSetFieldPresence" = aristaproto.enum_field(1) + enum_type: "FeatureSetEnumType" = aristaproto.enum_field(2) + repeated_field_encoding: "FeatureSetRepeatedFieldEncoding" = aristaproto.enum_field( + 3 + ) + utf8_validation: "FeatureSetUtf8Validation" = aristaproto.enum_field(4) + message_encoding: "FeatureSetMessageEncoding" = aristaproto.enum_field(5) + json_format: "FeatureSetJsonFormat" = aristaproto.enum_field(6) + + +@dataclass(eq=False, repr=False) +class FeatureSetDefaults(aristaproto.Message): + """ + A compiled specification for the defaults of a set of features. These + messages are generated from FeatureSet extensions and can be used to seed + feature resolution. The resolution with this object becomes a simple search + for the closest matching edition, followed by proto merges. + """ + + defaults: List[ + "FeatureSetDefaultsFeatureSetEditionDefault" + ] = aristaproto.message_field(1) + minimum_edition: "Edition" = aristaproto.enum_field(4) + """ + The minimum supported edition (inclusive) when this was constructed. + Editions before this will not have defaults. + """ + + maximum_edition: "Edition" = aristaproto.enum_field(5) + """ + The maximum known edition (inclusive) when this was constructed. Editions + after this will not have reliable defaults. + """ + + +@dataclass(eq=False, repr=False) +class FeatureSetDefaultsFeatureSetEditionDefault(aristaproto.Message): + """ + A map from every known edition with a unique set of defaults to its + defaults. Not all editions may be contained here. For a given edition, + the defaults at the closest matching edition ordered at or before it should + be used. This field must be in strict ascending order by edition. + """ + + edition: "Edition" = aristaproto.enum_field(3) + overridable_features: "FeatureSet" = aristaproto.message_field(4) + """Defaults of features that can be overridden in this edition.""" + + fixed_features: "FeatureSet" = aristaproto.message_field(5) + """Defaults of features that can't be overridden in this edition.""" + + features: "FeatureSet" = aristaproto.message_field(2) + """ + TODO Deprecate and remove this field, which is just the + above two merged. + """ + + +@dataclass(eq=False, repr=False) +class SourceCodeInfo(aristaproto.Message): + """ + Encapsulates information about the original source file from which a + FileDescriptorProto was generated. + """ + + location: List["SourceCodeInfoLocation"] = aristaproto.message_field(1) + """ + A Location identifies a piece of source code in a .proto file which + corresponds to a particular definition. This information is intended + to be useful to IDEs, code indexers, documentation generators, and similar + tools. + + For example, say we have a file like: + message Foo { + optional string foo = 1; + } + Let's look at just the field definition: + optional string foo = 1; + ^ ^^ ^^ ^ ^^^ + a bc de f ghi + We have the following locations: + span path represents + [a,i) [ 4, 0, 2, 0 ] The whole field definition. + [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). + [c,d) [ 4, 0, 2, 0, 5 ] The type (string). + [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). + [g,h) [ 4, 0, 2, 0, 3 ] The number (1). + + Notes: + - A location may refer to a repeated field itself (i.e. not to any + particular index within it). This is used whenever a set of elements are + logically enclosed in a single code segment. For example, an entire + extend block (possibly containing multiple extension definitions) will + have an outer location whose path refers to the "extensions" repeated + field without an index. + - Multiple locations may have the same path. This happens when a single + logical declaration is spread out across multiple places. The most + obvious example is the "extend" block again -- there may be multiple + extend blocks in the same scope, each of which will have the same path. + - A location's span is not always a subset of its parent's span. For + example, the "extendee" of an extension declaration appears at the + beginning of the "extend" block and is shared by all extensions within + the block. + - Just because a location's span is a subset of some other location's span + does not mean that it is a descendant. For example, a "group" defines + both a type and a field in a single declaration. Thus, the locations + corresponding to the type and field and their components will overlap. + - Code which tries to interpret locations should probably be designed to + ignore those that it doesn't understand, as more types of locations could + be recorded in the future. + """ + + +@dataclass(eq=False, repr=False) +class SourceCodeInfoLocation(aristaproto.Message): + path: List[int] = aristaproto.int32_field(1) + """ + Identifies which part of the FileDescriptorProto was defined at this + location. + + Each element is a field number or an index. They form a path from + the root FileDescriptorProto to the place where the definition appears. + For example, this path: + [ 4, 3, 2, 7, 1 ] + refers to: + file.message_type(3) // 4, 3 + .field(7) // 2, 7 + .name() // 1 + This is because FileDescriptorProto.message_type has field number 4: + repeated DescriptorProto message_type = 4; + and DescriptorProto.field has field number 2: + repeated FieldDescriptorProto field = 2; + and FieldDescriptorProto.name has field number 1: + optional string name = 1; + + Thus, the above path gives the location of a field name. If we removed + the last element: + [ 4, 3, 2, 7 ] + this path refers to the whole field declaration (from the beginning + of the label to the terminating semicolon). + """ + + span: List[int] = aristaproto.int32_field(2) + """ + Always has exactly three or four elements: start line, start column, + end line (optional, otherwise assumed same as start line), end column. + These are packed into a single field for efficiency. Note that line + and column numbers are zero-based -- typically you will want to add + 1 to each before displaying to a user. + """ + + leading_comments: str = aristaproto.string_field(3) + """ + If this SourceCodeInfo represents a complete declaration, these are any + comments appearing before and after the declaration which appear to be + attached to the declaration. + + A series of line comments appearing on consecutive lines, with no other + tokens appearing on those lines, will be treated as a single comment. + + leading_detached_comments will keep paragraphs of comments that appear + before (but not connected to) the current element. Each paragraph, + separated by empty lines, will be one comment element in the repeated + field. + + Only the comment content is provided; comment markers (e.g. //) are + stripped out. For block comments, leading whitespace and an asterisk + will be stripped from the beginning of each line other than the first. + Newlines are included in the output. + + Examples: + + optional int32 foo = 1; // Comment attached to foo. + // Comment attached to bar. + optional int32 bar = 2; + + optional string baz = 3; + // Comment attached to baz. + // Another line attached to baz. + + // Comment attached to moo. + // + // Another line attached to moo. + optional double moo = 4; + + // Detached comment for corge. This is not leading or trailing comments + // to moo or corge because there are blank lines separating it from + // both. + + // Detached comment for corge paragraph 2. + + optional string corge = 5; + /* Block comment attached + * to corge. Leading asterisks + * will be removed. */ + /* Block comment attached to + * grault. */ + optional int32 grault = 6; + + // ignored detached comments. + """ + + trailing_comments: str = aristaproto.string_field(4) + leading_detached_comments: List[str] = aristaproto.string_field(6) + + +@dataclass(eq=False, repr=False) +class GeneratedCodeInfo(aristaproto.Message): + """ + Describes the relationship between generated code and its original source + file. A GeneratedCodeInfo message is associated with only one generated + source file, but may contain references to different source .proto files. + """ + + annotation: List["GeneratedCodeInfoAnnotation"] = aristaproto.message_field(1) + """ + An Annotation connects some span of text in generated code to an element + of its generating .proto file. + """ + + +@dataclass(eq=False, repr=False) +class GeneratedCodeInfoAnnotation(aristaproto.Message): + path: List[int] = aristaproto.int32_field(1) + """ + Identifies the element in the original source .proto file. This field + is formatted the same as SourceCodeInfo.Location.path. + """ + + source_file: str = aristaproto.string_field(2) + """Identifies the filesystem path to the original source .proto.""" + + begin: int = aristaproto.int32_field(3) + """ + Identifies the starting offset in bytes in the generated code + that relates to the identified object. + """ + + end: int = aristaproto.int32_field(4) + """ + Identifies the ending offset in bytes in the generated code that + relates to the identified object. The end offset should be one past + the last relevant byte (so the length of the text = end - begin). + """ + + semantic: "GeneratedCodeInfoAnnotationSemantic" = aristaproto.enum_field(5) + + +@dataclass(eq=False, repr=False) +class Duration(aristaproto.Message): + """ + A Duration represents a signed, fixed-length span of time represented + as a count of seconds and fractions of seconds at nanosecond + resolution. It is independent of any calendar and concepts like "day" + or "month". It is related to Timestamp in that the difference between + two Timestamp values is a Duration and it can be added or subtracted + from a Timestamp. Range is approximately +-10,000 years. + + # Examples + + Example 1: Compute Duration from two Timestamps in pseudo code. + + Timestamp start = ...; + Timestamp end = ...; + Duration duration = ...; + + duration.seconds = end.seconds - start.seconds; + duration.nanos = end.nanos - start.nanos; + + if (duration.seconds < 0 && duration.nanos > 0) { + duration.seconds += 1; + duration.nanos -= 1000000000; + } else if (duration.seconds > 0 && duration.nanos < 0) { + duration.seconds -= 1; + duration.nanos += 1000000000; + } + + Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. + + Timestamp start = ...; + Duration duration = ...; + Timestamp end = ...; + + end.seconds = start.seconds + duration.seconds; + end.nanos = start.nanos + duration.nanos; + + if (end.nanos < 0) { + end.seconds -= 1; + end.nanos += 1000000000; + } else if (end.nanos >= 1000000000) { + end.seconds += 1; + end.nanos -= 1000000000; + } + + Example 3: Compute Duration from datetime.timedelta in Python. + + td = datetime.timedelta(days=3, minutes=10) + duration = Duration() + duration.FromTimedelta(td) + + # JSON Mapping + + In JSON format, the Duration type is encoded as a string rather than an + object, where the string ends in the suffix "s" (indicating seconds) and + is preceded by the number of seconds, with nanoseconds expressed as + fractional seconds. For example, 3 seconds with 0 nanoseconds should be + encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should + be expressed in JSON format as "3.000000001s", and 3 seconds and 1 + microsecond should be expressed in JSON format as "3.000001s". + """ + + seconds: int = aristaproto.int64_field(1) + """ + Signed seconds of the span of time. Must be from -315,576,000,000 + to +315,576,000,000 inclusive. Note: these bounds are computed from: + 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years + """ + + nanos: int = aristaproto.int32_field(2) + """ + Signed fractions of a second at nanosecond resolution of the span + of time. Durations less than one second are represented with a 0 + `seconds` field and a positive or negative `nanos` field. For durations + of one second or more, a non-zero value for the `nanos` field must be + of the same sign as the `seconds` field. Must be from -999,999,999 + to +999,999,999 inclusive. + """ + + +@dataclass(eq=False, repr=False) +class Empty(aristaproto.Message): + """ + A generic empty message that you can re-use to avoid defining duplicated + empty messages in your APIs. A typical example is to use it as the request + or the response type of an API method. For instance: + + service Foo { + rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); + } + """ + + pass + + +@dataclass(eq=False, repr=False) +class FieldMask(aristaproto.Message): + """ + `FieldMask` represents a set of symbolic field paths, for example: + + paths: "f.a" + paths: "f.b.d" + + Here `f` represents a field in some root message, `a` and `b` + fields in the message found in `f`, and `d` a field found in the + message in `f.b`. + + Field masks are used to specify a subset of fields that should be + returned by a get operation or modified by an update operation. + Field masks also have a custom JSON encoding (see below). + + # Field Masks in Projections + + When used in the context of a projection, a response message or + sub-message is filtered by the API to only contain those fields as + specified in the mask. For example, if the mask in the previous + example is applied to a response message as follows: + + f { + a : 22 + b { + d : 1 + x : 2 + } + y : 13 + } + z: 8 + + The result will not contain specific values for fields x,y and z + (their value will be set to the default, and omitted in proto text + output): + + + f { + a : 22 + b { + d : 1 + } + } + + A repeated field is not allowed except at the last position of a + paths string. + + If a FieldMask object is not present in a get operation, the + operation applies to all fields (as if a FieldMask of all fields + had been specified). + + Note that a field mask does not necessarily apply to the + top-level response message. In case of a REST get operation, the + field mask applies directly to the response, but in case of a REST + list operation, the mask instead applies to each individual message + in the returned resource list. In case of a REST custom method, + other definitions may be used. Where the mask applies will be + clearly documented together with its declaration in the API. In + any case, the effect on the returned resource/resources is required + behavior for APIs. + + # Field Masks in Update Operations + + A field mask in update operations specifies which fields of the + targeted resource are going to be updated. The API is required + to only change the values of the fields as specified in the mask + and leave the others untouched. If a resource is passed in to + describe the updated values, the API ignores the values of all + fields not covered by the mask. + + If a repeated field is specified for an update operation, new values will + be appended to the existing repeated field in the target resource. Note that + a repeated field is only allowed in the last position of a `paths` string. + + If a sub-message is specified in the last position of the field mask for an + update operation, then new value will be merged into the existing sub-message + in the target resource. + + For example, given the target message: + + f { + b { + d: 1 + x: 2 + } + c: [1] + } + + And an update message: + + f { + b { + d: 10 + } + c: [2] + } + + then if the field mask is: + + paths: ["f.b", "f.c"] + + then the result will be: + + f { + b { + d: 10 + x: 2 + } + c: [1, 2] + } + + An implementation may provide options to override this default behavior for + repeated and message fields. + + In order to reset a field's value to the default, the field must + be in the mask and set to the default value in the provided resource. + Hence, in order to reset all fields of a resource, provide a default + instance of the resource and set all fields in the mask, or do + not provide a mask as described below. + + If a field mask is not present on update, the operation applies to + all fields (as if a field mask of all fields has been specified). + Note that in the presence of schema evolution, this may mean that + fields the client does not know and has therefore not filled into + the request will be reset to their default. If this is unwanted + behavior, a specific service may require a client to always specify + a field mask, producing an error if not. + + As with get operations, the location of the resource which + describes the updated values in the request message depends on the + operation kind. In any case, the effect of the field mask is + required to be honored by the API. + + ## Considerations for HTTP REST + + The HTTP kind of an update operation which uses a field mask must + be set to PATCH instead of PUT in order to satisfy HTTP semantics + (PUT must only be used for full updates). + + # JSON Encoding of Field Masks + + In JSON, a field mask is encoded as a single string where paths are + separated by a comma. Fields name in each path are converted + to/from lower-camel naming conventions. + + As an example, consider the following message declarations: + + message Profile { + User user = 1; + Photo photo = 2; + } + message User { + string display_name = 1; + string address = 2; + } + + In proto a field mask for `Profile` may look as such: + + mask { + paths: "user.display_name" + paths: "photo" + } + + In JSON, the same mask is represented as below: + + { + mask: "user.displayName,photo" + } + + # Field Masks and Oneof Fields + + Field masks treat fields in oneofs just as regular fields. Consider the + following message: + + message SampleMessage { + oneof test_oneof { + string name = 4; + SubMessage sub_message = 9; + } + } + + The field mask can be: + + mask { + paths: "name" + } + + Or: + + mask { + paths: "sub_message" + } + + Note that oneof type names ("test_oneof" in this case) cannot be used in + paths. + + ## Field Mask Verification + + The implementation of any API method which has a FieldMask type field in the + request should verify the included field paths, and return an + `INVALID_ARGUMENT` error if any path is unmappable. + """ + + paths: List[str] = aristaproto.string_field(1) + """The set of field mask paths.""" + + +@dataclass(eq=False, repr=False) +class Struct(aristaproto.Message): + """ + `Struct` represents a structured data value, consisting of fields + which map to dynamically typed values. In some languages, `Struct` + might be supported by a native representation. For example, in + scripting languages like JS a struct is represented as an + object. The details of that representation are described together + with the proto support for the language. + + The JSON representation for `Struct` is JSON object. + """ + + fields: Dict[str, "Value"] = aristaproto.map_field( + 1, aristaproto.TYPE_STRING, aristaproto.TYPE_MESSAGE + ) + """Unordered map of dynamically typed values.""" + + @hybridmethod + def from_dict(cls: "type[Self]", value: Mapping[str, Any]) -> Self: # type: ignore + self = cls() + return self.from_dict(value) + + @from_dict.instancemethod + def from_dict(self, value: Mapping[str, Any]) -> Self: + fields = {**value} + for k in fields: + if hasattr(fields[k], "from_dict"): + fields[k] = fields[k].from_dict() + + self.fields = fields + return self + + def to_dict( + self, + casing: aristaproto.Casing = aristaproto.Casing.CAMEL, + include_default_values: bool = False, + ) -> Dict[str, Any]: + output = {**self.fields} + for k in self.fields: + if hasattr(self.fields[k], "to_dict"): + output[k] = self.fields[k].to_dict(casing, include_default_values) + return output + + +@dataclass(eq=False, repr=False) +class Value(aristaproto.Message): + """ + `Value` represents a dynamically typed value which can be either + null, a number, a string, a boolean, a recursive struct value, or a + list of values. A producer of value is expected to set one of these + variants. Absence of any variant indicates an error. + + The JSON representation for `Value` is JSON value. + """ + + null_value: "NullValue" = aristaproto.enum_field(1, group="kind") + """Represents a null value.""" + + number_value: float = aristaproto.double_field(2, group="kind") + """Represents a double value.""" + + string_value: str = aristaproto.string_field(3, group="kind") + """Represents a string value.""" + + bool_value: bool = aristaproto.bool_field(4, group="kind") + """Represents a boolean value.""" + + struct_value: "Struct" = aristaproto.message_field(5, group="kind") + """Represents a structured value.""" + + list_value: "ListValue" = aristaproto.message_field(6, group="kind") + """Represents a repeated `Value`.""" + + +@dataclass(eq=False, repr=False) +class ListValue(aristaproto.Message): + """ + `ListValue` is a wrapper around a repeated field of values. + + The JSON representation for `ListValue` is JSON array. + """ + + values: List["Value"] = aristaproto.message_field(1) + """Repeated field of dynamically typed values.""" + + +@dataclass(eq=False, repr=False) +class Timestamp(aristaproto.Message): + """ + A Timestamp represents a point in time independent of any time zone or local + calendar, encoded as a count of seconds and fractions of seconds at + nanosecond resolution. The count is relative to an epoch at UTC midnight on + January 1, 1970, in the proleptic Gregorian calendar which extends the + Gregorian calendar backwards to year one. + + All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap + second table is needed for interpretation, using a [24-hour linear + smear](https://developers.google.com/time/smear). + + The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By + restricting to that range, we ensure that we can convert to and from [RFC + 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. + + # Examples + + Example 1: Compute Timestamp from POSIX `time()`. + + Timestamp timestamp; + timestamp.set_seconds(time(NULL)); + timestamp.set_nanos(0); + + Example 2: Compute Timestamp from POSIX `gettimeofday()`. + + struct timeval tv; + gettimeofday(&tv, NULL); + + Timestamp timestamp; + timestamp.set_seconds(tv.tv_sec); + timestamp.set_nanos(tv.tv_usec * 1000); + + Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. + + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; + + // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z + // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. + Timestamp timestamp; + timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); + timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); + + Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. + + long millis = System.currentTimeMillis(); + + Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) + .setNanos((int) ((millis % 1000) * 1000000)).build(); + + Example 5: Compute Timestamp from Java `Instant.now()`. + + Instant now = Instant.now(); + + Timestamp timestamp = + Timestamp.newBuilder().setSeconds(now.getEpochSecond()) + .setNanos(now.getNano()).build(); + + Example 6: Compute Timestamp from current time in Python. + + timestamp = Timestamp() + timestamp.GetCurrentTime() + + # JSON Mapping + + In JSON format, the Timestamp type is encoded as a string in the + [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the + format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" + where {year} is always expressed using four digits while {month}, {day}, + {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional + seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), + are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone + is required. A proto3 JSON serializer should always use UTC (as indicated by + "Z") when printing the Timestamp type and a proto3 JSON parser should be + able to accept both UTC and other timezones (as indicated by an offset). + + For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past + 01:30 UTC on January 15, 2017. + + In JavaScript, one can convert a Date object to this format using the + standard + [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) + method. In Python, a standard `datetime.datetime` object can be converted + to this format using + [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with + the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use + the Joda Time's [`ISODateTimeFormat.dateTime()`]( + http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime() + ) to obtain a formatter capable of generating timestamps in this format. + """ + + seconds: int = aristaproto.int64_field(1) + """ + Represents seconds of UTC time since Unix epoch + 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to + 9999-12-31T23:59:59Z inclusive. + """ + + nanos: int = aristaproto.int32_field(2) + """ + Non-negative fractions of a second at nanosecond resolution. Negative + second values with fractions must still have non-negative nanos values + that count forward in time. Must be from 0 to 999,999,999 + inclusive. + """ + + +@dataclass(eq=False, repr=False) +class DoubleValue(aristaproto.Message): + """ + Wrapper message for `double`. + + The JSON representation for `DoubleValue` is JSON number. + """ + + value: float = aristaproto.double_field(1) + """The double value.""" + + +@dataclass(eq=False, repr=False) +class FloatValue(aristaproto.Message): + """ + Wrapper message for `float`. + + The JSON representation for `FloatValue` is JSON number. + """ + + value: float = aristaproto.float_field(1) + """The float value.""" + + +@dataclass(eq=False, repr=False) +class Int64Value(aristaproto.Message): + """ + Wrapper message for `int64`. + + The JSON representation for `Int64Value` is JSON string. + """ + + value: int = aristaproto.int64_field(1) + """The int64 value.""" + + +@dataclass(eq=False, repr=False) +class UInt64Value(aristaproto.Message): + """ + Wrapper message for `uint64`. + + The JSON representation for `UInt64Value` is JSON string. + """ + + value: int = aristaproto.uint64_field(1) + """The uint64 value.""" + + +@dataclass(eq=False, repr=False) +class Int32Value(aristaproto.Message): + """ + Wrapper message for `int32`. + + The JSON representation for `Int32Value` is JSON number. + """ + + value: int = aristaproto.int32_field(1) + """The int32 value.""" + + +@dataclass(eq=False, repr=False) +class UInt32Value(aristaproto.Message): + """ + Wrapper message for `uint32`. + + The JSON representation for `UInt32Value` is JSON number. + """ + + value: int = aristaproto.uint32_field(1) + """The uint32 value.""" + + +@dataclass(eq=False, repr=False) +class BoolValue(aristaproto.Message): + """ + Wrapper message for `bool`. + + The JSON representation for `BoolValue` is JSON `true` and `false`. + """ + + value: bool = aristaproto.bool_field(1) + """The bool value.""" + + +@dataclass(eq=False, repr=False) +class StringValue(aristaproto.Message): + """ + Wrapper message for `string`. + + The JSON representation for `StringValue` is JSON string. + """ + + value: str = aristaproto.string_field(1) + """The string value.""" + + +@dataclass(eq=False, repr=False) +class BytesValue(aristaproto.Message): + """ + Wrapper message for `bytes`. + + The JSON representation for `BytesValue` is JSON string. + """ + + value: bytes = aristaproto.bytes_field(1) + """The bytes value.""" diff --git a/src/aristaproto/lib/std/google/protobuf/compiler/__init__.py b/src/aristaproto/lib/std/google/protobuf/compiler/__init__.py new file mode 100644 index 0000000..a26dc86 --- /dev/null +++ b/src/aristaproto/lib/std/google/protobuf/compiler/__init__.py @@ -0,0 +1,198 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: google/protobuf/compiler/plugin.proto +# plugin: python-aristaproto +# This file has been @generated + +from dataclasses import dataclass +from typing import List + +import aristaproto +import aristaproto.lib.google.protobuf as aristaproto_lib_google_protobuf + + +class CodeGeneratorResponseFeature(aristaproto.Enum): + """Sync with code_generator.h.""" + + FEATURE_NONE = 0 + FEATURE_PROTO3_OPTIONAL = 1 + FEATURE_SUPPORTS_EDITIONS = 2 + + +@dataclass(eq=False, repr=False) +class Version(aristaproto.Message): + """The version number of protocol compiler.""" + + major: int = aristaproto.int32_field(1) + minor: int = aristaproto.int32_field(2) + patch: int = aristaproto.int32_field(3) + suffix: str = aristaproto.string_field(4) + """ + A suffix for alpha, beta or rc release, e.g., "alpha-1", "rc2". It should + be empty for mainline stable releases. + """ + + +@dataclass(eq=False, repr=False) +class CodeGeneratorRequest(aristaproto.Message): + """An encoded CodeGeneratorRequest is written to the plugin's stdin.""" + + file_to_generate: List[str] = aristaproto.string_field(1) + """ + The .proto files that were explicitly listed on the command-line. The + code generator should generate code only for these files. Each file's + descriptor will be included in proto_file, below. + """ + + parameter: str = aristaproto.string_field(2) + """The generator parameter passed on the command-line.""" + + proto_file: List[ + "aristaproto_lib_google_protobuf.FileDescriptorProto" + ] = aristaproto.message_field(15) + """ + FileDescriptorProtos for all files in files_to_generate and everything + they import. The files will appear in topological order, so each file + appears before any file that imports it. + + Note: the files listed in files_to_generate will include runtime-retention + options only, but all other files will include source-retention options. + The source_file_descriptors field below is available in case you need + source-retention options for files_to_generate. + + protoc guarantees that all proto_files will be written after + the fields above, even though this is not technically guaranteed by the + protobuf wire format. This theoretically could allow a plugin to stream + in the FileDescriptorProtos and handle them one by one rather than read + the entire set into memory at once. However, as of this writing, this + is not similarly optimized on protoc's end -- it will store all fields in + memory at once before sending them to the plugin. + + Type names of fields and extensions in the FileDescriptorProto are always + fully qualified. + """ + + source_file_descriptors: List[ + "aristaproto_lib_google_protobuf.FileDescriptorProto" + ] = aristaproto.message_field(17) + """ + File descriptors with all options, including source-retention options. + These descriptors are only provided for the files listed in + files_to_generate. + """ + + compiler_version: "Version" = aristaproto.message_field(3) + """The version number of protocol compiler.""" + + +@dataclass(eq=False, repr=False) +class CodeGeneratorResponse(aristaproto.Message): + """The plugin writes an encoded CodeGeneratorResponse to stdout.""" + + error: str = aristaproto.string_field(1) + """ + Error message. If non-empty, code generation failed. The plugin process + should exit with status code zero even if it reports an error in this way. + + This should be used to indicate errors in .proto files which prevent the + code generator from generating correct code. Errors which indicate a + problem in protoc itself -- such as the input CodeGeneratorRequest being + unparseable -- should be reported by writing a message to stderr and + exiting with a non-zero status code. + """ + + supported_features: int = aristaproto.uint64_field(2) + """ + A bitmask of supported features that the code generator supports. + This is a bitwise "or" of values from the Feature enum. + """ + + minimum_edition: int = aristaproto.int32_field(3) + """ + The minimum edition this plugin supports. This will be treated as an + Edition enum, but we want to allow unknown values. It should be specified + according the edition enum value, *not* the edition number. Only takes + effect for plugins that have FEATURE_SUPPORTS_EDITIONS set. + """ + + maximum_edition: int = aristaproto.int32_field(4) + """ + The maximum edition this plugin supports. This will be treated as an + Edition enum, but we want to allow unknown values. It should be specified + according the edition enum value, *not* the edition number. Only takes + effect for plugins that have FEATURE_SUPPORTS_EDITIONS set. + """ + + file: List["CodeGeneratorResponseFile"] = aristaproto.message_field(15) + + +@dataclass(eq=False, repr=False) +class CodeGeneratorResponseFile(aristaproto.Message): + """Represents a single generated file.""" + + name: str = aristaproto.string_field(1) + """ + The file name, relative to the output directory. The name must not + contain "." or ".." components and must be relative, not be absolute (so, + the file cannot lie outside the output directory). "/" must be used as + the path separator, not "\". + + If the name is omitted, the content will be appended to the previous + file. This allows the generator to break large files into small chunks, + and allows the generated text to be streamed back to protoc so that large + files need not reside completely in memory at one time. Note that as of + this writing protoc does not optimize for this -- it will read the entire + CodeGeneratorResponse before writing files to disk. + """ + + insertion_point: str = aristaproto.string_field(2) + """ + If non-empty, indicates that the named file should already exist, and the + content here is to be inserted into that file at a defined insertion + point. This feature allows a code generator to extend the output + produced by another code generator. The original generator may provide + insertion points by placing special annotations in the file that look + like: + @@protoc_insertion_point(NAME) + The annotation can have arbitrary text before and after it on the line, + which allows it to be placed in a comment. NAME should be replaced with + an identifier naming the point -- this is what other generators will use + as the insertion_point. Code inserted at this point will be placed + immediately above the line containing the insertion point (thus multiple + insertions to the same point will come out in the order they were added). + The double-@ is intended to make it unlikely that the generated code + could contain things that look like insertion points by accident. + + For example, the C++ code generator places the following line in the + .pb.h files that it generates: + // @@protoc_insertion_point(namespace_scope) + This line appears within the scope of the file's package namespace, but + outside of any particular class. Another plugin can then specify the + insertion_point "namespace_scope" to generate additional classes or + other declarations that should be placed in this scope. + + Note that if the line containing the insertion point begins with + whitespace, the same whitespace will be added to every line of the + inserted text. This is useful for languages like Python, where + indentation matters. In these languages, the insertion point comment + should be indented the same amount as any inserted code will need to be + in order to work correctly in that context. + + The code generator that generates the initial file and the one which + inserts into it must both run as part of a single invocation of protoc. + Code generators are executed in the order in which they appear on the + command line. + + If |insertion_point| is present, |name| must also be present. + """ + + content: str = aristaproto.string_field(15) + """The file contents.""" + + generated_code_info: "aristaproto_lib_google_protobuf.GeneratedCodeInfo" = ( + aristaproto.message_field(16) + ) + """ + Information describing the file content being inserted. If an insertion + point is used, this information will be appropriately offset and inserted + into the code generation metadata for the generated files. + """ diff --git a/src/aristaproto/plugin/__init__.py b/src/aristaproto/plugin/__init__.py new file mode 100644 index 0000000..c28a133 --- /dev/null +++ b/src/aristaproto/plugin/__init__.py @@ -0,0 +1 @@ +from .main import main diff --git a/src/aristaproto/plugin/__main__.py b/src/aristaproto/plugin/__main__.py new file mode 100644 index 0000000..bd95dae --- /dev/null +++ b/src/aristaproto/plugin/__main__.py @@ -0,0 +1,4 @@ +from .main import main + + +main() diff --git a/src/aristaproto/plugin/compiler.py b/src/aristaproto/plugin/compiler.py new file mode 100644 index 0000000..4bbcc48 --- /dev/null +++ b/src/aristaproto/plugin/compiler.py @@ -0,0 +1,50 @@ +import os.path + + +try: + # aristaproto[compiler] specific dependencies + import black + import isort.api + import jinja2 +except ImportError as err: + print( + "\033[31m" + f"Unable to import `{err.name}` from aristaproto plugin! " + "Please ensure that you've installed aristaproto as " + '`pip install "aristaproto[compiler]"` so that compiler dependencies ' + "are included." + "\033[0m" + ) + raise SystemExit(1) + +from .models import OutputTemplate + + +def outputfile_compiler(output_file: OutputTemplate) -> str: + templates_folder = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "templates") + ) + + env = jinja2.Environment( + trim_blocks=True, + lstrip_blocks=True, + loader=jinja2.FileSystemLoader(templates_folder), + ) + template = env.get_template("template.py.j2") + + code = template.render(output_file=output_file) + code = isort.api.sort_code_string( + code=code, + show_diff=False, + py_version=37, + profile="black", + combine_as_imports=True, + lines_after_imports=2, + quiet=True, + force_grid_wrap=2, + known_third_party=["grpclib", "aristaproto"], + ) + return black.format_str( + src_contents=code, + mode=black.Mode(), + ) diff --git a/src/aristaproto/plugin/main.py b/src/aristaproto/plugin/main.py new file mode 100755 index 0000000..aff3614 --- /dev/null +++ b/src/aristaproto/plugin/main.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +import os +import sys + +from aristaproto.lib.google.protobuf.compiler import ( + CodeGeneratorRequest, + CodeGeneratorResponse, +) +from aristaproto.plugin.models import monkey_patch_oneof_index +from aristaproto.plugin.parser import generate_code + + +def main() -> None: + """The plugin's main entry point.""" + # Read request message from stdin + data = sys.stdin.buffer.read() + + # Apply Work around for proto2/3 difference in protoc messages + monkey_patch_oneof_index() + + # Parse request + request = CodeGeneratorRequest() + request.parse(data) + + dump_file = os.getenv("ARISTAPROTO_DUMP") + if dump_file: + dump_request(dump_file, request) + + # Generate code + response = generate_code(request) + + # Serialise response message + output = response.SerializeToString() + + # Write to stdout + sys.stdout.buffer.write(output) + + +def dump_request(dump_file: str, request: CodeGeneratorRequest) -> None: + """ + For developers: Supports running plugin.py standalone so its possible to debug it. + Run protoc (or generate.py) with ARISTAPROTO_DUMP="yourfile.bin" to write the request to a file. + Then run plugin.py from your IDE in debugging mode, and redirect stdin to the file. + """ + with open(str(dump_file), "wb") as fh: + sys.stderr.write(f"\033[31mWriting input from protoc to: {dump_file}\033[0m\n") + fh.write(request.SerializeToString()) + + +if __name__ == "__main__": + main() diff --git a/src/aristaproto/plugin/models.py b/src/aristaproto/plugin/models.py new file mode 100644 index 0000000..484b40d --- /dev/null +++ b/src/aristaproto/plugin/models.py @@ -0,0 +1,851 @@ +"""Plugin model dataclasses. + +These classes are meant to be an intermediate representation +of protobuf objects. They are used to organize the data collected during parsing. + +The general intention is to create a doubly-linked tree-like structure +with the following types of references: +- Downwards references: from message -> fields, from output package -> messages +or from service -> service methods +- Upwards references: from field -> message, message -> package. +- Input/output message references: from a service method to it's corresponding +input/output messages, which may even be in another package. + +There are convenience methods to allow climbing up and down this tree, for +example to retrieve the list of all messages that are in the same package as +the current message. + +Most of these classes take as inputs: +- proto_obj: A reference to it's corresponding protobuf object as +presented by the protoc plugin. +- parent: a reference to the parent object in the tree. + +With this information, the class is able to expose attributes, +such as a pythonized name, that will be calculated from proto_obj. + +The instantiation should also attach a reference to the new object +into the corresponding place within it's parent object. For example, +instantiating field `A` with parent message `B` should add a +reference to `A` to `B`'s `fields` attribute. +""" + + +import builtins +import re +import textwrap +from dataclasses import ( + dataclass, + field, +) +from typing import ( + Dict, + Iterable, + Iterator, + List, + Optional, + Set, + Type, + Union, +) + +import aristaproto +from aristaproto import which_one_of +from aristaproto.casing import sanitize_name +from aristaproto.compile.importing import ( + get_type_reference, + parse_source_type_name, +) +from aristaproto.compile.naming import ( + pythonize_class_name, + pythonize_field_name, + pythonize_method_name, +) +from aristaproto.lib.google.protobuf import ( + DescriptorProto, + EnumDescriptorProto, + Field, + FieldDescriptorProto, + FieldDescriptorProtoLabel, + FieldDescriptorProtoType, + FileDescriptorProto, + MethodDescriptorProto, +) +from aristaproto.lib.google.protobuf.compiler import CodeGeneratorRequest + +from ..compile.importing import ( + get_type_reference, + parse_source_type_name, +) +from ..compile.naming import ( + pythonize_class_name, + pythonize_enum_member_name, + pythonize_field_name, + pythonize_method_name, +) + + +# Create a unique placeholder to deal with +# https://stackoverflow.com/questions/51575931/class-inheritance-in-python-3-7-dataclasses +PLACEHOLDER = object() + +# Organize proto types into categories +PROTO_FLOAT_TYPES = ( + FieldDescriptorProtoType.TYPE_DOUBLE, # 1 + FieldDescriptorProtoType.TYPE_FLOAT, # 2 +) +PROTO_INT_TYPES = ( + FieldDescriptorProtoType.TYPE_INT64, # 3 + FieldDescriptorProtoType.TYPE_UINT64, # 4 + FieldDescriptorProtoType.TYPE_INT32, # 5 + FieldDescriptorProtoType.TYPE_FIXED64, # 6 + FieldDescriptorProtoType.TYPE_FIXED32, # 7 + FieldDescriptorProtoType.TYPE_UINT32, # 13 + FieldDescriptorProtoType.TYPE_SFIXED32, # 15 + FieldDescriptorProtoType.TYPE_SFIXED64, # 16 + FieldDescriptorProtoType.TYPE_SINT32, # 17 + FieldDescriptorProtoType.TYPE_SINT64, # 18 +) +PROTO_BOOL_TYPES = (FieldDescriptorProtoType.TYPE_BOOL,) # 8 +PROTO_STR_TYPES = (FieldDescriptorProtoType.TYPE_STRING,) # 9 +PROTO_BYTES_TYPES = (FieldDescriptorProtoType.TYPE_BYTES,) # 12 +PROTO_MESSAGE_TYPES = ( + FieldDescriptorProtoType.TYPE_MESSAGE, # 11 + FieldDescriptorProtoType.TYPE_ENUM, # 14 +) +PROTO_MAP_TYPES = (FieldDescriptorProtoType.TYPE_MESSAGE,) # 11 +PROTO_PACKED_TYPES = ( + FieldDescriptorProtoType.TYPE_DOUBLE, # 1 + FieldDescriptorProtoType.TYPE_FLOAT, # 2 + FieldDescriptorProtoType.TYPE_INT64, # 3 + FieldDescriptorProtoType.TYPE_UINT64, # 4 + FieldDescriptorProtoType.TYPE_INT32, # 5 + FieldDescriptorProtoType.TYPE_FIXED64, # 6 + FieldDescriptorProtoType.TYPE_FIXED32, # 7 + FieldDescriptorProtoType.TYPE_BOOL, # 8 + FieldDescriptorProtoType.TYPE_UINT32, # 13 + FieldDescriptorProtoType.TYPE_SFIXED32, # 15 + FieldDescriptorProtoType.TYPE_SFIXED64, # 16 + FieldDescriptorProtoType.TYPE_SINT32, # 17 + FieldDescriptorProtoType.TYPE_SINT64, # 18 +) + + +def monkey_patch_oneof_index(): + """ + The compiler message types are written for proto2, but we read them as proto3. + For this to work in the case of the oneof_index fields, which depend on being able + to tell whether they were set, we have to treat them as oneof fields. This method + monkey patches the generated classes after the fact to force this behaviour. + """ + object.__setattr__( + FieldDescriptorProto.__dataclass_fields__["oneof_index"].metadata[ + "aristaproto" + ], + "group", + "oneof_index", + ) + object.__setattr__( + Field.__dataclass_fields__["oneof_index"].metadata["aristaproto"], + "group", + "oneof_index", + ) + + +def get_comment( + proto_file: "FileDescriptorProto", path: List[int], indent: int = 4 +) -> str: + pad = " " * indent + for sci_loc in proto_file.source_code_info.location: + if list(sci_loc.path) == path and sci_loc.leading_comments: + lines = sci_loc.leading_comments.strip().replace("\t", " ").split("\n") + # This is a field, message, enum, service, or method + if len(lines) == 1 and len(lines[0]) < 79 - indent - 6: + lines[0] = lines[0].strip('"') + # rstrip to remove trailing spaces including whitespaces from empty lines. + return f'{pad}"""{lines[0]}"""' + else: + # rstrip to remove trailing spaces including empty lines. + padded = [f"\n{pad}{line}".rstrip(" ") for line in lines] + joined = "".join(padded) + return f'{pad}"""{joined}\n{pad}"""' + + return "" + + +class ProtoContentBase: + """Methods common to MessageCompiler, ServiceCompiler and ServiceMethodCompiler.""" + + source_file: FileDescriptorProto + path: List[int] + comment_indent: int = 4 + parent: Union["aristaproto.Message", "OutputTemplate"] + + __dataclass_fields__: Dict[str, object] + + def __post_init__(self) -> None: + """Checks that no fake default fields were left as placeholders.""" + for field_name, field_val in self.__dataclass_fields__.items(): + if field_val is PLACEHOLDER: + raise ValueError(f"`{field_name}` is a required field.") + + @property + def output_file(self) -> "OutputTemplate": + current = self + while not isinstance(current, OutputTemplate): + current = current.parent + return current + + @property + def request(self) -> "PluginRequestCompiler": + current = self + while not isinstance(current, OutputTemplate): + current = current.parent + return current.parent_request + + @property + def comment(self) -> str: + """Crawl the proto source code and retrieve comments + for this object. + """ + return get_comment( + proto_file=self.source_file, path=self.path, indent=self.comment_indent + ) + + +@dataclass +class PluginRequestCompiler: + plugin_request_obj: CodeGeneratorRequest + output_packages: Dict[str, "OutputTemplate"] = field(default_factory=dict) + + @property + def all_messages(self) -> List["MessageCompiler"]: + """All of the messages in this request. + + Returns + ------- + List[MessageCompiler] + List of all of the messages in this request. + """ + return [ + msg for output in self.output_packages.values() for msg in output.messages + ] + + +@dataclass +class OutputTemplate: + """Representation of an output .py file. + + Each output file corresponds to a .proto input file, + but may need references to other .proto files to be + built. + """ + + parent_request: PluginRequestCompiler + package_proto_obj: FileDescriptorProto + input_files: List[str] = field(default_factory=list) + imports: Set[str] = field(default_factory=set) + datetime_imports: Set[str] = field(default_factory=set) + typing_imports: Set[str] = field(default_factory=set) + pydantic_imports: Set[str] = field(default_factory=set) + builtins_import: bool = False + messages: List["MessageCompiler"] = field(default_factory=list) + enums: List["EnumDefinitionCompiler"] = field(default_factory=list) + services: List["ServiceCompiler"] = field(default_factory=list) + imports_type_checking_only: Set[str] = field(default_factory=set) + pydantic_dataclasses: bool = False + output: bool = True + + @property + def package(self) -> str: + """Name of input package. + + Returns + ------- + str + Name of input package. + """ + return self.package_proto_obj.package + + @property + def input_filenames(self) -> Iterable[str]: + """Names of the input files used to build this output. + + Returns + ------- + Iterable[str] + Names of the input files used to build this output. + """ + return sorted(f.name for f in self.input_files) + + @property + def python_module_imports(self) -> Set[str]: + imports = set() + if any(x for x in self.messages if any(x.deprecated_fields)): + imports.add("warnings") + if self.builtins_import: + imports.add("builtins") + return imports + + +@dataclass +class MessageCompiler(ProtoContentBase): + """Representation of a protobuf message.""" + + source_file: FileDescriptorProto + parent: Union["MessageCompiler", OutputTemplate] = PLACEHOLDER + proto_obj: DescriptorProto = PLACEHOLDER + path: List[int] = PLACEHOLDER + fields: List[Union["FieldCompiler", "MessageCompiler"]] = field( + default_factory=list + ) + deprecated: bool = field(default=False, init=False) + builtins_types: Set[str] = field(default_factory=set) + + def __post_init__(self) -> None: + # Add message to output file + if isinstance(self.parent, OutputTemplate): + if isinstance(self, EnumDefinitionCompiler): + self.output_file.enums.append(self) + else: + self.output_file.messages.append(self) + self.deprecated = self.proto_obj.options.deprecated + super().__post_init__() + + @property + def proto_name(self) -> str: + return self.proto_obj.name + + @property + def py_name(self) -> str: + return pythonize_class_name(self.proto_name) + + @property + def annotation(self) -> str: + if self.repeated: + return f"List[{self.py_name}]" + return self.py_name + + @property + def deprecated_fields(self) -> Iterator[str]: + for f in self.fields: + if f.deprecated: + yield f.py_name + + @property + def has_deprecated_fields(self) -> bool: + return any(self.deprecated_fields) + + @property + def has_oneof_fields(self) -> bool: + return any(isinstance(field, OneOfFieldCompiler) for field in self.fields) + + @property + def has_message_field(self) -> bool: + return any( + ( + field.proto_obj.type in PROTO_MESSAGE_TYPES + for field in self.fields + if isinstance(field.proto_obj, FieldDescriptorProto) + ) + ) + + +def is_map( + proto_field_obj: FieldDescriptorProto, parent_message: DescriptorProto +) -> bool: + """True if proto_field_obj is a map, otherwise False.""" + if proto_field_obj.type == FieldDescriptorProtoType.TYPE_MESSAGE: + if not hasattr(parent_message, "nested_type"): + return False + + # This might be a map... + message_type = proto_field_obj.type_name.split(".").pop().lower() + map_entry = f"{proto_field_obj.name.replace('_', '').lower()}entry" + if message_type == map_entry: + for nested in parent_message.nested_type: # parent message + if ( + nested.name.replace("_", "").lower() == map_entry + and nested.options.map_entry + ): + return True + return False + + +def is_oneof(proto_field_obj: FieldDescriptorProto) -> bool: + """ + True if proto_field_obj is a OneOf, otherwise False. + + .. warning:: + Becuase the message from protoc is defined in proto2, and aristaproto works with + proto3, and interpreting the FieldDescriptorProto.oneof_index field requires + distinguishing between default and unset values (which proto3 doesn't support), + we have to hack the generated FieldDescriptorProto class for this to work. + The hack consists of setting group="oneof_index" in the field metadata, + essentially making oneof_index the sole member of a one_of group, which allows + us to tell whether it was set, via the which_one_of interface. + """ + + return ( + not proto_field_obj.proto3_optional + and which_one_of(proto_field_obj, "oneof_index")[0] == "oneof_index" + ) + + +@dataclass +class FieldCompiler(MessageCompiler): + parent: MessageCompiler = PLACEHOLDER + proto_obj: FieldDescriptorProto = PLACEHOLDER + + def __post_init__(self) -> None: + # Add field to message + self.parent.fields.append(self) + # Check for new imports + self.add_imports_to(self.output_file) + super().__post_init__() # call FieldCompiler-> MessageCompiler __post_init__ + + def get_field_string(self, indent: int = 4) -> str: + """Construct string representation of this field as a field.""" + name = f"{self.py_name}" + annotations = f": {self.annotation}" + field_args = ", ".join( + ([""] + self.aristaproto_field_args) if self.aristaproto_field_args else [] + ) + aristaproto_field_type = ( + f"aristaproto.{self.field_type}_field({self.proto_obj.number}{field_args})" + ) + if self.py_name in dir(builtins): + self.parent.builtins_types.add(self.py_name) + return f"{name}{annotations} = {aristaproto_field_type}" + + @property + def aristaproto_field_args(self) -> List[str]: + args = [] + if self.field_wraps: + args.append(f"wraps={self.field_wraps}") + if self.optional: + args.append(f"optional=True") + return args + + @property + def datetime_imports(self) -> Set[str]: + imports = set() + annotation = self.annotation + # FIXME: false positives - e.g. `MyDatetimedelta` + if "timedelta" in annotation: + imports.add("timedelta") + if "datetime" in annotation: + imports.add("datetime") + return imports + + @property + def typing_imports(self) -> Set[str]: + imports = set() + annotation = self.annotation + if "Optional[" in annotation: + imports.add("Optional") + if "List[" in annotation: + imports.add("List") + if "Dict[" in annotation: + imports.add("Dict") + return imports + + @property + def pydantic_imports(self) -> Set[str]: + return set() + + @property + def use_builtins(self) -> bool: + return self.py_type in self.parent.builtins_types or ( + self.py_type == self.py_name and self.py_name in dir(builtins) + ) + + def add_imports_to(self, output_file: OutputTemplate) -> None: + output_file.datetime_imports.update(self.datetime_imports) + output_file.typing_imports.update(self.typing_imports) + output_file.pydantic_imports.update(self.pydantic_imports) + output_file.builtins_import = output_file.builtins_import or self.use_builtins + + @property + def field_wraps(self) -> Optional[str]: + """Returns aristaproto wrapped field type or None.""" + match_wrapper = re.match( + r"\.google\.protobuf\.(.+)Value$", self.proto_obj.type_name + ) + if match_wrapper: + wrapped_type = "TYPE_" + match_wrapper.group(1).upper() + if hasattr(aristaproto, wrapped_type): + return f"aristaproto.{wrapped_type}" + return None + + @property + def repeated(self) -> bool: + return ( + self.proto_obj.label == FieldDescriptorProtoLabel.LABEL_REPEATED + and not is_map(self.proto_obj, self.parent) + ) + + @property + def optional(self) -> bool: + return self.proto_obj.proto3_optional + + @property + def mutable(self) -> bool: + """True if the field is a mutable type, otherwise False.""" + return self.annotation.startswith(("List[", "Dict[")) + + @property + def field_type(self) -> str: + """String representation of proto field type.""" + return ( + FieldDescriptorProtoType(self.proto_obj.type) + .name.lower() + .replace("type_", "") + ) + + @property + def default_value_string(self) -> str: + """Python representation of the default proto value.""" + if self.repeated: + return "[]" + if self.optional: + return "None" + if self.py_type == "int": + return "0" + if self.py_type == "float": + return "0.0" + elif self.py_type == "bool": + return "False" + elif self.py_type == "str": + return '""' + elif self.py_type == "bytes": + return 'b""' + elif self.field_type == "enum": + enum_proto_obj_name = self.proto_obj.type_name.split(".").pop() + enum = next( + e + for e in self.output_file.enums + if e.proto_obj.name == enum_proto_obj_name + ) + return enum.default_value_string + else: + # Message type + return "None" + + @property + def packed(self) -> bool: + """True if the wire representation is a packed format.""" + return self.repeated and self.proto_obj.type in PROTO_PACKED_TYPES + + @property + def py_name(self) -> str: + """Pythonized name.""" + return pythonize_field_name(self.proto_name) + + @property + def proto_name(self) -> str: + """Original protobuf name.""" + return self.proto_obj.name + + @property + def py_type(self) -> str: + """String representation of Python type.""" + if self.proto_obj.type in PROTO_FLOAT_TYPES: + return "float" + elif self.proto_obj.type in PROTO_INT_TYPES: + return "int" + elif self.proto_obj.type in PROTO_BOOL_TYPES: + return "bool" + elif self.proto_obj.type in PROTO_STR_TYPES: + return "str" + elif self.proto_obj.type in PROTO_BYTES_TYPES: + return "bytes" + elif self.proto_obj.type in PROTO_MESSAGE_TYPES: + # Type referencing another defined Message or a named enum + return get_type_reference( + package=self.output_file.package, + imports=self.output_file.imports, + source_type=self.proto_obj.type_name, + pydantic=self.output_file.pydantic_dataclasses, + ) + else: + raise NotImplementedError(f"Unknown type {self.proto_obj.type}") + + @property + def annotation(self) -> str: + py_type = self.py_type + if self.use_builtins: + py_type = f"builtins.{py_type}" + if self.repeated: + return f"List[{py_type}]" + if self.optional: + return f"Optional[{py_type}]" + return py_type + + +@dataclass +class OneOfFieldCompiler(FieldCompiler): + @property + def aristaproto_field_args(self) -> List[str]: + args = super().aristaproto_field_args + group = self.parent.proto_obj.oneof_decl[self.proto_obj.oneof_index].name + args.append(f'group="{group}"') + return args + + +@dataclass +class PydanticOneOfFieldCompiler(OneOfFieldCompiler): + @property + def optional(self) -> bool: + # Force the optional to be True. This will allow the pydantic dataclass + # to validate the object correctly by allowing the field to be let empty. + # We add a pydantic validator later to ensure exactly one field is defined. + return True + + @property + def pydantic_imports(self) -> Set[str]: + return {"root_validator"} + + +@dataclass +class MapEntryCompiler(FieldCompiler): + py_k_type: Type = PLACEHOLDER + py_v_type: Type = PLACEHOLDER + proto_k_type: str = PLACEHOLDER + proto_v_type: str = PLACEHOLDER + + def __post_init__(self) -> None: + """Explore nested types and set k_type and v_type if unset.""" + map_entry = f"{self.proto_obj.name.replace('_', '').lower()}entry" + for nested in self.parent.proto_obj.nested_type: + if ( + nested.name.replace("_", "").lower() == map_entry + and nested.options.map_entry + ): + # Get Python types + self.py_k_type = FieldCompiler( + source_file=self.source_file, + parent=self, + proto_obj=nested.field[0], # key + ).py_type + self.py_v_type = FieldCompiler( + source_file=self.source_file, + parent=self, + proto_obj=nested.field[1], # value + ).py_type + + # Get proto types + self.proto_k_type = FieldDescriptorProtoType(nested.field[0].type).name + self.proto_v_type = FieldDescriptorProtoType(nested.field[1].type).name + super().__post_init__() # call FieldCompiler-> MessageCompiler __post_init__ + + @property + def aristaproto_field_args(self) -> List[str]: + return [f"aristaproto.{self.proto_k_type}", f"aristaproto.{self.proto_v_type}"] + + @property + def field_type(self) -> str: + return "map" + + @property + def annotation(self) -> str: + return f"Dict[{self.py_k_type}, {self.py_v_type}]" + + @property + def repeated(self) -> bool: + return False # maps cannot be repeated + + +@dataclass +class EnumDefinitionCompiler(MessageCompiler): + """Representation of a proto Enum definition.""" + + proto_obj: EnumDescriptorProto = PLACEHOLDER + entries: List["EnumDefinitionCompiler.EnumEntry"] = PLACEHOLDER + + @dataclass(unsafe_hash=True) + class EnumEntry: + """Representation of an Enum entry.""" + + name: str + value: int + comment: str + + def __post_init__(self) -> None: + # Get entries/allowed values for this Enum + self.entries = [ + self.EnumEntry( + name=pythonize_enum_member_name( + entry_proto_value.name, self.proto_obj.name + ), + value=entry_proto_value.number, + comment=get_comment( + proto_file=self.source_file, path=self.path + [2, entry_number] + ), + ) + for entry_number, entry_proto_value in enumerate(self.proto_obj.value) + ] + super().__post_init__() # call MessageCompiler __post_init__ + + @property + def default_value_string(self) -> str: + """Python representation of the default value for Enums. + + As per the spec, this is the first value of the Enum. + """ + return str(self.entries[0].value) # ideally, should ALWAYS be int(0)! + + +@dataclass +class ServiceCompiler(ProtoContentBase): + parent: OutputTemplate = PLACEHOLDER + proto_obj: DescriptorProto = PLACEHOLDER + path: List[int] = PLACEHOLDER + methods: List["ServiceMethodCompiler"] = field(default_factory=list) + + def __post_init__(self) -> None: + # Add service to output file + self.output_file.services.append(self) + self.output_file.typing_imports.add("Dict") + super().__post_init__() # check for unset fields + + @property + def proto_name(self) -> str: + return self.proto_obj.name + + @property + def py_name(self) -> str: + return pythonize_class_name(self.proto_name) + + +@dataclass +class ServiceMethodCompiler(ProtoContentBase): + parent: ServiceCompiler + proto_obj: MethodDescriptorProto + path: List[int] = PLACEHOLDER + comment_indent: int = 8 + + def __post_init__(self) -> None: + # Add method to service + self.parent.methods.append(self) + + # Check for imports + if "Optional" in self.py_output_message_type: + self.output_file.typing_imports.add("Optional") + + # Check for Async imports + if self.client_streaming: + self.output_file.typing_imports.add("AsyncIterable") + self.output_file.typing_imports.add("Iterable") + self.output_file.typing_imports.add("Union") + + # Required by both client and server + if self.client_streaming or self.server_streaming: + self.output_file.typing_imports.add("AsyncIterator") + + # add imports required for request arguments timeout, deadline and metadata + self.output_file.typing_imports.add("Optional") + self.output_file.imports_type_checking_only.add("import grpclib.server") + self.output_file.imports_type_checking_only.add( + "from aristaproto.grpc.grpclib_client import MetadataLike" + ) + self.output_file.imports_type_checking_only.add( + "from grpclib.metadata import Deadline" + ) + + super().__post_init__() # check for unset fields + + @property + def py_name(self) -> str: + """Pythonized method name.""" + return pythonize_method_name(self.proto_obj.name) + + @property + def proto_name(self) -> str: + """Original protobuf name.""" + return self.proto_obj.name + + @property + def route(self) -> str: + package_part = ( + f"{self.output_file.package}." if self.output_file.package else "" + ) + return f"/{package_part}{self.parent.proto_name}/{self.proto_name}" + + @property + def py_input_message(self) -> Optional[MessageCompiler]: + """Find the input message object. + + Returns + ------- + Optional[MessageCompiler] + Method instance representing the input message. + If not input message could be found or there are no + input messages, None is returned. + """ + package, name = parse_source_type_name(self.proto_obj.input_type) + + # Nested types are currently flattened without dots. + # Todo: keep a fully quantified name in types, that is + # comparable with method.input_type + for msg in self.request.all_messages: + if ( + msg.py_name == pythonize_class_name(name.replace(".", "")) + and msg.output_file.package == package + ): + return msg + return None + + @property + def py_input_message_type(self) -> str: + """String representation of the Python type corresponding to the + input message. + + Returns + ------- + str + String representation of the Python type corresponding to the input message. + """ + return get_type_reference( + package=self.output_file.package, + imports=self.output_file.imports, + source_type=self.proto_obj.input_type, + unwrap=False, + pydantic=self.output_file.pydantic_dataclasses, + ).strip('"') + + @property + def py_input_message_param(self) -> str: + """Param name corresponding to py_input_message_type. + + Returns + ------- + str + Param name corresponding to py_input_message_type. + """ + return pythonize_field_name(self.py_input_message_type) + + @property + def py_output_message_type(self) -> str: + """String representation of the Python type corresponding to the + output message. + + Returns + ------- + str + String representation of the Python type corresponding to the output message. + """ + return get_type_reference( + package=self.output_file.package, + imports=self.output_file.imports, + source_type=self.proto_obj.output_type, + unwrap=False, + pydantic=self.output_file.pydantic_dataclasses, + ).strip('"') + + @property + def client_streaming(self) -> bool: + return self.proto_obj.client_streaming + + @property + def server_streaming(self) -> bool: + return self.proto_obj.server_streaming diff --git a/src/aristaproto/plugin/parser.py b/src/aristaproto/plugin/parser.py new file mode 100644 index 0000000..f761af6 --- /dev/null +++ b/src/aristaproto/plugin/parser.py @@ -0,0 +1,221 @@ +import pathlib +import sys +from typing import ( + Generator, + List, + Set, + Tuple, + Union, +) + +from aristaproto.lib.google.protobuf import ( + DescriptorProto, + EnumDescriptorProto, + FieldDescriptorProto, + FileDescriptorProto, + ServiceDescriptorProto, +) +from aristaproto.lib.google.protobuf.compiler import ( + CodeGeneratorRequest, + CodeGeneratorResponse, + CodeGeneratorResponseFeature, + CodeGeneratorResponseFile, +) + +from .compiler import outputfile_compiler +from .models import ( + EnumDefinitionCompiler, + FieldCompiler, + MapEntryCompiler, + MessageCompiler, + OneOfFieldCompiler, + OutputTemplate, + PluginRequestCompiler, + PydanticOneOfFieldCompiler, + ServiceCompiler, + ServiceMethodCompiler, + is_map, + is_oneof, +) + + +def traverse( + proto_file: FileDescriptorProto, +) -> Generator[ + Tuple[Union[EnumDescriptorProto, DescriptorProto], List[int]], None, None +]: + # Todo: Keep information about nested hierarchy + def _traverse( + path: List[int], + items: Union[List[EnumDescriptorProto], List[DescriptorProto]], + prefix: str = "", + ) -> Generator[ + Tuple[Union[EnumDescriptorProto, DescriptorProto], List[int]], None, None + ]: + for i, item in enumerate(items): + # Adjust the name since we flatten the hierarchy. + # Todo: don't change the name, but include full name in returned tuple + item.name = next_prefix = f"{prefix}_{item.name}" + yield item, [*path, i] + + if isinstance(item, DescriptorProto): + # Get nested types. + yield from _traverse([*path, i, 4], item.enum_type, next_prefix) + yield from _traverse([*path, i, 3], item.nested_type, next_prefix) + + yield from _traverse([5], proto_file.enum_type) + yield from _traverse([4], proto_file.message_type) + + +def generate_code(request: CodeGeneratorRequest) -> CodeGeneratorResponse: + response = CodeGeneratorResponse() + + plugin_options = request.parameter.split(",") if request.parameter else [] + response.supported_features = CodeGeneratorResponseFeature.FEATURE_PROTO3_OPTIONAL + + request_data = PluginRequestCompiler(plugin_request_obj=request) + # Gather output packages + for proto_file in request.proto_file: + output_package_name = proto_file.package + if output_package_name not in request_data.output_packages: + # Create a new output if there is no output for this package + request_data.output_packages[output_package_name] = OutputTemplate( + parent_request=request_data, package_proto_obj=proto_file + ) + # Add this input file to the output corresponding to this package + request_data.output_packages[output_package_name].input_files.append(proto_file) + + if ( + proto_file.package == "google.protobuf" + and "INCLUDE_GOOGLE" not in plugin_options + ): + # If not INCLUDE_GOOGLE, + # skip outputting Google's well-known types + request_data.output_packages[output_package_name].output = False + + if "pydantic_dataclasses" in plugin_options: + request_data.output_packages[ + output_package_name + ].pydantic_dataclasses = True + + # Read Messages and Enums + # We need to read Messages before Services in so that we can + # get the references to input/output messages for each service + for output_package_name, output_package in request_data.output_packages.items(): + for proto_input_file in output_package.input_files: + for item, path in traverse(proto_input_file): + read_protobuf_type( + source_file=proto_input_file, + item=item, + path=path, + output_package=output_package, + ) + + # Read Services + for output_package_name, output_package in request_data.output_packages.items(): + for proto_input_file in output_package.input_files: + for index, service in enumerate(proto_input_file.service): + read_protobuf_service(service, index, output_package) + + # Generate output files + output_paths: Set[pathlib.Path] = set() + for output_package_name, output_package in request_data.output_packages.items(): + if not output_package.output: + continue + + # Add files to the response object + output_path = pathlib.Path(*output_package_name.split("."), "__init__.py") + output_paths.add(output_path) + + response.file.append( + CodeGeneratorResponseFile( + name=str(output_path), + # Render and then format the output file + content=outputfile_compiler(output_file=output_package), + ) + ) + + # Make each output directory a package with __init__ file + init_files = { + directory.joinpath("__init__.py") + for path in output_paths + for directory in path.parents + if not directory.joinpath("__init__.py").exists() + } - output_paths + + for init_file in init_files: + response.file.append(CodeGeneratorResponseFile(name=str(init_file))) + + for output_package_name in sorted(output_paths.union(init_files)): + print(f"Writing {output_package_name}", file=sys.stderr) + + return response + + +def _make_one_of_field_compiler( + output_package: OutputTemplate, + source_file: "FileDescriptorProto", + parent: MessageCompiler, + proto_obj: "FieldDescriptorProto", + path: List[int], +) -> FieldCompiler: + pydantic = output_package.pydantic_dataclasses + Cls = PydanticOneOfFieldCompiler if pydantic else OneOfFieldCompiler + return Cls( + source_file=source_file, + parent=parent, + proto_obj=proto_obj, + path=path, + ) + + +def read_protobuf_type( + item: DescriptorProto, + path: List[int], + source_file: "FileDescriptorProto", + output_package: OutputTemplate, +) -> None: + if isinstance(item, DescriptorProto): + if item.options.map_entry: + # Skip generated map entry messages since we just use dicts + return + # Process Message + message_data = MessageCompiler( + source_file=source_file, parent=output_package, proto_obj=item, path=path + ) + for index, field in enumerate(item.field): + if is_map(field, item): + MapEntryCompiler( + source_file=source_file, + parent=message_data, + proto_obj=field, + path=path + [2, index], + ) + elif is_oneof(field): + _make_one_of_field_compiler( + output_package, source_file, message_data, field, path + [2, index] + ) + else: + FieldCompiler( + source_file=source_file, + parent=message_data, + proto_obj=field, + path=path + [2, index], + ) + elif isinstance(item, EnumDescriptorProto): + # Enum + EnumDefinitionCompiler( + source_file=source_file, parent=output_package, proto_obj=item, path=path + ) + + +def read_protobuf_service( + service: ServiceDescriptorProto, index: int, output_package: OutputTemplate +) -> None: + service_data = ServiceCompiler( + parent=output_package, proto_obj=service, path=[6, index] + ) + for j, method in enumerate(service.method): + ServiceMethodCompiler( + parent=service_data, proto_obj=method, path=[6, index, 2, j] + ) diff --git a/src/aristaproto/plugin/plugin.bat b/src/aristaproto/plugin/plugin.bat new file mode 100644 index 0000000..2a4444d --- /dev/null +++ b/src/aristaproto/plugin/plugin.bat @@ -0,0 +1,2 @@ +@SET plugin_dir=%~dp0 +@python -m %plugin_dir% %*
\ No newline at end of file diff --git a/src/aristaproto/py.typed b/src/aristaproto/py.typed new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/aristaproto/py.typed diff --git a/src/aristaproto/templates/template.py.j2 b/src/aristaproto/templates/template.py.j2 new file mode 100644 index 0000000..f2f1425 --- /dev/null +++ b/src/aristaproto/templates/template.py.j2 @@ -0,0 +1,257 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: {{ ', '.join(output_file.input_filenames) }} +# plugin: python-aristaproto +# This file has been @generated +{% for i in output_file.python_module_imports|sort %} +import {{ i }} +{% endfor %} + +{% if output_file.pydantic_dataclasses %} +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from dataclasses import dataclass +else: + from pydantic.dataclasses import dataclass +{%- else -%} +from dataclasses import dataclass +{% endif %} + +{% if output_file.datetime_imports %} +from datetime import {% for i in output_file.datetime_imports|sort %}{{ i }}{% if not loop.last %}, {% endif %}{% endfor %} + +{% endif%} +{% if output_file.typing_imports %} +from typing import {% for i in output_file.typing_imports|sort %}{{ i }}{% if not loop.last %}, {% endif %}{% endfor %} + +{% endif %} + +{% if output_file.pydantic_imports %} +from pydantic import {% for i in output_file.pydantic_imports|sort %}{{ i }}{% if not loop.last %}, {% endif %}{% endfor %} + +{% endif %} + +import aristaproto +{% if output_file.services %} +from aristaproto.grpc.grpclib_server import ServiceBase +import grpclib +{% endif %} + +{% for i in output_file.imports|sort %} +{{ i }} +{% endfor %} + +{% if output_file.imports_type_checking_only %} +from typing import TYPE_CHECKING + +if TYPE_CHECKING: +{% for i in output_file.imports_type_checking_only|sort %} {{ i }} +{% endfor %} +{% endif %} + +{% if output_file.enums %}{% for enum in output_file.enums %} +class {{ enum.py_name }}(aristaproto.Enum): + {% if enum.comment %} +{{ enum.comment }} + + {% endif %} + {% for entry in enum.entries %} + {{ entry.name }} = {{ entry.value }} + {% if entry.comment %} +{{ entry.comment }} + + {% endif %} + {% endfor %} + + +{% endfor %} +{% endif %} +{% for message in output_file.messages %} +@dataclass(eq=False, repr=False) +class {{ message.py_name }}(aristaproto.Message): + {% if message.comment %} +{{ message.comment }} + + {% endif %} + {% for field in message.fields %} + {{ field.get_field_string() }} + {% if field.comment %} +{{ field.comment }} + + {% endif %} + {% endfor %} + {% if not message.fields %} + pass + {% endif %} + + {% if message.deprecated or message.has_deprecated_fields %} + def __post_init__(self) -> None: + {% if message.deprecated %} + warnings.warn("{{ message.py_name }} is deprecated", DeprecationWarning) + {% endif %} + super().__post_init__() + {% for field in message.deprecated_fields %} + if self.is_set("{{ field }}"): + warnings.warn("{{ message.py_name }}.{{ field }} is deprecated", DeprecationWarning) + {% endfor %} + {% endif %} + + {% if output_file.pydantic_dataclasses and message.has_oneof_fields %} + @root_validator() + def check_oneof(cls, values): + return cls._validate_field_groups(values) + {% endif %} + +{% endfor %} +{% for service in output_file.services %} +class {{ service.py_name }}Stub(aristaproto.ServiceStub): + {% if service.comment %} +{{ service.comment }} + + {% elif not service.methods %} + pass + {% endif %} + {% for method in service.methods %} + async def {{ method.py_name }}(self + {%- if not method.client_streaming -%} + {%- if method.py_input_message -%}, {{ method.py_input_message_param }}: "{{ method.py_input_message_type }}"{%- endif -%} + {%- else -%} + {# Client streaming: need a request iterator instead #} + , {{ method.py_input_message_param }}_iterator: Union[AsyncIterable["{{ method.py_input_message_type }}"], Iterable["{{ method.py_input_message_type }}"]] + {%- endif -%} + , + * + , timeout: Optional[float] = None + , deadline: Optional["Deadline"] = None + , metadata: Optional["MetadataLike"] = None + ) -> {% if method.server_streaming %}AsyncIterator["{{ method.py_output_message_type }}"]{% else %}"{{ method.py_output_message_type }}"{% endif %}: + {% if method.comment %} +{{ method.comment }} + + {% endif %} + {% if method.server_streaming %} + {% if method.client_streaming %} + async for response in self._stream_stream( + "{{ method.route }}", + {{ method.py_input_message_param }}_iterator, + {{ method.py_input_message_type }}, + {{ method.py_output_message_type.strip('"') }}, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ): + yield response + {% else %}{# i.e. not client streaming #} + async for response in self._unary_stream( + "{{ method.route }}", + {{ method.py_input_message_param }}, + {{ method.py_output_message_type.strip('"') }}, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ): + yield response + + {% endif %}{# if client streaming #} + {% else %}{# i.e. not server streaming #} + {% if method.client_streaming %} + return await self._stream_unary( + "{{ method.route }}", + {{ method.py_input_message_param }}_iterator, + {{ method.py_input_message_type }}, + {{ method.py_output_message_type.strip('"') }}, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + {% else %}{# i.e. not client streaming #} + return await self._unary_unary( + "{{ method.route }}", + {{ method.py_input_message_param }}, + {{ method.py_output_message_type.strip('"') }}, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + {% endif %}{# client streaming #} + {% endif %} + + {% endfor %} +{% endfor %} + +{% for service in output_file.services %} +class {{ service.py_name }}Base(ServiceBase): + {% if service.comment %} +{{ service.comment }} + + {% endif %} + + {% for method in service.methods %} + async def {{ method.py_name }}(self + {%- if not method.client_streaming -%} + {%- if method.py_input_message -%}, {{ method.py_input_message_param }}: "{{ method.py_input_message_type }}"{%- endif -%} + {%- else -%} + {# Client streaming: need a request iterator instead #} + , {{ method.py_input_message_param }}_iterator: AsyncIterator["{{ method.py_input_message_type }}"] + {%- endif -%} + ) -> {% if method.server_streaming %}AsyncIterator["{{ method.py_output_message_type }}"]{% else %}"{{ method.py_output_message_type }}"{% endif %}: + {% if method.comment %} +{{ method.comment }} + + {% endif %} + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + {% if method.server_streaming %} + {# Commented out to avoid unreachable code. #} + {# yield {{ method.py_output_message_type }}() #} + {% endif %} + + {% endfor %} + + {% for method in service.methods %} + async def __rpc_{{ method.py_name }}(self, stream: "grpclib.server.Stream[{{ method.py_input_message_type }}, {{ method.py_output_message_type }}]") -> None: + {% if not method.client_streaming %} + request = await stream.recv_message() + {% else %} + request = stream.__aiter__() + {% endif %} + {% if not method.server_streaming %} + response = await self.{{ method.py_name }}(request) + await stream.send_message(response) + {% else %} + await self._call_rpc_handler_server_stream( + self.{{ method.py_name }}, + stream, + request, + ) + {% endif %} + + {% endfor %} + + def __mapping__(self) -> Dict[str, grpclib.const.Handler]: + return { + {% for method in service.methods %} + "{{ method.route }}": grpclib.const.Handler( + self.__rpc_{{ method.py_name }}, + {% if not method.client_streaming and not method.server_streaming %} + grpclib.const.Cardinality.UNARY_UNARY, + {% elif not method.client_streaming and method.server_streaming %} + grpclib.const.Cardinality.UNARY_STREAM, + {% elif method.client_streaming and not method.server_streaming %} + grpclib.const.Cardinality.STREAM_UNARY, + {% else %} + grpclib.const.Cardinality.STREAM_STREAM, + {% endif %} + {{ method.py_input_message_type }}, + {{ method.py_output_message_type }}, + ), + {% endfor %} + } + +{% endfor %} + +{% if output_file.pydantic_dataclasses %} +{% for message in output_file.messages %} +{% if message.has_message_field %} +{{ message.py_name }}.__pydantic_model__.update_forward_refs() # type: ignore +{% endif %} +{% endfor %} +{% endif %} diff --git a/src/aristaproto/utils.py b/src/aristaproto/utils.py new file mode 100644 index 0000000..b977fc7 --- /dev/null +++ b/src/aristaproto/utils.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from typing import ( + Any, + Callable, + Generic, + Optional, + Type, + TypeVar, +) + +from typing_extensions import ( + Concatenate, + ParamSpec, + Self, +) + + +SelfT = TypeVar("SelfT") +P = ParamSpec("P") +HybridT = TypeVar("HybridT", covariant=True) + + +class hybridmethod(Generic[SelfT, P, HybridT]): + def __init__( + self, + func: Callable[ + Concatenate[type[SelfT], P], HybridT + ], # Must be the classmethod version + ): + self.cls_func = func + self.__doc__ = func.__doc__ + + def instancemethod(self, func: Callable[Concatenate[SelfT, P], HybridT]) -> Self: + self.instance_func = func + return self + + def __get__( + self, instance: Optional[SelfT], owner: Type[SelfT] + ) -> Callable[P, HybridT]: + if instance is None or self.instance_func is None: + # either bound to the class, or no instance method available + return self.cls_func.__get__(owner, None) + return self.instance_func.__get__(instance, owner) + + +T_co = TypeVar("T_co") +TT_co = TypeVar("TT_co", bound="type[Any]") + + +class classproperty(Generic[TT_co, T_co]): + def __init__(self, func: Callable[[TT_co], T_co]): + self.__func__ = func + + def __get__(self, instance: Any, type: TT_co) -> T_co: + return self.__func__(type) |