From 97c7fa7289072625a7116be3fe128ed2f9488855 Mon Sep 17 00:00:00 2001 From: Dmytro Yaroshenko <73843436+o-murphy@users.noreply.github.com> Date: Mon, 6 Nov 2023 18:48:53 +0200 Subject: [PATCH] v0.0.6, protovalidate added (#3) --- MANIFEST.in | 2 +- a7p/__init__.py | 2 +- a7p/buf/__init__.py | 0 a7p/buf/validate/__init__.py | 0 {buf => a7p/buf}/validate/expression_pb2.py | 0 a7p/buf/validate/priv/__init__.py | 0 {buf => a7p/buf}/validate/priv/private_pb2.py | 0 {buf => a7p/buf}/validate/validate_pb2.py | 4 +- a7p/profedit_pb2.py | 90 +- a7p/profedit_pb2.py.bak | 38 + ...{profedit_pb2.pyi => profedit_pb2.pyi.bak} | 0 .../profedit_validate.proto | 0 a7p/protovalidate/__init__.py | 26 + a7p/protovalidate/internal/__init__.py | 13 + a7p/protovalidate/internal/constraints.py | 800 ++++++++++++++++++ a7p/protovalidate/internal/extra_func.py | 158 ++++ a7p/protovalidate/internal/string_format.py | 173 ++++ a7p/protovalidate/validator.py | 108 +++ a7p/tests.py | 2 +- profedit_validate_pb2.py | 92 -- test.py | 29 - 21 files changed, 1393 insertions(+), 144 deletions(-) create mode 100644 a7p/buf/__init__.py create mode 100644 a7p/buf/validate/__init__.py rename {buf => a7p/buf}/validate/expression_pb2.py (100%) create mode 100644 a7p/buf/validate/priv/__init__.py rename {buf => a7p/buf}/validate/priv/private_pb2.py (100%) rename {buf => a7p/buf}/validate/validate_pb2.py (99%) create mode 100644 a7p/profedit_pb2.py.bak rename a7p/{profedit_pb2.pyi => profedit_pb2.pyi.bak} (100%) rename profedit_validate.proto => a7p/profedit_validate.proto (100%) create mode 100644 a7p/protovalidate/__init__.py create mode 100644 a7p/protovalidate/internal/__init__.py create mode 100644 a7p/protovalidate/internal/constraints.py create mode 100644 a7p/protovalidate/internal/extra_func.py create mode 100644 a7p/protovalidate/internal/string_format.py create mode 100644 a7p/protovalidate/validator.py delete mode 100644 profedit_validate_pb2.py delete mode 100644 test.py diff --git a/MANIFEST.in b/MANIFEST.in index 9270459..8ca2d15 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include requirements.txt -include a7p/profedit.proto +include a7p/*.proto include a7p/*.pyi include README.MD include LICENSE diff --git a/a7p/__init__.py b/a7p/__init__.py index 6c52465..1408063 100644 --- a/a7p/__init__.py +++ b/a7p/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.0.6a1' +__version__ = '0.0.6' __author__ = "o-murphy" __credits__ = ["Dmytro Yaroshenko"] __copyright__ = ("",) diff --git a/a7p/buf/__init__.py b/a7p/buf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/a7p/buf/validate/__init__.py b/a7p/buf/validate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/buf/validate/expression_pb2.py b/a7p/buf/validate/expression_pb2.py similarity index 100% rename from buf/validate/expression_pb2.py rename to a7p/buf/validate/expression_pb2.py diff --git a/a7p/buf/validate/priv/__init__.py b/a7p/buf/validate/priv/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/buf/validate/priv/private_pb2.py b/a7p/buf/validate/priv/private_pb2.py similarity index 100% rename from buf/validate/priv/private_pb2.py rename to a7p/buf/validate/priv/private_pb2.py diff --git a/buf/validate/validate_pb2.py b/a7p/buf/validate/validate_pb2.py similarity index 99% rename from buf/validate/validate_pb2.py rename to a7p/buf/validate/validate_pb2.py index ee5c55e..3837d3b 100644 --- a/buf/validate/validate_pb2.py +++ b/a7p/buf/validate/validate_pb2.py @@ -11,8 +11,8 @@ _sym_db = _symbol_database.Default() -from buf.validate import expression_pb2 as buf_dot_validate_dot_expression__pb2 -from buf.validate.priv import private_pb2 as buf_dot_validate_dot_priv_dot_private__pb2 +from a7p.buf.validate import expression_pb2 as buf_dot_validate_dot_expression__pb2 +from a7p.buf.validate.priv import private_pb2 as buf_dot_validate_dot_priv_dot_private__pb2 from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 diff --git a/a7p/profedit_pb2.py b/a7p/profedit_pb2.py index d40dea3..265256b 100644 --- a/a7p/profedit_pb2.py +++ b/a7p/profedit_pb2.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! -# source: profedit.proto +# source: a7p/profedit_validate.proto """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,28 +11,82 @@ _sym_db = _symbol_database.Default() +from a7p.buf.validate import validate_pb2 as buf_dot_validate_dot_validate__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eprofedit.proto\x12\x08profedit\"-\n\x07Payload\x12\"\n\x07profile\x18\x01 \x01(\x0b\x32\x11.profedit.Profile\"$\n\x07\x43oefRow\x12\r\n\x05\x62\x63_cd\x18\x01 \x01(\x05\x12\n\n\x02mv\x18\x02 \x01(\x05\"s\n\x05SwPos\x12\r\n\x05\x63_idx\x18\x01 \x01(\x05\x12\x13\n\x0breticle_idx\x18\x02 \x01(\x05\x12\x0c\n\x04zoom\x18\x03 \x01(\x05\x12\x10\n\x08\x64istance\x18\x04 \x01(\x05\x12&\n\rdistance_from\x18\x05 \x01(\x0e\x32\x0f.profedit.DType\"\xcd\x05\n\x07Profile\x12\x14\n\x0cprofile_name\x18\x01 \x01(\t\x12\x16\n\x0e\x63\x61rtridge_name\x18\x02 \x01(\t\x12\x13\n\x0b\x62ullet_name\x18\x03 \x01(\t\x12\x16\n\x0eshort_name_top\x18\x04 \x01(\t\x12\x16\n\x0eshort_name_bot\x18\x05 \x01(\t\x12\x11\n\tuser_note\x18\x06 \x01(\t\x12\x0e\n\x06zero_x\x18\x07 \x01(\x05\x12\x0e\n\x06zero_y\x18\x08 \x01(\x05\x12\x11\n\tsc_height\x18\t \x01(\x05\x12\x0f\n\x07r_twist\x18\n \x01(\x05\x12\x19\n\x11\x63_muzzle_velocity\x18\x0b \x01(\x05\x12\x1a\n\x12\x63_zero_temperature\x18\x0c \x01(\x05\x12\x11\n\tc_t_coeff\x18\r \x01(\x05\x12\x1b\n\x13\x63_zero_distance_idx\x18\x0e \x01(\x05\x12\x1e\n\x16\x63_zero_air_temperature\x18\x0f \x01(\x05\x12\x1b\n\x13\x63_zero_air_pressure\x18\x10 \x01(\x05\x12\x1b\n\x13\x63_zero_air_humidity\x18\x11 \x01(\x05\x12\x16\n\x0e\x63_zero_w_pitch\x18\x12 \x01(\x05\x12\x1c\n\x14\x63_zero_p_temperature\x18\x13 \x01(\x05\x12\x12\n\nb_diameter\x18\x14 \x01(\x05\x12\x10\n\x08\x62_weight\x18\x15 \x01(\x05\x12\x10\n\x08\x62_length\x18\x16 \x01(\x05\x12%\n\ttwist_dir\x18\x17 \x01(\x0e\x32\x12.profedit.TwistDir\x12 \n\x07\x62\x63_type\x18\x18 \x01(\x0e\x32\x0f.profedit.GType\x12!\n\x08switches\x18\x19 \x03(\x0b\x32\x0f.profedit.SwPos\x12\x11\n\tdistances\x18\x1a \x03(\x05\x12$\n\tcoef_rows\x18\x1b \x03(\x0b\x32\x11.profedit.CoefRow\x12\x0f\n\x07\x63\x61liber\x18\x1c \x01(\t\x12\x13\n\x0b\x64\x65vice_uuid\x18\x1d \x01(\t*\x1d\n\x05\x44Type\x12\t\n\x05VALUE\x10\x00\x12\t\n\x05INDEX\x10\x01*#\n\x05GType\x12\x06\n\x02G1\x10\x00\x12\x06\n\x02G7\x10\x01\x12\n\n\x06\x43USTOM\x10\x02*\x1f\n\x08TwistDir\x12\t\n\x05RIGHT\x10\x00\x12\x08\n\x04LEFT\x10\x01\x42\x32Z0github.com/jaremko/a7p_transfer_example/profeditb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x61\x37p/profedit_validate.proto\x12\x08profedit\x1a\x1b\x62uf/validate/validate.proto\"6\n\x07Payload\x12+\n\x07profile\x18\x01 \x01(\x0b\x32\x11.profedit.ProfileR\x07profile\".\n\x07\x43oefRow\x12\x13\n\x05\x62\x63_cd\x18\x01 \x01(\x05R\x04\x62\x63\x43\x64\x12\x0e\n\x02mv\x18\x02 \x01(\x05R\x02mv\"\xa3\x01\n\x05SwPos\x12\x13\n\x05\x63_idx\x18\x01 \x01(\x05R\x04\x63Idx\x12\x1f\n\x0breticle_idx\x18\x02 \x01(\x05R\nreticleIdx\x12\x12\n\x04zoom\x18\x03 \x01(\x05R\x04zoom\x12\x1a\n\x08\x64istance\x18\x04 \x01(\x05R\x08\x64istance\x12\x34\n\rdistance_from\x18\x05 \x01(\x0e\x32\x0f.profedit.DTypeR\x0c\x64istanceFrom\"\x97\x0b\n\x07Profile\x12*\n\x0cprofile_name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x18\x32R\x0bprofileName\x12.\n\x0e\x63\x61rtridge_name\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x18\x32R\rcartridgeName\x12(\n\x0b\x62ullet_name\x18\x03 \x01(\tB\x07\xbaH\x04r\x02\x18\x32R\nbulletName\x12-\n\x0eshort_name_top\x18\x04 \x01(\tB\x07\xbaH\x04r\x02\x18\x08R\x0cshortNameTop\x12-\n\x0eshort_name_bot\x18\x05 \x01(\tB\x07\xbaH\x04r\x02\x18\x08R\x0cshortNameBot\x12%\n\tuser_note\x18\x06 \x01(\tB\x08\xbaH\x05r\x03\x18\xfa\x01R\x08userNote\x12+\n\x06zero_x\x18\x07 \x01(\x05\x42\x14\xbaH\x11\x1a\x0f\x18\xc0\xcf$(\xc0\xb0\xdb\xff\xff\xff\xff\xff\xff\x01R\x05zeroX\x12+\n\x06zero_y\x18\x08 \x01(\x05\x42\x14\xbaH\x11\x1a\x0f\x18\xc0\xcf$(\xc0\xb0\xdb\xff\xff\xff\xff\xff\xff\x01R\x05zeroY\x12\x30\n\tsc_height\x18\t \x01(\x05\x42\x13\xbaH\x10\x1a\x0e\x18\x88\'(\xf8\xd8\xff\xff\xff\xff\xff\xff\xff\x01R\x08scHeight\x12#\n\x07r_twist\x18\n \x01(\x05\x42\n\xbaH\x07\x1a\x05\x18\x90N(\x00R\x06rTwist\x12\x37\n\x11\x63_muzzle_velocity\x18\x0b \x01(\x05\x42\x0b\xbaH\x08\x1a\x06\x18\xb0\xea\x01(dR\x0f\x63MuzzleVelocity\x12@\n\x12\x63_zero_temperature\x18\x0c \x01(\x05\x42\x12\xbaH\x0f\x1a\r\x18\x64(\x9c\xff\xff\xff\xff\xff\xff\xff\xff\x01R\x10\x63ZeroTemperature\x12&\n\tc_t_coeff\x18\r \x01(\x05\x42\n\xbaH\x07\x1a\x05\x18\xb8\x17(\x02R\x07\x63TCoeff\x12\x39\n\x13\x63_zero_distance_idx\x18\x0e \x01(\x05\x42\n\xbaH\x07\x1a\x05\x10\xc8\x01(\x00R\x10\x63ZeroDistanceIdx\x12G\n\x16\x63_zero_air_temperature\x18\x0f \x01(\x05\x42\x12\xbaH\x0f\x1a\r\x18\x64(\x9c\xff\xff\xff\xff\xff\xff\xff\xff\x01R\x13\x63ZeroAirTemperature\x12-\n\x13\x63_zero_air_pressure\x18\x10 \x01(\x05R\x10\x63ZeroAirPressure\x12\x38\n\x13\x63_zero_air_humidity\x18\x11 \x01(\x05\x42\t\xbaH\x06\x1a\x04\x18\x64(\x00R\x10\x63ZeroAirHumidity\x12\x37\n\x0e\x63_zero_w_pitch\x18\x12 \x01(\x05\x42\x12\xbaH\x0f\x1a\r\x18Z(\xa6\xff\xff\xff\xff\xff\xff\xff\xff\x01R\x0b\x63ZeroWPitch\x12\x43\n\x14\x63_zero_p_temperature\x18\x13 \x01(\x05\x42\x12\xbaH\x0f\x1a\r\x18\x64(\x9c\xff\xff\xff\xff\xff\xff\xff\xff\x01R\x11\x63ZeroPTemperature\x12*\n\nb_diameter\x18\x14 \x01(\x05\x42\x0b\xbaH\x08\x1a\x06\x18\xff\xff\x03(\x01R\tbDiameter\x12&\n\x08\x62_weight\x18\x15 \x01(\x05\x42\x0b\xbaH\x08\x1a\x06\x18\xff\xff\x03(\nR\x07\x62Weight\x12%\n\x08\x62_length\x18\x16 \x01(\x05\x42\n\xbaH\x07\x1a\x05\x18\x90N(\x01R\x07\x62Length\x12/\n\ttwist_dir\x18\x17 \x01(\x0e\x32\x12.profedit.TwistDirR\x08twistDir\x12(\n\x07\x62\x63_type\x18\x18 \x01(\x0e\x32\x0f.profedit.GTypeR\x06\x62\x63Type\x12\x37\n\x08switches\x18\x19 \x03(\x0b\x32\x0f.profedit.SwPosB\n\xbaH\x07\x92\x01\x04\x08\x01\x10\x04R\x08switches\x12)\n\tdistances\x18\x1a \x03(\x05\x42\x0b\xbaH\x08\x92\x01\x05\x08\x04\x10\xc8\x01R\tdistances\x12;\n\tcoef_rows\x18\x1b \x03(\x0b\x32\x11.profedit.CoefRowB\x0b\xbaH\x08\x92\x01\x05\x08\x01\x10\xc8\x01R\x08\x63oefRows\x12!\n\x07\x63\x61liber\x18\x1c \x01(\tB\x07\xbaH\x04r\x02\x18\x32R\x07\x63\x61liber\x12(\n\x0b\x64\x65vice_uuid\x18\x1d \x01(\tB\x07\xbaH\x04r\x02\x18\x32R\ndeviceUuid*\x1d\n\x05\x44Type\x12\t\n\x05VALUE\x10\x00\x12\t\n\x05INDEX\x10\x01*#\n\x05GType\x12\x06\n\x02G1\x10\x00\x12\x06\n\x02G7\x10\x01\x12\n\n\x06\x43USTOM\x10\x02*\x1f\n\x08TwistDir\x12\t\n\x05RIGHT\x10\x00\x12\x08\n\x04LEFT\x10\x01\x42\x32Z0github.com/jaremko/a7p_transfer_example/profeditb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'profedit_pb2', _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'a7p.profedit_validate_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None DESCRIPTOR._serialized_options = b'Z0github.com/jaremko/a7p_transfer_example/profedit' - _globals['_DTYPE']._serialized_start=950 - _globals['_DTYPE']._serialized_end=979 - _globals['_GTYPE']._serialized_start=981 - _globals['_GTYPE']._serialized_end=1016 - _globals['_TWISTDIR']._serialized_start=1018 - _globals['_TWISTDIR']._serialized_end=1049 - _globals['_PAYLOAD']._serialized_start=28 - _globals['_PAYLOAD']._serialized_end=73 - _globals['_COEFROW']._serialized_start=75 - _globals['_COEFROW']._serialized_end=111 - _globals['_SWPOS']._serialized_start=113 - _globals['_SWPOS']._serialized_end=228 - _globals['_PROFILE']._serialized_start=231 - _globals['_PROFILE']._serialized_end=948 -# @@protoc_insertion_point(module_scope) \ No newline at end of file + _PROFILE.fields_by_name['profile_name']._options = None + _PROFILE.fields_by_name['profile_name']._serialized_options = b'\272H\004r\002\0302' + _PROFILE.fields_by_name['cartridge_name']._options = None + _PROFILE.fields_by_name['cartridge_name']._serialized_options = b'\272H\004r\002\0302' + _PROFILE.fields_by_name['bullet_name']._options = None + _PROFILE.fields_by_name['bullet_name']._serialized_options = b'\272H\004r\002\0302' + _PROFILE.fields_by_name['short_name_top']._options = None + _PROFILE.fields_by_name['short_name_top']._serialized_options = b'\272H\004r\002\030\010' + _PROFILE.fields_by_name['short_name_bot']._options = None + _PROFILE.fields_by_name['short_name_bot']._serialized_options = b'\272H\004r\002\030\010' + _PROFILE.fields_by_name['user_note']._options = None + _PROFILE.fields_by_name['user_note']._serialized_options = b'\272H\005r\003\030\372\001' + _PROFILE.fields_by_name['zero_x']._options = None + _PROFILE.fields_by_name['zero_x']._serialized_options = b'\272H\021\032\017\030\300\317$(\300\260\333\377\377\377\377\377\377\001' + _PROFILE.fields_by_name['zero_y']._options = None + _PROFILE.fields_by_name['zero_y']._serialized_options = b'\272H\021\032\017\030\300\317$(\300\260\333\377\377\377\377\377\377\001' + _PROFILE.fields_by_name['sc_height']._options = None + _PROFILE.fields_by_name['sc_height']._serialized_options = b'\272H\020\032\016\030\210\'(\370\330\377\377\377\377\377\377\377\001' + _PROFILE.fields_by_name['r_twist']._options = None + _PROFILE.fields_by_name['r_twist']._serialized_options = b'\272H\007\032\005\030\220N(\000' + _PROFILE.fields_by_name['c_muzzle_velocity']._options = None + _PROFILE.fields_by_name['c_muzzle_velocity']._serialized_options = b'\272H\010\032\006\030\260\352\001(d' + _PROFILE.fields_by_name['c_zero_temperature']._options = None + _PROFILE.fields_by_name['c_zero_temperature']._serialized_options = b'\272H\017\032\r\030d(\234\377\377\377\377\377\377\377\377\001' + _PROFILE.fields_by_name['c_t_coeff']._options = None + _PROFILE.fields_by_name['c_t_coeff']._serialized_options = b'\272H\007\032\005\030\270\027(\002' + _PROFILE.fields_by_name['c_zero_distance_idx']._options = None + _PROFILE.fields_by_name['c_zero_distance_idx']._serialized_options = b'\272H\007\032\005\020\310\001(\000' + _PROFILE.fields_by_name['c_zero_air_temperature']._options = None + _PROFILE.fields_by_name['c_zero_air_temperature']._serialized_options = b'\272H\017\032\r\030d(\234\377\377\377\377\377\377\377\377\001' + _PROFILE.fields_by_name['c_zero_air_humidity']._options = None + _PROFILE.fields_by_name['c_zero_air_humidity']._serialized_options = b'\272H\006\032\004\030d(\000' + _PROFILE.fields_by_name['c_zero_w_pitch']._options = None + _PROFILE.fields_by_name['c_zero_w_pitch']._serialized_options = b'\272H\017\032\r\030Z(\246\377\377\377\377\377\377\377\377\001' + _PROFILE.fields_by_name['c_zero_p_temperature']._options = None + _PROFILE.fields_by_name['c_zero_p_temperature']._serialized_options = b'\272H\017\032\r\030d(\234\377\377\377\377\377\377\377\377\001' + _PROFILE.fields_by_name['b_diameter']._options = None + _PROFILE.fields_by_name['b_diameter']._serialized_options = b'\272H\010\032\006\030\377\377\003(\001' + _PROFILE.fields_by_name['b_weight']._options = None + _PROFILE.fields_by_name['b_weight']._serialized_options = b'\272H\010\032\006\030\377\377\003(\n' + _PROFILE.fields_by_name['b_length']._options = None + _PROFILE.fields_by_name['b_length']._serialized_options = b'\272H\007\032\005\030\220N(\001' + _PROFILE.fields_by_name['switches']._options = None + _PROFILE.fields_by_name['switches']._serialized_options = b'\272H\007\222\001\004\010\001\020\004' + _PROFILE.fields_by_name['distances']._options = None + _PROFILE.fields_by_name['distances']._serialized_options = b'\272H\010\222\001\005\010\004\020\310\001' + _PROFILE.fields_by_name['coef_rows']._options = None + _PROFILE.fields_by_name['coef_rows']._serialized_options = b'\272H\010\222\001\005\010\001\020\310\001' + _PROFILE.fields_by_name['caliber']._options = None + _PROFILE.fields_by_name['caliber']._serialized_options = b'\272H\004r\002\0302' + _PROFILE.fields_by_name['device_uuid']._options = None + _PROFILE.fields_by_name['device_uuid']._serialized_options = b'\272H\004r\002\0302' + _globals['_DTYPE']._serialized_start=1774 + _globals['_DTYPE']._serialized_end=1803 + _globals['_GTYPE']._serialized_start=1805 + _globals['_GTYPE']._serialized_end=1840 + _globals['_TWISTDIR']._serialized_start=1842 + _globals['_TWISTDIR']._serialized_end=1873 + _globals['_PAYLOAD']._serialized_start=70 + _globals['_PAYLOAD']._serialized_end=124 + _globals['_COEFROW']._serialized_start=126 + _globals['_COEFROW']._serialized_end=172 + _globals['_SWPOS']._serialized_start=175 + _globals['_SWPOS']._serialized_end=338 + _globals['_PROFILE']._serialized_start=341 + _globals['_PROFILE']._serialized_end=1772 +# @@protoc_insertion_point(module_scope) diff --git a/a7p/profedit_pb2.py.bak b/a7p/profedit_pb2.py.bak new file mode 100644 index 0000000..d40dea3 --- /dev/null +++ b/a7p/profedit_pb2.py.bak @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: profedit.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eprofedit.proto\x12\x08profedit\"-\n\x07Payload\x12\"\n\x07profile\x18\x01 \x01(\x0b\x32\x11.profedit.Profile\"$\n\x07\x43oefRow\x12\r\n\x05\x62\x63_cd\x18\x01 \x01(\x05\x12\n\n\x02mv\x18\x02 \x01(\x05\"s\n\x05SwPos\x12\r\n\x05\x63_idx\x18\x01 \x01(\x05\x12\x13\n\x0breticle_idx\x18\x02 \x01(\x05\x12\x0c\n\x04zoom\x18\x03 \x01(\x05\x12\x10\n\x08\x64istance\x18\x04 \x01(\x05\x12&\n\rdistance_from\x18\x05 \x01(\x0e\x32\x0f.profedit.DType\"\xcd\x05\n\x07Profile\x12\x14\n\x0cprofile_name\x18\x01 \x01(\t\x12\x16\n\x0e\x63\x61rtridge_name\x18\x02 \x01(\t\x12\x13\n\x0b\x62ullet_name\x18\x03 \x01(\t\x12\x16\n\x0eshort_name_top\x18\x04 \x01(\t\x12\x16\n\x0eshort_name_bot\x18\x05 \x01(\t\x12\x11\n\tuser_note\x18\x06 \x01(\t\x12\x0e\n\x06zero_x\x18\x07 \x01(\x05\x12\x0e\n\x06zero_y\x18\x08 \x01(\x05\x12\x11\n\tsc_height\x18\t \x01(\x05\x12\x0f\n\x07r_twist\x18\n \x01(\x05\x12\x19\n\x11\x63_muzzle_velocity\x18\x0b \x01(\x05\x12\x1a\n\x12\x63_zero_temperature\x18\x0c \x01(\x05\x12\x11\n\tc_t_coeff\x18\r \x01(\x05\x12\x1b\n\x13\x63_zero_distance_idx\x18\x0e \x01(\x05\x12\x1e\n\x16\x63_zero_air_temperature\x18\x0f \x01(\x05\x12\x1b\n\x13\x63_zero_air_pressure\x18\x10 \x01(\x05\x12\x1b\n\x13\x63_zero_air_humidity\x18\x11 \x01(\x05\x12\x16\n\x0e\x63_zero_w_pitch\x18\x12 \x01(\x05\x12\x1c\n\x14\x63_zero_p_temperature\x18\x13 \x01(\x05\x12\x12\n\nb_diameter\x18\x14 \x01(\x05\x12\x10\n\x08\x62_weight\x18\x15 \x01(\x05\x12\x10\n\x08\x62_length\x18\x16 \x01(\x05\x12%\n\ttwist_dir\x18\x17 \x01(\x0e\x32\x12.profedit.TwistDir\x12 \n\x07\x62\x63_type\x18\x18 \x01(\x0e\x32\x0f.profedit.GType\x12!\n\x08switches\x18\x19 \x03(\x0b\x32\x0f.profedit.SwPos\x12\x11\n\tdistances\x18\x1a \x03(\x05\x12$\n\tcoef_rows\x18\x1b \x03(\x0b\x32\x11.profedit.CoefRow\x12\x0f\n\x07\x63\x61liber\x18\x1c \x01(\t\x12\x13\n\x0b\x64\x65vice_uuid\x18\x1d \x01(\t*\x1d\n\x05\x44Type\x12\t\n\x05VALUE\x10\x00\x12\t\n\x05INDEX\x10\x01*#\n\x05GType\x12\x06\n\x02G1\x10\x00\x12\x06\n\x02G7\x10\x01\x12\n\n\x06\x43USTOM\x10\x02*\x1f\n\x08TwistDir\x12\t\n\x05RIGHT\x10\x00\x12\x08\n\x04LEFT\x10\x01\x42\x32Z0github.com/jaremko/a7p_transfer_example/profeditb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'profedit_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'Z0github.com/jaremko/a7p_transfer_example/profedit' + _globals['_DTYPE']._serialized_start=950 + _globals['_DTYPE']._serialized_end=979 + _globals['_GTYPE']._serialized_start=981 + _globals['_GTYPE']._serialized_end=1016 + _globals['_TWISTDIR']._serialized_start=1018 + _globals['_TWISTDIR']._serialized_end=1049 + _globals['_PAYLOAD']._serialized_start=28 + _globals['_PAYLOAD']._serialized_end=73 + _globals['_COEFROW']._serialized_start=75 + _globals['_COEFROW']._serialized_end=111 + _globals['_SWPOS']._serialized_start=113 + _globals['_SWPOS']._serialized_end=228 + _globals['_PROFILE']._serialized_start=231 + _globals['_PROFILE']._serialized_end=948 +# @@protoc_insertion_point(module_scope) \ No newline at end of file diff --git a/a7p/profedit_pb2.pyi b/a7p/profedit_pb2.pyi.bak similarity index 100% rename from a7p/profedit_pb2.pyi rename to a7p/profedit_pb2.pyi.bak diff --git a/profedit_validate.proto b/a7p/profedit_validate.proto similarity index 100% rename from profedit_validate.proto rename to a7p/profedit_validate.proto diff --git a/a7p/protovalidate/__init__.py b/a7p/protovalidate/__init__.py new file mode 100644 index 0000000..7c8ea4c --- /dev/null +++ b/a7p/protovalidate/__init__.py @@ -0,0 +1,26 @@ +# Copyright 2023 Buf Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from a7p.protovalidate import validator + +Validator = validator.Validator +CompilationError = validator.CompilationError +ValidationError = validator.ValidationError +Violations = validator.Violations + +_validator = Validator() +validate = _validator.validate +collect_violations = _validator.collect_violations + +__all__ = ["Validator", "CompilationError", "ValidationError", "Violations", "validate", "collect_violations"] diff --git a/a7p/protovalidate/internal/__init__.py b/a7p/protovalidate/internal/__init__.py new file mode 100644 index 0000000..29a76bb --- /dev/null +++ b/a7p/protovalidate/internal/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Buf Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/a7p/protovalidate/internal/constraints.py b/a7p/protovalidate/internal/constraints.py new file mode 100644 index 0000000..774cb8d --- /dev/null +++ b/a7p/protovalidate/internal/constraints.py @@ -0,0 +1,800 @@ +# Copyright 2023 Buf Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +import typing + +import celpy # type: ignore +from celpy import celtypes # type: ignore +from google.protobuf import any_pb2, descriptor, message + +from a7p.buf.validate import expression_pb2, validate_pb2 # type: ignore +from a7p.buf.validate.priv import private_pb2 # type: ignore +from a7p.protovalidate.internal import string_format + + +class CompilationError(Exception): + pass + + +def make_key_path(field_name: str, key: celtypes.Value) -> str: + return f"{field_name}[{string_format.format_value(key)}]" + + +def make_duration(msg: message.Message) -> celtypes.DurationType: + return celtypes.DurationType( + seconds=msg.seconds, # type: ignore + nanos=msg.nanos, # type: ignore + ) + + +def make_timestamp(msg: message.Message) -> celtypes.TimestampType: + return make_duration(msg) + celtypes.TimestampType(1970, 1, 1) + + +def unwrap(msg: message.Message) -> celtypes.Value: + return _field_to_cel(msg, msg.DESCRIPTOR.fields_by_name["value"]) + + +_MSG_TYPE_URL_TO_CTOR = { + "google.protobuf.Duration": make_duration, + "google.protobuf.Timestamp": make_timestamp, + "google.protobuf.StringValue": unwrap, + "google.protobuf.BytesValue": unwrap, + "google.protobuf.Int32Value": unwrap, + "google.protobuf.Int64Value": unwrap, + "google.protobuf.UInt32Value": unwrap, + "google.protobuf.UInt64Value": unwrap, + "google.protobuf.FloatValue": unwrap, + "google.protobuf.DoubleValue": unwrap, + "google.protobuf.BoolValue": unwrap, +} + + +def _msg_to_cel(msg: message.Message) -> dict[str, celtypes.Value]: + ctor = _MSG_TYPE_URL_TO_CTOR.get(msg.DESCRIPTOR.full_name) + if ctor is not None: + return ctor(msg) + result = celtypes.MapType() + field: descriptor.FieldDescriptor + for field in msg.DESCRIPTOR.fields: + if field.containing_oneof is not None and not msg.HasField(field.name): + continue + result[field.name] = _field_to_cel(msg, field) + return result + + +_TYPE_TO_CTOR = { + descriptor.FieldDescriptor.TYPE_MESSAGE: _msg_to_cel, + descriptor.FieldDescriptor.TYPE_ENUM: celtypes.IntType, + descriptor.FieldDescriptor.TYPE_BOOL: celtypes.BoolType, + descriptor.FieldDescriptor.TYPE_BYTES: celtypes.BytesType, + descriptor.FieldDescriptor.TYPE_STRING: celtypes.StringType, + descriptor.FieldDescriptor.TYPE_FLOAT: celtypes.DoubleType, + descriptor.FieldDescriptor.TYPE_DOUBLE: celtypes.DoubleType, + descriptor.FieldDescriptor.TYPE_INT32: celtypes.IntType, + descriptor.FieldDescriptor.TYPE_INT64: celtypes.IntType, + descriptor.FieldDescriptor.TYPE_UINT32: celtypes.UintType, + descriptor.FieldDescriptor.TYPE_UINT64: celtypes.UintType, + descriptor.FieldDescriptor.TYPE_SINT32: celtypes.IntType, + descriptor.FieldDescriptor.TYPE_SINT64: celtypes.IntType, + descriptor.FieldDescriptor.TYPE_FIXED32: celtypes.UintType, + descriptor.FieldDescriptor.TYPE_FIXED64: celtypes.UintType, + descriptor.FieldDescriptor.TYPE_SFIXED32: celtypes.IntType, + descriptor.FieldDescriptor.TYPE_SFIXED64: celtypes.IntType, +} + + +def _scalar_field_value_to_cel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: + ctor = _TYPE_TO_CTOR.get(field.type) + if ctor is None: + msg = "unknown field type" + raise CompilationError(msg) + return ctor(val) + + +def _field_value_to_cel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: + if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if field.message_type is not None and field.message_type.GetOptions().map_entry: + return _map_field_value_to_cel(val, field) + return _repeated_field_value_to_cel(val, field) + return _scalar_field_value_to_cel(val, field) + + +def _is_empty_field(msg: message.Message, field: descriptor.FieldDescriptor) -> bool: + if field.containing_oneof is not None and not msg.HasField(field.name): + return True + if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + return len(getattr(msg, field.name)) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_MESSAGE: + return not msg.HasField(field.name) + if field.type == descriptor.FieldDescriptor.TYPE_BOOL: + return not getattr(msg, field.name) + if field.type == descriptor.FieldDescriptor.TYPE_BYTES: + return len(getattr(msg, field.name)) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_STRING: + return len(getattr(msg, field.name)) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_FLOAT: + return getattr(msg, field.name) == 0.0 + if field.type == descriptor.FieldDescriptor.TYPE_DOUBLE: + return getattr(msg, field.name) == 0.0 + if field.type == descriptor.FieldDescriptor.TYPE_INT32: + return getattr(msg, field.name) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_INT64: + return getattr(msg, field.name) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_UINT32: + return getattr(msg, field.name) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_UINT64: + return getattr(msg, field.name) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_SINT32: + return getattr(msg, field.name) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_SINT64: + return getattr(msg, field.name) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_FIXED32: + return getattr(msg, field.name) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_FIXED64: + return getattr(msg, field.name) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_SFIXED32: + return getattr(msg, field.name) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_SFIXED64: + return getattr(msg, field.name) == 0 + if field.type == descriptor.FieldDescriptor.TYPE_ENUM: + return getattr(msg, field.name) == 0 + exception_msg = "unknown field type" + raise ValueError(exception_msg) + + +def _repeated_field_to_cel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: + if field.message_type is not None and field.message_type.GetOptions().map_entry: + return _map_field_to_cel(msg, field) + return _repeated_field_value_to_cel(getattr(msg, field.name), field) + + +def _repeated_field_value_to_cel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: + result = celtypes.ListType() + for item in val: + result.append(_scalar_field_value_to_cel(item, field)) + return result + + +def _map_field_value_to_cel(mapping: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: + result = celtypes.MapType() + key_field = field.message_type.fields[0] + val_field = field.message_type.fields[1] + for key, val in mapping.items(): + result[_field_value_to_cel(key, key_field)] = _field_value_to_cel(val, val_field) + return result + + +def _map_field_to_cel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: + return _map_field_value_to_cel(getattr(msg, field.name), field) + + +def _field_to_cel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: + if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + return _repeated_field_to_cel(msg, field) + elif field.message_type is not None and not msg.HasField(field.name): + return None + else: + return _scalar_field_value_to_cel(getattr(msg, field.name), field) + + +class ConstraintContext: + """The state associated with a single constraint evaluation.""" + + def __init__(self, fail_fast: bool = False, violations: expression_pb2.Violations = None): # noqa: FBT001, FBT002 + self._fail_fast = fail_fast + if violations is None: + violations = expression_pb2.Violations() + self._violations = violations + + @property + def fail_fast(self) -> bool: + return self._fail_fast + + @property + def violations(self) -> expression_pb2.Violations: + return self._violations + + def add(self, field_name: str, constraint_id: str, message: str, *, for_key: bool = False): + self._violations.violations.append( + expression_pb2.Violation( + field_path=field_name, + constraint_id=constraint_id, + message=message, + for_key=for_key, + ) + ) + + def add_errors(self, other_ctx): + self._violations.violations.extend(other_ctx.violations.violations) + + def add_path_prefix(self, prefix: str, delim="."): + for violation in self._violations.violations: + if violation.field_path: + violation.field_path = prefix + delim + violation.field_path + else: + violation.field_path = prefix + + @property + def done(self) -> bool: + return self._fail_fast and self.has_errors() + + def has_errors(self) -> bool: + return len(self._violations.violations) > 0 + + def sub_context(self): + return ConstraintContext(self._fail_fast) + + +class ConstraintRules: + """The constraints associated with a single 'rules' message.""" + + def validate(self, ctx: ConstraintContext, message: message.Message): # noqa: ARG002 + """Validate the message against the rules in this constraint.""" + ctx.add("", "unimplemented", "Unimplemented") + + +class CelConstraintRules(ConstraintRules): + """A constraint that has rules written in CEL.""" + + _runners: list[typing.Tuple[celpy.Runner, expression_pb2.Constraint | private_pb2.Constraint]] + _rules_cel: celtypes.Value = None + + def __init__(self, rules: message.Message | None): + self._runners = [] + if rules is not None: + self._rules_cel = _msg_to_cel(rules) + + def _validate_cel( + self, ctx: ConstraintContext, field_name: str, activation: dict[str, typing.Any], *, for_key: bool = False + ): + activation["rules"] = self._rules_cel + activation["now"] = celtypes.TimestampType(datetime.datetime.now(tz=datetime.timezone.utc)) + for runner, constraint in self._runners: + result = runner.evaluate(activation) + if isinstance(result, celtypes.BoolType): + if not result: + ctx.add(field_name, constraint.id, constraint.message, for_key=for_key) + elif isinstance(result, celtypes.StringType): + if result: + ctx.add(field_name, constraint.id, result, for_key=for_key) + elif isinstance(result, Exception): + raise result + + def add_rule( + self, + env: celpy.Environment, + funcs: dict[str, celpy.CELFunction], + rules: expression_pb2.Constraint | private_pb2.Constraint, + ): + ast = env.compile(rules.expression) + prog = env.program(ast, functions=funcs) + self._runners.append((prog, rules)) + + +class MessageConstraintRules(CelConstraintRules): + """Message-level rules.""" + + def validate(self, ctx: ConstraintContext, message: message.Message): + self._validate_cel(ctx, "", {"this": _msg_to_cel(message)}) + + +def check_field_type(field: descriptor.FieldDescriptor, expected: int, wrapper_name: str | None = None): + if field.type != expected and ( + field.type != descriptor.FieldDescriptor.TYPE_MESSAGE or field.message_type.full_name != wrapper_name + ): + msg = f"field {field.name} has type {field.type} but expected {expected}" + raise CompilationError(msg) + + +class FieldConstraintRules(CelConstraintRules): + """Field-level rules.""" + + _ignore_empty = False + _required = False + + def __init__( + self, + env: celpy.Environment, + funcs: dict[str, celpy.CELFunction], + field: descriptor.FieldDescriptor, + field_level: validate_pb2.FieldConstraints, + ): + type_case = field_level.WhichOneof("type") + super().__init__(None if type_case is None else getattr(field_level, type_case)) + self._field = field + if field_level.ignore_empty: + self._ignore_empty = True + if field_level.required: + self._required = True + type_case = field_level.WhichOneof("type") + if type_case is not None: + rules = getattr(field_level, type_case) + # For each set field in the message, look for the private constraint + # extension. + for field, _ in rules.ListFields(): + if private_pb2.field in field.GetOptions().Extensions: + for cel in field.GetOptions().Extensions[private_pb2.field].cel: + self.add_rule(env, funcs, cel) + for cel in field_level.cel: + self.add_rule(env, funcs, cel) + + def validate(self, ctx: ConstraintContext, message: message.Message): + if _is_empty_field(message, self._field): + if self._required: + ctx.add( + self._field.name, + "required", + "value is required", + ) + return + if ( + self._ignore_empty + or ( + self._field.label != descriptor.FieldDescriptor.LABEL_REPEATED + and self._field.type == descriptor.FieldDescriptor.TYPE_MESSAGE + ) + or self._field.containing_oneof is not None + ): + return + val = getattr(message, self._field.name) + self._validate_value(ctx, self._field.name, val) + self._validate_cel(ctx, self._field.name, {"this": _field_value_to_cel(val, self._field)}) + + def validate_item(self, ctx: ConstraintContext, field_path: str, val: typing.Any, *, for_key: bool = False): + self._validate_value(ctx, field_path, val, for_key=for_key) + self._validate_cel(ctx, field_path, {"this": _scalar_field_value_to_cel(val, self._field)}, for_key=for_key) + + def _validate_value(self, ctx: ConstraintContext, field_path: str, val: typing.Any, *, for_key: bool = False): + pass + + +class AnyConstraintRules(FieldConstraintRules): + """Rules for an Any field.""" + + _in: typing.List[str] = [] # noqa: RUF012 + _not_in: typing.List[str] = [] # noqa: RUF012 + + def __init__( + self, + env: celpy.Environment, + funcs: dict[str, celpy.CELFunction], + field: descriptor.FieldDescriptor, + field_level: validate_pb2.FieldConstraints, + ): + super().__init__(env, funcs, field, field_level) + if getattr(field_level.any, "in"): + self._in = getattr(field_level.any, "in") + if field_level.any.not_in: + self._not_in = field_level.any.not_in + + def _validate_value(self, ctx: ConstraintContext, field_path: str, value: any_pb2.Any, *, for_key: bool = False): + if len(self._in) > 0: + if value.type_url not in self._in: + ctx.add( + field_path, + "any.in", + "type URL must be in the allow list", + for_key=for_key, + ) + if value.type_url in self._not_in: + ctx.add( + field_path, + "any.not_in", + "type URL must not be in the block list", + for_key=for_key, + ) + + +class EnumConstraintRules(FieldConstraintRules): + """Rules for an enum field.""" + + _defined_only = False + + def __init__( + self, + env: celpy.Environment, + funcs: dict[str, celpy.CELFunction], + field: descriptor.FieldDescriptor, + field_level: validate_pb2.FieldConstraints, + ): + super().__init__(env, funcs, field, field_level) + if field_level.enum.defined_only: + self._defined_only = True + + def validate(self, ctx: ConstraintContext, message: message.Message): + super().validate(ctx, message) + if ctx.done: + return + if self._defined_only: + value = getattr(message, self._field.name) + if value not in self._field.enum_type.values_by_number: + ctx.add( + self._field.name, + "enum.defined_only", + "value must be one of the defined enum values", + ) + + +class RepeatedConstraintRules(FieldConstraintRules): + """Rules for a repeated field.""" + + _item_rules: FieldConstraintRules | None = None + + def __init__( + self, + env: celpy.Environment, + funcs: dict[str, celpy.CELFunction], + field: descriptor.FieldDescriptor, + field_level: validate_pb2.FieldConstraints, + item_rules: FieldConstraintRules | None, + ): + super().__init__(env, funcs, field, field_level) + if item_rules is not None: + self._item_rules = item_rules + + def validate(self, ctx: ConstraintContext, message: message.Message): + super().validate(ctx, message) + if ctx.done: + return + value = getattr(message, self._field.name) + if self._item_rules is not None: + for i, item in enumerate(value): + sub_ctx = ctx.sub_context() + self._item_rules.validate_item(sub_ctx, "", item) + if sub_ctx.has_errors(): + sub_ctx.add_path_prefix(f"{self._field.name}[{i}]", "") + ctx.add_errors(sub_ctx) + if ctx.done: + return + + +class MapConstraintRules(FieldConstraintRules): + """Rules for a map field.""" + + _key_rules: FieldConstraintRules | None = None + _value_rules: FieldConstraintRules | None = None + + def __init__( + self, + env: celpy.Environment, + funcs: dict[str, celpy.CELFunction], + field: descriptor.FieldDescriptor, + field_level: validate_pb2.FieldConstraints, + key_rules: FieldConstraintRules | None, + value_rules: FieldConstraintRules | None, + ): + super().__init__(env, funcs, field, field_level) + if key_rules is not None: + self._key_rules = key_rules + if value_rules is not None: + self._value_rules = value_rules + + def validate(self, ctx: ConstraintContext, message: message.Message): + super().validate(ctx, message) + if ctx.done: + return + value = getattr(message, self._field.name) + for k, v in value.items(): + key_field_path = make_key_path(self._field.name, k) + if self._key_rules is not None: + self._key_rules.validate_item(ctx, key_field_path, k, for_key=True) + if self._value_rules is not None: + self._value_rules.validate_item(ctx, key_field_path, v) + + +class OneofConstraintRules(ConstraintRules): + """Rules for a oneof definition.""" + + required = True + + def __init__(self, oneof: descriptor.OneofDescriptor, rules: validate_pb2.OneofConstraints): + self._oneof = oneof + if not rules.required: + self.required = False + + def validate(self, ctx: ConstraintContext, message: message.Message): + if not message.WhichOneof(self._oneof.name): + if self.required: + ctx.add( + self._oneof.name, + "required", + "exactly one field is required in oneof", + ) + return + + +class ConstraintFactory: + """Factory for creating and caching constraints.""" + + _env: celpy.Environment + _funcs: dict[str, celpy.CELFunction] + _cache: dict[descriptor.Descriptor, list[ConstraintRules] | Exception] + + def __init__(self, funcs: dict[str, celpy.CELFunction]): + self._env = celpy.Environment() + self._funcs = funcs + self._cache = {} + + def get(self, descriptor: descriptor.Descriptor) -> list[ConstraintRules]: + if descriptor not in self._cache: + try: + self._cache[descriptor] = self._new_constraints(descriptor) + except Exception as e: + self._cache[descriptor] = e + result = self._cache[descriptor] + if isinstance(result, Exception): + raise result + return result + + def _new_message_constraint(self, rules: validate_pb2.message) -> MessageConstraintRules: + result = MessageConstraintRules(rules) + for cel in rules.cel: + result.add_rule(self._env, self._funcs, cel) + return result + + def _new_scalar_field_constraint( + self, + field: descriptor.FieldDescriptor, + field_level: validate_pb2.field, + ): + if field_level.skipped: + return None + type_case = field_level.WhichOneof("type") + if type_case is None: + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "duration": + check_field_type(field, 0, "google.protobuf.Duration") + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "timestamp": + check_field_type(field, 0, "google.protobuf.Timestamp") + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "enum": + check_field_type(field, descriptor.FieldDescriptor.TYPE_ENUM) + result = EnumConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "bool": + check_field_type(field, descriptor.FieldDescriptor.TYPE_BOOL, "google.protobuf.BoolValue") + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "bytes": + check_field_type( + field, + descriptor.FieldDescriptor.TYPE_BYTES, + "google.protobuf.BytesValue", + ) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "fixed32": + check_field_type(field, descriptor.FieldDescriptor.TYPE_FIXED32) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "fixed64": + check_field_type(field, descriptor.FieldDescriptor.TYPE_FIXED64) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "float": + check_field_type( + field, + descriptor.FieldDescriptor.TYPE_FLOAT, + "google.protobuf.FloatValue", + ) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "double": + check_field_type( + field, + descriptor.FieldDescriptor.TYPE_DOUBLE, + "google.protobuf.DoubleValue", + ) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "int32": + check_field_type( + field, + descriptor.FieldDescriptor.TYPE_INT32, + "google.protobuf.Int32Value", + ) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "int64": + check_field_type( + field, + descriptor.FieldDescriptor.TYPE_INT64, + "google.protobuf.Int64Value", + ) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "sfixed32": + check_field_type(field, descriptor.FieldDescriptor.TYPE_SFIXED32) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "sfixed64": + check_field_type(field, descriptor.FieldDescriptor.TYPE_SFIXED64) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "sint32": + check_field_type(field, descriptor.FieldDescriptor.TYPE_SINT32) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "sint64": + check_field_type(field, descriptor.FieldDescriptor.TYPE_SINT64) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "uint32": + check_field_type( + field, + descriptor.FieldDescriptor.TYPE_UINT32, + "google.protobuf.UInt32Value", + ) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "uint64": + check_field_type( + field, + descriptor.FieldDescriptor.TYPE_UINT64, + "google.protobuf.UInt64Value", + ) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "string": + check_field_type( + field, + descriptor.FieldDescriptor.TYPE_STRING, + "google.protobuf.StringValue", + ) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) + return result + elif type_case == "any": + check_field_type(field, descriptor.FieldDescriptor.TYPE_MESSAGE, "google.protobuf.Any") + result = AnyConstraintRules(self._env, self._funcs, field, field_level) + return result + + def _new_field_constraint( + self, + field: descriptor.FieldDescriptor, + rules: validate_pb2.field, + ) -> FieldConstraintRules: + if field.label != descriptor.FieldDescriptor.LABEL_REPEATED: + return self._new_scalar_field_constraint(field, rules) + if field.message_type is not None and field.message_type.GetOptions().map_entry: + key_rules = None + if rules.map.HasField("keys"): + key_field = field.message_type.fields_by_name["key"] + key_rules = self._new_scalar_field_constraint(key_field, rules.map.keys) + value_rules = None + if rules.map.HasField("values"): + value_field = field.message_type.fields_by_name["value"] + value_rules = self._new_scalar_field_constraint(value_field, rules.map.values) + return MapConstraintRules(self._env, self._funcs, field, rules, key_rules, value_rules) + item_rule = None + if rules.repeated.HasField("items"): + item_rule = self._new_scalar_field_constraint(field, rules.repeated.items) + return RepeatedConstraintRules(self._env, self._funcs, field, rules, item_rule) + + def _new_constraints(self, desc: descriptor.Descriptor) -> list[ConstraintRules]: + result: list[ConstraintRules] = [] + constraint: ConstraintRules | None = None + if validate_pb2.message in desc.GetOptions().Extensions: + message_level = desc.GetOptions().Extensions[validate_pb2.message] + if message_level.disabled: + return [] + if constraint := self._new_message_constraint(message_level): + result.append(constraint) + + for oneof in desc.oneofs: + if validate_pb2.oneof in oneof.GetOptions().Extensions: + if constraint := OneofConstraintRules(oneof, oneof.GetOptions().Extensions[validate_pb2.oneof]): + result.append(constraint) + + for field in desc.fields: + if validate_pb2.field in field.GetOptions().Extensions: + field_level = field.GetOptions().Extensions[validate_pb2.field] + if field_level.skipped: + continue + result.append(self._new_field_constraint(field, field_level)) + if field_level.repeated.items.skipped: + continue + if field.message_type is None: + continue + if field.message_type.GetOptions().map_entry: + value_field = field.message_type.fields_by_name["value"] + if value_field.type != descriptor.FieldDescriptor.TYPE_MESSAGE: + continue + result.append(MapValMsgConstraint(self, field, value_field)) + elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + result.append(RepeatedMsgConstraint(self, field)) + else: + result.append(SubMsgConstraint(self, field)) + return result + + +class SubMsgConstraint(ConstraintRules): + def __init__( + self, + factory: ConstraintFactory, + field: descriptor.FieldDescriptor, + ): + self._factory = factory + self._field = field + + def validate(self, ctx: ConstraintContext, message: message.Message): + if not message.HasField(self._field.name): + return + constraints = self._factory.get(self._field.message_type) + if constraints is None: + return + val = getattr(message, self._field.name) + sub_ctx = ctx.sub_context() + for constraint in constraints: + constraint.validate(sub_ctx, val) + if sub_ctx.has_errors(): + sub_ctx.add_path_prefix(self._field.name) + ctx.add_errors(sub_ctx) + + +class MapValMsgConstraint(ConstraintRules): + def __init__( + self, + factory: ConstraintFactory, + field: descriptor.FieldDescriptor, + value_field: descriptor.FieldDescriptor, + ): + self._factory = factory + self._field = field + self._value_field = value_field + + def validate(self, ctx: ConstraintContext, message: message.Message): + val = getattr(message, self._field.name) + if not val: + return + constraints = self._factory.get(self._value_field.message_type) + if constraints is None: + return + for k, v in val.items(): + sub_ctx = ctx.sub_context() + for constraint in constraints: + constraint.validate(sub_ctx, v) + if sub_ctx.has_errors(): + sub_ctx.add_path_prefix(f"{self._field.name}[{k}]") + ctx.add_errors(sub_ctx) + + +class RepeatedMsgConstraint(ConstraintRules): + def __init__( + self, + factory: ConstraintFactory, + field: descriptor.FieldDescriptor, + ): + self._factory = factory + self._field = field + + def validate(self, ctx: ConstraintContext, message: message.Message): + val = getattr(message, self._field.name) + if not val: + return + constraints = self._factory.get(self._field.message_type) + if constraints is None: + return + for idx, item in enumerate(val): + sub_ctx = ctx.sub_context() + for constraint in constraints: + constraint.validate(sub_ctx, item) + if sub_ctx.has_errors(): + sub_ctx.add_path_prefix(f"{self._field.name}[{idx}]") + ctx.add_errors(sub_ctx) diff --git a/a7p/protovalidate/internal/extra_func.py b/a7p/protovalidate/internal/extra_func.py new file mode 100644 index 0000000..8228a2f --- /dev/null +++ b/a7p/protovalidate/internal/extra_func.py @@ -0,0 +1,158 @@ +# Copyright 2023 Buf Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from ipaddress import IPv4Address, IPv6Address, ip_address +from urllib import parse as urlparse + +import celpy # type: ignore +from celpy import celtypes # type: ignore + +from a7p.protovalidate.internal import string_format + + +def _validate_hostname(host): + if not host: + return False + if len(host) > 253: + return False + + if host[-1] == ".": + host = host[:-1] + + for part in host.split("."): + if len(part) == 0 or len(part) > 63: + return False + + # Host names cannot begin or end with hyphens + if part[0] == "-" or part[-1] == "-": + return False + for r in part: + if (r < "A" or r > "Z") and (r < "a" or r > "z") and (r < "0" or r > "9") and r != "-": + return False + return True + + +def validate_email(addr): + if "<" in addr and ">" in addr: + addr = addr.split("<")[1].split(">")[0] + + if len(addr) > 254: + return False + + parts = addr.split("@") + if len(parts) != 2: + return False + if len(parts[0]) > 64: + return False + return _validate_hostname(parts[1]) + + +def is_ip(val: celtypes.Value, version: celtypes.Value | None = None) -> celpy.Result: + if not isinstance(val, (celtypes.BytesType, celtypes.StringType)): + msg = "invalid argument, expected string or bytes" + raise celpy.EvalError(msg) + try: + if version is None: + ip_address(val) + elif version == 4: + IPv4Address(val) + elif version == 6: + IPv6Address(val) + else: + msg = "invalid argument, expected 4 or 6" + raise celpy.EvalError(msg) + return celtypes.BoolType(True) + except ValueError: + return celtypes.BoolType(False) + + +def is_email(string: celtypes.Value) -> celpy.Result: + if not isinstance(string, celtypes.StringType): + msg = "invalid argument, expected string" + raise celpy.EvalError(msg) + return celtypes.BoolType(validate_email(string)) + + +def is_uri(string: celtypes.Value) -> celpy.Result: + url = urlparse.urlparse(string) + if not all([url.scheme, url.netloc, url.path]): + return celtypes.BoolType(False) + return celtypes.BoolType(True) + + +def is_uri_ref(string: celtypes.Value) -> celpy.Result: + url = urlparse.urlparse(string) + if not all([url.scheme, url.path]) and url.fragment: + return celtypes.BoolType(False) + return celtypes.BoolType(True) + + +def is_hostname(string: celtypes.Value) -> celpy.Result: + if not isinstance(string, celtypes.StringType): + msg = "invalid argument, expected string" + raise celpy.EvalError(msg) + return celtypes.BoolType(_validate_hostname(string)) + + +def is_nan(val: celtypes.Value) -> celpy.Result: + if not isinstance(val, celtypes.DoubleType): + msg = "invalid argument, expected double" + raise celpy.EvalError(msg) + return celtypes.BoolType(math.isnan(val)) + + +def is_inf(val: celtypes.Value, sign: None | celtypes.Value = None) -> celpy.Result: + if not isinstance(val, celtypes.DoubleType): + msg = "invalid argument, expected double" + raise celpy.EvalError(msg) + if sign is None: + return celtypes.BoolType(math.isinf(val)) + + if not isinstance(sign, celtypes.IntType): + msg = "invalid argument, expected int" + raise celpy.EvalError(msg) + if sign > 0: + return celtypes.BoolType(math.isinf(val) and val > 0) + elif sign < 0: + return celtypes.BoolType(math.isinf(val) and val < 0) + else: + return celtypes.BoolType(math.isinf(val)) + + +def unique(val: celtypes.Value) -> celpy.Result: + if not isinstance(val, celtypes.ListType): + msg = "invalid argument, expected list" + raise celpy.EvalError(msg) + return celtypes.BoolType(len(val) == len(set(val))) + + +def make_extra_funcs(locale: str) -> dict[str, celpy.CELFunction]: + string_fmt = string_format.StringFormat(locale) + return { + # Missing standard functions + "format": string_fmt.format, + # protovalidate specific functions + "isNan": is_nan, + "isInf": is_inf, + "isIp": is_ip, + "isEmail": is_email, + "isUri": is_uri, + "isUriRef": is_uri_ref, + "isHostname": is_hostname, + "unique": unique, + } + + +EXTRA_FUNCS = make_extra_funcs("en_US") diff --git a/a7p/protovalidate/internal/string_format.py b/a7p/protovalidate/internal/string_format.py new file mode 100644 index 0000000..bed78f5 --- /dev/null +++ b/a7p/protovalidate/internal/string_format.py @@ -0,0 +1,173 @@ +# Copyright 2023 Buf Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import celpy # type: ignore +from celpy import celtypes # type: ignore + +QUOTE_TRANS = str.maketrans( + { + "\a": r"\a", + "\b": r"\b", + "\f": r"\f", + "\n": r"\n", + "\r": r"\r", + "\t": r"\t", + "\v": r"\v", + "\\": r"\\", + '"': r"\"", + } +) + + +def quote(s: str) -> str: + return '"' + s.translate(QUOTE_TRANS) + '"' + + +class StringFormat: + """An implementation of string.format() in CEL.""" + + def __init__(self, locale: str): + self.locale = locale + + def format(self, fmt: celtypes.Value, args: celtypes.Value) -> celpy.Result: # noqa: A003 + if not isinstance(fmt, celtypes.StringType): + return celpy.native_to_cel(celpy.new_error("format() requires a string as the first argument")) + if not isinstance(args, celtypes.ListType): + return celpy.native_to_cel(celpy.new_error("format() requires a list as the second argument")) + # printf style formatting + i = 0 + j = 0 + result = "" + while i < len(fmt): + if fmt[i] != "%": + result += fmt[i] + i += 1 + continue + + if i + 1 < len(fmt) and fmt[i + 1] == "%": + result += "%" + i += 2 + continue + if j >= len(args): + return celpy.CELEvalError("format() not enough arguments for format string") + arg = args[j] + j += 1 + i += 1 + if i >= len(fmt): + return celpy.CELEvalError("format() incomplete format specifier") + precision = 6 + if fmt[i] == ".": + i += 1 + precision = 0 + while i < len(fmt) and fmt[i].isdigit(): + precision = precision * 10 + int(fmt[i]) + i += 1 + if i >= len(fmt): + return celpy.CELEvalError("format() incomplete format specifier") + if fmt[i] == "f": + result += self.format_float(arg, precision) + if fmt[i] == "e": + result += self.format_exponential(arg, precision) + elif fmt[i] == "d": + result += self.format_int(arg) + elif fmt[i] == "s": + result += self.format_string(arg) + elif fmt[i] == "x": + result += self.format_hex(arg) + elif fmt[i] == "X": + result += self.format_hex(arg).upper() + elif fmt[i] == "o": + result += self.format_oct(arg) + elif fmt[i] == "b": + result += self.format_bin(arg) + else: + return celpy.CELEvalError("format() unknown format specifier: " + fmt[i]) + i += 1 + if j < len(args): + return celpy.CELEvalError("format() too many arguments for format string") + return celtypes.StringType(result) + + def format_float(self, arg: celtypes.Value, precision: int) -> celpy.Result: + if isinstance(arg, celtypes.DoubleType): + return celtypes.StringType(f"{arg:.{precision}f}") + return self.format_int(arg) + + def format_exponential(self, arg: celtypes.Value, precision: int) -> celpy.Result: + if isinstance(arg, celtypes.DoubleType): + return celtypes.StringType(f"{arg:.{precision}e}") + return self.format_int(arg) + + def format_int(self, arg: celtypes.Value) -> celpy.Result: + if isinstance(arg, celtypes.IntType): + return celtypes.StringType(arg) + if isinstance(arg, celtypes.UintType): + return celtypes.StringType(arg) + return celpy.CELEvalError("format_int() requires an integer argument") + + def format_hex(self, arg: celtypes.Value) -> celpy.Result: + if isinstance(arg, celtypes.IntType): + return celtypes.StringType(f"{arg:x}") + if isinstance(arg, celtypes.UintType): + return celtypes.StringType(f"{arg:x}") + if isinstance(arg, celtypes.BytesType): + return celtypes.StringType(arg.hex()) + if isinstance(arg, celtypes.StringType): + return celtypes.StringType(arg.encode("utf-8").hex()) + return celpy.CELEvalError("format_hex() requires an integer, string, or binary argument") + + def format_oct(self, arg: celtypes.Value) -> celpy.Result: + if isinstance(arg, celtypes.IntType): + return celtypes.StringType(f"{arg:o}") + if isinstance(arg, celtypes.UintType): + return celtypes.StringType(f"{arg:o}") + return celpy.CELEvalError("format_oct() requires an integer argument") + + def format_bin(self, arg: celtypes.Value) -> celpy.Result: + if isinstance(arg, celtypes.IntType): + return celtypes.StringType(f"{arg:b}") + if isinstance(arg, celtypes.UintType): + return celtypes.StringType(f"{arg:b}") + if isinstance(arg, celtypes.BoolType): + return celtypes.StringType(f"{arg:b}") + return celpy.CELEvalError("format_bin() requires an integer argument") + + def format_string(self, arg: celtypes.Value) -> celpy.Result: + if isinstance(arg, celtypes.StringType): + return arg + if isinstance(arg, celtypes.BytesType): + return celtypes.StringType(arg.hex()) + if isinstance(arg, celtypes.ListType): + return self.format_list(arg) + return celtypes.StringType(arg) + + def format_value(self, arg: celtypes.Value) -> celpy.Result: + if isinstance(arg, (celtypes.StringType, str)): + return celtypes.StringType(quote(arg)) + if isinstance(arg, celtypes.UintType): + return celtypes.StringType(arg) + return self.format_string(arg) + + def format_list(self, arg: celtypes.ListType) -> celpy.Result: + result = "[" + for i in range(len(arg)): + if i > 0: + result += ", " + result += self.format_value(arg[i]) + result += "]" + return celtypes.StringType(result) + + +_default_format = StringFormat("en_US") +format = _default_format.format # noqa: A001 +format_value = _default_format.format_value diff --git a/a7p/protovalidate/validator.py b/a7p/protovalidate/validator.py new file mode 100644 index 0000000..7e6582a --- /dev/null +++ b/a7p/protovalidate/validator.py @@ -0,0 +1,108 @@ +# Copyright 2023 Buf Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing + +from google.protobuf import message + +from a7p.buf.validate import expression_pb2 # type: ignore +from a7p.protovalidate.internal import constraints as _constraints +from a7p.protovalidate.internal import extra_func + +CompilationError = _constraints.CompilationError +Violations = expression_pb2.Violations + + +class Validator: + """ + Validates protobuf messages against static constraints. + + Each validator instance caches internal state generated from the static + constraints, so reusing the same instance for multiple validations + significantly improves performance. + """ + + _factory: _constraints.ConstraintFactory + + def __init__(self): + self._factory = _constraints.ConstraintFactory(extra_func.EXTRA_FUNCS) + + def validate( + self, + message: message.Message, + *, + fail_fast: bool = False, + ): + """ + Validates the given message against the static constraints defined in + the message's descriptor. + + Parameters: + message: The message to validate. + fail_fast: If true, validation will stop after the first violation. + Raises: + CompilationError: If the static constraints could not be compiled. + ValidationError: If the message is invalid. + """ + violations = self.collect_violations(message, fail_fast=fail_fast) + if violations.violations: + msg = f"invalid {message.DESCRIPTOR.name}" + raise ValidationError(msg, violations) + + def collect_violations( + self, + message: message.Message, + *, + fail_fast: bool = False, + into: expression_pb2.Violations = None, + ) -> expression_pb2.Violations: + """ + Validates the given message against the static constraints defined in + the message's descriptor. Compared to validate, collect_violations is + faster but puts the burden of raising an appropriate exception on the + caller. + + Parameters: + message: The message to validate. + fail_fast: If true, validation will stop after the first violation. + into: If provided, any violations will be appended to the + Violations object and the same object will be returned. + Raises: + CompilationError: If the static constraints could not be compiled. + """ + ctx = _constraints.ConstraintContext(fail_fast=fail_fast, violations=into) + for constraint in self._factory.get(message.DESCRIPTOR): + constraint.validate(ctx, message) + if ctx.done: + break + return ctx.violations + + +class ValidationError(ValueError): + """ + An error raised when a message fails to validate. + """ + + violations: expression_pb2.Violations + + def __init__(self, msg: str, violations: expression_pb2.Violations): + super().__init__(msg) + self.violations = violations + + def errors(self) -> typing.List[expression_pb2.Violation]: + """ + Returns the validation errors as a simple Python list, rather than the + Protobuf-specific collection type used by Violations. + """ + return list(self.violations.violations) diff --git a/a7p/tests.py b/a7p/tests.py index bd57ddf..4524a4d 100644 --- a/a7p/tests.py +++ b/a7p/tests.py @@ -1,6 +1,6 @@ from unittest import TestCase -from py_a7p import A7PFile, A7PDataError +from a7p import A7PFile, A7PDataError class TestA7P(TestCase): diff --git a/profedit_validate_pb2.py b/profedit_validate_pb2.py deleted file mode 100644 index 1fc9914..0000000 --- a/profedit_validate_pb2.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: profedit_validate.proto -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from buf.validate import validate_pb2 as buf_dot_validate_dot_validate__pb2 - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17profedit_validate.proto\x12\x08profedit\x1a\x1b\x62uf/validate/validate.proto\"6\n\x07Payload\x12+\n\x07profile\x18\x01 \x01(\x0b\x32\x11.profedit.ProfileR\x07profile\".\n\x07\x43oefRow\x12\x13\n\x05\x62\x63_cd\x18\x01 \x01(\x05R\x04\x62\x63\x43\x64\x12\x0e\n\x02mv\x18\x02 \x01(\x05R\x02mv\"\xa3\x01\n\x05SwPos\x12\x13\n\x05\x63_idx\x18\x01 \x01(\x05R\x04\x63Idx\x12\x1f\n\x0breticle_idx\x18\x02 \x01(\x05R\nreticleIdx\x12\x12\n\x04zoom\x18\x03 \x01(\x05R\x04zoom\x12\x1a\n\x08\x64istance\x18\x04 \x01(\x05R\x08\x64istance\x12\x34\n\rdistance_from\x18\x05 \x01(\x0e\x32\x0f.profedit.DTypeR\x0c\x64istanceFrom\"\x97\x0b\n\x07Profile\x12*\n\x0cprofile_name\x18\x01 \x01(\tB\x07\xbaH\x04r\x02\x18\x32R\x0bprofileName\x12.\n\x0e\x63\x61rtridge_name\x18\x02 \x01(\tB\x07\xbaH\x04r\x02\x18\x32R\rcartridgeName\x12(\n\x0b\x62ullet_name\x18\x03 \x01(\tB\x07\xbaH\x04r\x02\x18\x32R\nbulletName\x12-\n\x0eshort_name_top\x18\x04 \x01(\tB\x07\xbaH\x04r\x02\x18\x08R\x0cshortNameTop\x12-\n\x0eshort_name_bot\x18\x05 \x01(\tB\x07\xbaH\x04r\x02\x18\x08R\x0cshortNameBot\x12%\n\tuser_note\x18\x06 \x01(\tB\x08\xbaH\x05r\x03\x18\xfa\x01R\x08userNote\x12+\n\x06zero_x\x18\x07 \x01(\x05\x42\x14\xbaH\x11\x1a\x0f\x18\xc0\xcf$(\xc0\xb0\xdb\xff\xff\xff\xff\xff\xff\x01R\x05zeroX\x12+\n\x06zero_y\x18\x08 \x01(\x05\x42\x14\xbaH\x11\x1a\x0f\x18\xc0\xcf$(\xc0\xb0\xdb\xff\xff\xff\xff\xff\xff\x01R\x05zeroY\x12\x30\n\tsc_height\x18\t \x01(\x05\x42\x13\xbaH\x10\x1a\x0e\x18\x88\'(\xf8\xd8\xff\xff\xff\xff\xff\xff\xff\x01R\x08scHeight\x12#\n\x07r_twist\x18\n \x01(\x05\x42\n\xbaH\x07\x1a\x05\x18\x90N(\x00R\x06rTwist\x12\x37\n\x11\x63_muzzle_velocity\x18\x0b \x01(\x05\x42\x0b\xbaH\x08\x1a\x06\x18\xb0\xea\x01(dR\x0f\x63MuzzleVelocity\x12@\n\x12\x63_zero_temperature\x18\x0c \x01(\x05\x42\x12\xbaH\x0f\x1a\r\x18\x64(\x9c\xff\xff\xff\xff\xff\xff\xff\xff\x01R\x10\x63ZeroTemperature\x12&\n\tc_t_coeff\x18\r \x01(\x05\x42\n\xbaH\x07\x1a\x05\x18\xb8\x17(\x02R\x07\x63TCoeff\x12\x39\n\x13\x63_zero_distance_idx\x18\x0e \x01(\x05\x42\n\xbaH\x07\x1a\x05\x10\xc8\x01(\x00R\x10\x63ZeroDistanceIdx\x12G\n\x16\x63_zero_air_temperature\x18\x0f \x01(\x05\x42\x12\xbaH\x0f\x1a\r\x18\x64(\x9c\xff\xff\xff\xff\xff\xff\xff\xff\x01R\x13\x63ZeroAirTemperature\x12-\n\x13\x63_zero_air_pressure\x18\x10 \x01(\x05R\x10\x63ZeroAirPressure\x12\x38\n\x13\x63_zero_air_humidity\x18\x11 \x01(\x05\x42\t\xbaH\x06\x1a\x04\x18\x64(\x00R\x10\x63ZeroAirHumidity\x12\x37\n\x0e\x63_zero_w_pitch\x18\x12 \x01(\x05\x42\x12\xbaH\x0f\x1a\r\x18Z(\xa6\xff\xff\xff\xff\xff\xff\xff\xff\x01R\x0b\x63ZeroWPitch\x12\x43\n\x14\x63_zero_p_temperature\x18\x13 \x01(\x05\x42\x12\xbaH\x0f\x1a\r\x18\x64(\x9c\xff\xff\xff\xff\xff\xff\xff\xff\x01R\x11\x63ZeroPTemperature\x12*\n\nb_diameter\x18\x14 \x01(\x05\x42\x0b\xbaH\x08\x1a\x06\x18\xff\xff\x03(\x01R\tbDiameter\x12&\n\x08\x62_weight\x18\x15 \x01(\x05\x42\x0b\xbaH\x08\x1a\x06\x18\xff\xff\x03(\nR\x07\x62Weight\x12%\n\x08\x62_length\x18\x16 \x01(\x05\x42\n\xbaH\x07\x1a\x05\x18\x90N(\x01R\x07\x62Length\x12/\n\ttwist_dir\x18\x17 \x01(\x0e\x32\x12.profedit.TwistDirR\x08twistDir\x12(\n\x07\x62\x63_type\x18\x18 \x01(\x0e\x32\x0f.profedit.GTypeR\x06\x62\x63Type\x12\x37\n\x08switches\x18\x19 \x03(\x0b\x32\x0f.profedit.SwPosB\n\xbaH\x07\x92\x01\x04\x08\x01\x10\x04R\x08switches\x12)\n\tdistances\x18\x1a \x03(\x05\x42\x0b\xbaH\x08\x92\x01\x05\x08\x04\x10\xc8\x01R\tdistances\x12;\n\tcoef_rows\x18\x1b \x03(\x0b\x32\x11.profedit.CoefRowB\x0b\xbaH\x08\x92\x01\x05\x08\x01\x10\xc8\x01R\x08\x63oefRows\x12!\n\x07\x63\x61liber\x18\x1c \x01(\tB\x07\xbaH\x04r\x02\x18\x32R\x07\x63\x61liber\x12(\n\x0b\x64\x65vice_uuid\x18\x1d \x01(\tB\x07\xbaH\x04r\x02\x18\x32R\ndeviceUuid*\x1d\n\x05\x44Type\x12\t\n\x05VALUE\x10\x00\x12\t\n\x05INDEX\x10\x01*#\n\x05GType\x12\x06\n\x02G1\x10\x00\x12\x06\n\x02G7\x10\x01\x12\n\n\x06\x43USTOM\x10\x02*\x1f\n\x08TwistDir\x12\t\n\x05RIGHT\x10\x00\x12\x08\n\x04LEFT\x10\x01\x42\x32Z0github.com/jaremko/a7p_transfer_example/profeditb\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'profedit_validate_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'Z0github.com/jaremko/a7p_transfer_example/profedit' - _PROFILE.fields_by_name['profile_name']._options = None - _PROFILE.fields_by_name['profile_name']._serialized_options = b'\272H\004r\002\0302' - _PROFILE.fields_by_name['cartridge_name']._options = None - _PROFILE.fields_by_name['cartridge_name']._serialized_options = b'\272H\004r\002\0302' - _PROFILE.fields_by_name['bullet_name']._options = None - _PROFILE.fields_by_name['bullet_name']._serialized_options = b'\272H\004r\002\0302' - _PROFILE.fields_by_name['short_name_top']._options = None - _PROFILE.fields_by_name['short_name_top']._serialized_options = b'\272H\004r\002\030\010' - _PROFILE.fields_by_name['short_name_bot']._options = None - _PROFILE.fields_by_name['short_name_bot']._serialized_options = b'\272H\004r\002\030\010' - _PROFILE.fields_by_name['user_note']._options = None - _PROFILE.fields_by_name['user_note']._serialized_options = b'\272H\005r\003\030\372\001' - _PROFILE.fields_by_name['zero_x']._options = None - _PROFILE.fields_by_name['zero_x']._serialized_options = b'\272H\021\032\017\030\300\317$(\300\260\333\377\377\377\377\377\377\001' - _PROFILE.fields_by_name['zero_y']._options = None - _PROFILE.fields_by_name['zero_y']._serialized_options = b'\272H\021\032\017\030\300\317$(\300\260\333\377\377\377\377\377\377\001' - _PROFILE.fields_by_name['sc_height']._options = None - _PROFILE.fields_by_name['sc_height']._serialized_options = b'\272H\020\032\016\030\210\'(\370\330\377\377\377\377\377\377\377\001' - _PROFILE.fields_by_name['r_twist']._options = None - _PROFILE.fields_by_name['r_twist']._serialized_options = b'\272H\007\032\005\030\220N(\000' - _PROFILE.fields_by_name['c_muzzle_velocity']._options = None - _PROFILE.fields_by_name['c_muzzle_velocity']._serialized_options = b'\272H\010\032\006\030\260\352\001(d' - _PROFILE.fields_by_name['c_zero_temperature']._options = None - _PROFILE.fields_by_name['c_zero_temperature']._serialized_options = b'\272H\017\032\r\030d(\234\377\377\377\377\377\377\377\377\001' - _PROFILE.fields_by_name['c_t_coeff']._options = None - _PROFILE.fields_by_name['c_t_coeff']._serialized_options = b'\272H\007\032\005\030\270\027(\002' - _PROFILE.fields_by_name['c_zero_distance_idx']._options = None - _PROFILE.fields_by_name['c_zero_distance_idx']._serialized_options = b'\272H\007\032\005\020\310\001(\000' - _PROFILE.fields_by_name['c_zero_air_temperature']._options = None - _PROFILE.fields_by_name['c_zero_air_temperature']._serialized_options = b'\272H\017\032\r\030d(\234\377\377\377\377\377\377\377\377\001' - _PROFILE.fields_by_name['c_zero_air_humidity']._options = None - _PROFILE.fields_by_name['c_zero_air_humidity']._serialized_options = b'\272H\006\032\004\030d(\000' - _PROFILE.fields_by_name['c_zero_w_pitch']._options = None - _PROFILE.fields_by_name['c_zero_w_pitch']._serialized_options = b'\272H\017\032\r\030Z(\246\377\377\377\377\377\377\377\377\001' - _PROFILE.fields_by_name['c_zero_p_temperature']._options = None - _PROFILE.fields_by_name['c_zero_p_temperature']._serialized_options = b'\272H\017\032\r\030d(\234\377\377\377\377\377\377\377\377\001' - _PROFILE.fields_by_name['b_diameter']._options = None - _PROFILE.fields_by_name['b_diameter']._serialized_options = b'\272H\010\032\006\030\377\377\003(\001' - _PROFILE.fields_by_name['b_weight']._options = None - _PROFILE.fields_by_name['b_weight']._serialized_options = b'\272H\010\032\006\030\377\377\003(\n' - _PROFILE.fields_by_name['b_length']._options = None - _PROFILE.fields_by_name['b_length']._serialized_options = b'\272H\007\032\005\030\220N(\001' - _PROFILE.fields_by_name['switches']._options = None - _PROFILE.fields_by_name['switches']._serialized_options = b'\272H\007\222\001\004\010\001\020\004' - _PROFILE.fields_by_name['distances']._options = None - _PROFILE.fields_by_name['distances']._serialized_options = b'\272H\010\222\001\005\010\004\020\310\001' - _PROFILE.fields_by_name['coef_rows']._options = None - _PROFILE.fields_by_name['coef_rows']._serialized_options = b'\272H\010\222\001\005\010\001\020\310\001' - _PROFILE.fields_by_name['caliber']._options = None - _PROFILE.fields_by_name['caliber']._serialized_options = b'\272H\004r\002\0302' - _PROFILE.fields_by_name['device_uuid']._options = None - _PROFILE.fields_by_name['device_uuid']._serialized_options = b'\272H\004r\002\0302' - _globals['_DTYPE']._serialized_start=1770 - _globals['_DTYPE']._serialized_end=1799 - _globals['_GTYPE']._serialized_start=1801 - _globals['_GTYPE']._serialized_end=1836 - _globals['_TWISTDIR']._serialized_start=1838 - _globals['_TWISTDIR']._serialized_end=1869 - _globals['_PAYLOAD']._serialized_start=66 - _globals['_PAYLOAD']._serialized_end=120 - _globals['_COEFROW']._serialized_start=122 - _globals['_COEFROW']._serialized_end=168 - _globals['_SWPOS']._serialized_start=171 - _globals['_SWPOS']._serialized_end=334 - _globals['_PROFILE']._serialized_start=337 - _globals['_PROFILE']._serialized_end=1768 -# @@protoc_insertion_point(module_scope) diff --git a/test.py b/test.py deleted file mode 100644 index 95dd632..0000000 --- a/test.py +++ /dev/null @@ -1,29 +0,0 @@ -import hashlib -from typing import BinaryIO -from profedit_validate_pb2 import Payload -import protovalidate - - -def loads(string: bytes): - data = string[32:] - md5_hash = hashlib.md5(data).hexdigest() - if md5_hash == string[:32].decode(): - profile = Payload() - profile.ParseFromString(data) - return profile - else: - raise ValueError - - -def load(file: BinaryIO) -> Payload: - string = file.read() - return loads(string) - - -with open('a7p/test.a7p', 'rb') as fp: - payload = load(fp) - - -payload.profile.profile_name = "a" * 55 - -protovalidate.validate(payload)