From 79843aa2ae8693885cc3524bd1bf726ac11c4352 Mon Sep 17 00:00:00 2001 From: Moirtz Wagner Date: Wed, 2 Sep 2026 20:46:59 +0200 Subject: [PATCH] SolarManager unter Versionsverwaltung Erster Stand der Hintergrundprozesse, die auf der Synology unter /volume1/homes/wagner/SolarManager laufen: der Manager selbst, die Sammler je Geraet, die MQTT-Bruecke, der Wecker und - neu hinzugezogen - der AutoAction-Runner, der als Hintergrundprozess hierher gehoert und nicht ins Web-Verzeichnis. Zugangsdaten stehen nicht mehr im Quelltext, sondern in config.ini, die nicht mit eingecheckt wird. Vorlage ist config.ini.example, gelesen wird sie von konfig.py. Betroffen waren solarManager.py (Datenbank und Wattpilot), zeit.py, gatherWaterData.py, wecker.py und skoda_testdaten.py, das sich das Passwort bisher aus dem Quelltext eines anderen Moduls herausgesucht hat. Die Kia-Anbindung ist mit dem Fahrzeug entfallen: kiaTest.py, gatherCarData.py und hyundai_kia_connect_api sind nicht mehr dabei, ebenso gatherInverterData.py, auf das nur noch eine auskommentierte Zeile zeigte. Die mitgelieferten Bibliotheken bleiben im Repository - die NAS hat kein pip, sie muessen neben den Skripten liegen. Co-Authored-By: Claude Opus 5 --- .gitattributes | 6 + .gitignore | 30 + Saldierung.txt | 4 + Wattpilot/Wattpilot.py | 673 ++ aiohttp/__init__.py | 230 + aiohttp/_cparser.pxd | 158 + aiohttp/_find_header.h | 14 + aiohttp/_find_header.pxd | 2 + aiohttp/_helpers.pyi | 6 + aiohttp/_helpers.pyx | 35 + aiohttp/_http_parser.pyx | 844 ++ aiohttp/_http_writer.pyx | 163 + aiohttp/_websocket.pyx | 56 + aiohttp/abc.py | 214 + aiohttp/base_protocol.py | 90 + aiohttp/client.py | 1289 +++ aiohttp/client_exceptions.py | 313 + aiohttp/client_proto.py | 269 + aiohttp/client_reqrep.py | 1089 +++ aiohttp/client_ws.py | 327 + aiohttp/compression_utils.py | 151 + aiohttp/connector.py | 1386 +++ aiohttp/cookiejar.py | 417 + aiohttp/formdata.py | 173 + aiohttp/hdrs.py | 108 + aiohttp/helpers.py | 1065 ++ aiohttp/http.py | 60 + aiohttp/http_exceptions.py | 105 + aiohttp/http_parser.py | 930 ++ aiohttp/http_websocket.py | 710 ++ aiohttp/http_writer.py | 198 + aiohttp/locks.py | 41 + aiohttp/log.py | 8 + aiohttp/multipart.py | 1031 ++ aiohttp/payload.py | 458 + aiohttp/py.typed | 1 + aiohttp/pytest_plugin.py | 363 + aiohttp/resolver.py | 118 + aiohttp/streams.py | 652 ++ aiohttp/tcp_helpers.py | 37 + aiohttp/test_utils.py | 618 ++ aiohttp/tracing.py | 471 + aiohttp/typedefs.py | 54 + aiohttp/web.py | 573 ++ aiohttp/web_app.py | 445 + aiohttp/web_exceptions.py | 509 + aiohttp/web_fileresponse.py | 281 + aiohttp/web_log.py | 213 + aiohttp/web_middlewares.py | 121 + aiohttp/web_protocol.py | 711 ++ aiohttp/web_request.py | 912 ++ aiohttp/web_response.py | 728 ++ aiohttp/web_routedef.py | 215 + aiohttp/web_runner.py | 450 + aiohttp/web_server.py | 79 + aiohttp/web_urldispatcher.py | 1198 +++ aiohttp/web_ws.py | 525 + aiohttp/worker.py | 239 + aiosignal/__init__.py | 36 + aiosignal/__init__.pyi | 12 + aiosignal/py.typed | 0 async_timeout/__init__.py | 239 + async_timeout/py.typed | 1 + autoActions/README.md | 272 + autoActions/autoaction_runner.py | 1022 ++ autoActions/config.ini.example | 87 + autoActions/fetch_calendar.py | 102 + autoActions/transports.py | 626 ++ charger_goE.py | 132 + config.ini.example | 40 + crcmod/__init__.py | 8 + crcmod/_crcfunpy.py | 107 + crcmod/crcmod.py | 457 + crcmod/predefined.py | 162 + crcmod/test.py | 540 ++ dateutil/__init__.py | 24 + dateutil/_common.py | 43 + dateutil/_version.py | 4 + dateutil/easter.py | 89 + dateutil/parser/__init__.py | 61 + dateutil/parser/_parser.py | 1613 ++++ dateutil/parser/isoparser.py | 416 + dateutil/relativedelta.py | 599 ++ dateutil/rrule.py | 1737 ++++ dateutil/tz/__init__.py | 12 + dateutil/tz/_common.py | 419 + dateutil/tz/_factories.py | 80 + dateutil/tz/tz.py | 1849 ++++ dateutil/tz/win.py | 370 + dateutil/tzwin.py | 2 + dateutil/utils.py | 71 + dateutil/zoneinfo/__init__.py | 167 + dateutil/zoneinfo/dateutil-zoneinfo.tar.gz | Bin 0 -> 156400 bytes dateutil/zoneinfo/rebuild.py | 75 + frozenlist/__init__.py | 95 + frozenlist/__init__.pyi | 47 + frozenlist/_frozenlist.pyx | 123 + frozenlist/py.typed | 1 + gatherDTUBIData.py | 59 + gatherHeaterData.py | 294 + gatherModbusData.py | 561 ++ gatherOpenDTUData.py | 371 + gatherShellyEM3DataEG.py | 44 + gatherShellyEM3DataUG.py | 43 + gatherSkodaData.py | 839 ++ gatherWaterData.py | 33 + goodwe/__init__.py | 138 + goodwe/const.py | 265 + goodwe/dt.py | 235 + goodwe/es.py | 440 + goodwe/et.py | 508 + goodwe/exceptions.py | 37 + goodwe/goodwe.py | 22 + goodwe/inverter.py | 305 + goodwe/modbus.py | 149 + goodwe/model.py | 26 + goodwe/processor.py | 37 + goodwe/protocol.py | 270 + goodwe/sensor.py | 761 ++ goodwe/xs.py | 43 + google/__init__.py | 4 + google/protobuf/__init__.py | 10 + google/protobuf/any_pb2.py | 37 + google/protobuf/api_pb2.py | 43 + google/protobuf/compiler/__init__.py | 0 google/protobuf/compiler/plugin_pb2.py | 46 + google/protobuf/descriptor.py | 1511 +++ google/protobuf/descriptor.upb.c | 1 + google/protobuf/descriptor.upb.h | 6699 +++++++++++++ google/protobuf/descriptor.upb_minitable.c | 1364 +++ google/protobuf/descriptor.upb_minitable.h | 79 + google/protobuf/descriptor.upbdefs.c | 514 + google/protobuf/descriptor.upbdefs.h | 192 + google/protobuf/descriptor_database.py | 154 + google/protobuf/descriptor_pb2.py | 3169 ++++++ google/protobuf/descriptor_pool.py | 1355 +++ google/protobuf/duration_pb2.py | 37 + google/protobuf/empty_pb2.py | 37 + google/protobuf/field_mask_pb2.py | 37 + google/protobuf/internal/__init__.py | 7 + google/protobuf/internal/_parameterized.py | 420 + .../protobuf/internal/api_implementation.py | 142 + google/protobuf/internal/builder.py | 118 + google/protobuf/internal/containers.py | 677 ++ google/protobuf/internal/decoder.py | 1024 ++ google/protobuf/internal/encoder.py | 806 ++ google/protobuf/internal/enum_type_wrapper.py | 101 + google/protobuf/internal/extension_dict.py | 194 + google/protobuf/internal/field_mask.py | 310 + google/protobuf/internal/message_listener.py | 55 + .../internal/python_edition_defaults.py | 5 + google/protobuf/internal/python_message.py | 1508 +++ google/protobuf/internal/testing_refleaks.py | 119 + google/protobuf/internal/type_checkers.py | 408 + google/protobuf/internal/well_known_types.py | 600 ++ google/protobuf/internal/wire_format.py | 245 + google/protobuf/json_format.py | 1060 ++ google/protobuf/message.py | 394 + google/protobuf/message_factory.py | 233 + google/protobuf/proto_builder.py | 111 + google/protobuf/pyext/__init__.py | 0 google/protobuf/pyext/cpp_message.py | 49 + google/protobuf/reflection.py | 72 + google/protobuf/runtime_version.py | 97 + google/protobuf/service.py | 205 + google/protobuf/service_reflection.py | 272 + google/protobuf/source_context_pb2.py | 37 + google/protobuf/struct_pb2.py | 47 + google/protobuf/symbol_database.py | 197 + google/protobuf/testdata/__init__.py | 0 google/protobuf/text_encoding.py | 106 + google/protobuf/text_format.py | 1864 ++++ google/protobuf/timestamp_pb2.py | 37 + google/protobuf/type_pb2.py | 53 + google/protobuf/unknown_fields.py | 97 + google/protobuf/util/__init__.py | 0 google/protobuf/wrappers_pb2.py | 53 + hoymiles_wifi/__init__.py | 9 + hoymiles_wifi/__main__.py | 398 + hoymiles_wifi/const.py | 118 + hoymiles_wifi/dtu.py | 422 + hoymiles_wifi/hoymiles.py | 304 + hoymiles_wifi/protobuf/APPHeartbeatPB.proto | 15 + hoymiles_wifi/protobuf/APPHeartbeatPB_pb2.py | 28 + .../protobuf/APPInfomationData.proto | 89 + .../protobuf/APPInfomationData_pb2.py | 38 + hoymiles_wifi/protobuf/AlarmData.proto | 46 + hoymiles_wifi/protobuf/AlarmData_pb2.py | 34 + hoymiles_wifi/protobuf/AppGetHistED.proto | 19 + hoymiles_wifi/protobuf/AppGetHistED_pb2.py | 30 + hoymiles_wifi/protobuf/AppGetHistPower.proto | 25 + hoymiles_wifi/protobuf/AppGetHistPower_pb2.py | 28 + hoymiles_wifi/protobuf/AutoSearch.proto | 17 + hoymiles_wifi/protobuf/AutoSearch_pb2.py | 28 + hoymiles_wifi/protobuf/CommandPB.proto | 78 + hoymiles_wifi/protobuf/CommandPB_pb2.py | 42 + hoymiles_wifi/protobuf/DevConfig.proto | 76 + hoymiles_wifi/protobuf/DevConfig_pb2.py | 36 + hoymiles_wifi/protobuf/EventData.proto | 26 + hoymiles_wifi/protobuf/EventData_pb2.py | 30 + hoymiles_wifi/protobuf/GPSTData.proto | 41 + hoymiles_wifi/protobuf/GPSTData_pb2.py | 32 + hoymiles_wifi/protobuf/GetConfig.proto | 71 + hoymiles_wifi/protobuf/GetConfig_pb2.py | 28 + hoymiles_wifi/protobuf/InfomationData.proto | 83 + hoymiles_wifi/protobuf/InfomationData_pb2.py | 38 + hoymiles_wifi/protobuf/NetworkInfo.proto | 19 + hoymiles_wifi/protobuf/NetworkInfo_pb2.py | 28 + hoymiles_wifi/protobuf/RealData.proto | 78 + hoymiles_wifi/protobuf/RealDataNew.proto | 123 + hoymiles_wifi/protobuf/RealDataNew_pb2.py | 40 + hoymiles_wifi/protobuf/RealData_pb2.py | 34 + hoymiles_wifi/protobuf/SetConfig.proto | 62 + hoymiles_wifi/protobuf/SetConfig_pb2.py | 28 + hoymiles_wifi/protobuf/WarnData.proto | 50 + hoymiles_wifi/protobuf/WarnData_pb2.py | 34 + hoymiles_wifi/protobuf/__init__.py | 0 hoymiles_wifi/protobuf/compile_proto.sh | 7 + hoymiles_wifi/utils.py | 61 + idna/__init__.py | 44 + idna/codec.py | 118 + idna/compat.py | 13 + idna/core.py | 400 + idna/idnadata.py | 2148 ++++ idna/intranges.py | 54 + idna/package_data.py | 2 + idna/py.typed | 0 idna/uts46data.py | 8600 +++++++++++++++++ konfig.py | 62 + logging.ini | 74 + mqttClient.py | 89 + multidict/__init__.py | 48 + multidict/__init__.pyi | 150 + multidict/_abc.py | 48 + multidict/_compat.py | 14 + multidict/_multidict.c | 1824 ++++ multidict/_multidict_base.py | 144 + multidict/_multidict_py.py | 526 + multidict/_multilib/defs.h | 22 + multidict/_multilib/dict.h | 24 + multidict/_multilib/istr.h | 85 + multidict/_multilib/iter.h | 238 + multidict/_multilib/pair_list.h | 1244 +++ multidict/_multilib/views.h | 464 + multidict/py.typed | 1 + mysql/__init__.py | 0 mysql/connector/__init__.py | 123 + mysql/connector/abstracts.py | 1806 ++++ mysql/connector/authentication.py | 77 + mysql/connector/charsets.py | 620 ++ mysql/connector/connection.py | 1741 ++++ mysql/connector/connection_cext.py | 1004 ++ mysql/connector/constants.py | 1148 +++ mysql/connector/conversion.py | 740 ++ mysql/connector/cursor.py | 1756 ++++ mysql/connector/cursor_cext.py | 1288 +++ mysql/connector/custom_types.py | 50 + mysql/connector/dbapi.py | 85 + mysql/connector/django/__init__.py | 0 mysql/connector/django/base.py | 636 ++ mysql/connector/django/client.py | 106 + mysql/connector/django/compiler.py | 45 + mysql/connector/django/creation.py | 33 + mysql/connector/django/features.py | 50 + mysql/connector/django/introspection.py | 461 + mysql/connector/django/operations.py | 104 + mysql/connector/django/schema.py | 59 + mysql/connector/django/validation.py | 33 + mysql/connector/errorcode.py | 1877 ++++ mysql/connector/errors.py | 336 + mysql/connector/locales/__init__.py | 80 + mysql/connector/locales/eng/__init__.py | 30 + mysql/connector/locales/eng/client_error.py | 152 + mysql/connector/logger.py | 33 + mysql/connector/network.py | 744 ++ mysql/connector/opentelemetry/__init__.py | 0 mysql/connector/opentelemetry/constants.py | 56 + .../opentelemetry/context_propagation.py | 92 + .../opentelemetry/instrumentation.py | 514 + mysql/connector/optionfiles.py | 357 + mysql/connector/plugins/__init__.py | 102 + .../plugins/authentication_kerberos_client.py | 462 + .../authentication_ldap_sasl_client.py | 496 + .../plugins/authentication_oci_client.py | 190 + .../plugins/caching_sha2_password.py | 101 + .../connector/plugins/mysql_clear_password.py | 40 + .../plugins/mysql_native_password.py | 74 + mysql/connector/plugins/sha256_password.py | 44 + mysql/connector/pooling.py | 622 ++ mysql/connector/protocol.py | 994 ++ mysql/connector/py.typed | 0 mysql/connector/types.py | 130 + mysql/connector/utils.py | 636 ++ mysql/connector/version.py | 46 + mysql/opentelemetry/__init__.py | 0 .../INSTALLER | 1 + .../METADATA | 45 + .../opentelemetry_api-1.18.0.dist-info/RECORD | 63 + .../opentelemetry_api-1.18.0.dist-info/WHEEL | 4 + .../entry_points.txt | 15 + .../INSTALLER | 1 + .../METADATA | 46 + .../opentelemetry_sdk-1.18.0.dist-info/RECORD | 86 + .../opentelemetry_sdk-1.18.0.dist-info/WHEEL | 4 + .../entry_points.txt | 35 + .../INSTALLER | 1 + .../METADATA | 59 + .../RECORD | 15 + .../WHEEL | 4 + mysql/opentelemetry/_logs/__init__.py | 60 + .../opentelemetry/_logs/_internal/__init__.py | 227 + .../opentelemetry/_logs/severity/__init__.py | 115 + mysql/opentelemetry/attributes/__init__.py | 191 + mysql/opentelemetry/baggage/__init__.py | 128 + .../baggage/propagation/__init__.py | 144 + mysql/opentelemetry/context/__init__.py | 170 + mysql/opentelemetry/context/context.py | 54 + .../context/contextvars_context.py | 51 + mysql/opentelemetry/environment_variables.py | 60 + .../importlib_metadata/__init__.py | 983 ++ .../importlib_metadata/_adapters.py | 89 + .../importlib_metadata/_collections.py | 30 + .../importlib_metadata/_compat.py | 81 + .../importlib_metadata/_functools.py | 104 + .../importlib_metadata/_itertools.py | 73 + .../opentelemetry/importlib_metadata/_meta.py | 63 + .../importlib_metadata/_py39compat.py | 35 + .../opentelemetry/importlib_metadata/_text.py | 99 + .../opentelemetry/importlib_metadata/py.typed | 0 mysql/opentelemetry/metrics/__init__.py | 126 + .../metrics/_internal/__init__.py | 759 ++ .../metrics/_internal/instrument.py | 389 + .../metrics/_internal/observation.py | 50 + mysql/opentelemetry/propagate/__init__.py | 163 + mysql/opentelemetry/propagators/__init__.py | 0 mysql/opentelemetry/propagators/composite.py | 88 + mysql/opentelemetry/propagators/textmap.py | 192 + mysql/opentelemetry/py.typed | 0 mysql/opentelemetry/sdk/__init__.py | 0 .../sdk/_configuration/__init__.py | 410 + mysql/opentelemetry/sdk/_logs/__init__.py | 32 + .../sdk/_logs/_internal/__init__.py | 470 + .../sdk/_logs/_internal/export/__init__.py | 455 + .../export/in_memory_log_exporter.py | 51 + .../sdk/_logs/export/__init__.py | 35 + .../sdk/environment_variables.py | 670 ++ .../sdk/error_handler/__init__.py | 140 + mysql/opentelemetry/sdk/metrics/__init__.py | 37 + .../sdk/metrics/_internal/__init__.py | 467 + .../_internal/_view_instrument_match.py | 130 + .../sdk/metrics/_internal/aggregation.py | 1034 ++ .../sdk/metrics/_internal/exceptions.py | 17 + .../exponential_histogram/__init__.py | 0 .../exponential_histogram/buckets.py | 170 + .../exponential_histogram/mapping/__init__.py | 96 + .../exponential_histogram/mapping/errors.py | 26 + .../mapping/exponent_mapping.py | 139 + .../exponential_histogram/mapping/ieee_754.py | 118 + .../mapping/logarithm_mapping.py | 132 + .../sdk/metrics/_internal/export/__init__.py | 525 + .../sdk/metrics/_internal/instrument.py | 224 + .../sdk/metrics/_internal/measurement.py | 30 + .../metrics/_internal/measurement_consumer.py | 120 + .../_internal/metric_reader_storage.py | 299 + .../sdk/metrics/_internal/point.py | 258 + .../metrics/_internal/sdk_configuration.py | 29 + .../sdk/metrics/_internal/view.py | 155 + .../sdk/metrics/export/__init__.py | 63 + .../sdk/metrics/view/__init__.py | 35 + mysql/opentelemetry/sdk/py.typed | 0 mysql/opentelemetry/sdk/resources/__init__.py | 378 + mysql/opentelemetry/sdk/trace/__init__.py | 1206 +++ .../sdk/trace/export/__init__.py | 506 + .../trace/export/in_memory_span_exporter.py | 61 + mysql/opentelemetry/sdk/trace/id_generator.py | 52 + mysql/opentelemetry/sdk/trace/sampling.py | 447 + mysql/opentelemetry/sdk/util/__init__.py | 144 + .../opentelemetry/sdk/util/instrumentation.py | 147 + mysql/opentelemetry/sdk/version.py | 15 + mysql/opentelemetry/semconv/__init__.py | 0 .../opentelemetry/semconv/metrics/__init__.py | 33 + .../semconv/resource/__init__.py | 657 ++ mysql/opentelemetry/semconv/trace/__init__.py | 1254 +++ mysql/opentelemetry/semconv/version.py | 15 + mysql/opentelemetry/trace/__init__.py | 623 ++ .../trace/propagation/__init__.py | 49 + .../trace/propagation/tracecontext.py | 114 + mysql/opentelemetry/trace/span.py | 563 ++ mysql/opentelemetry/trace/status.py | 82 + mysql/opentelemetry/util/__init__.py | 0 .../opentelemetry/util/_importlib_metadata.py | 35 + mysql/opentelemetry/util/_once.py | 47 + mysql/opentelemetry/util/_providers.py | 50 + mysql/opentelemetry/util/re.py | 76 + mysql/opentelemetry/util/types.py | 44 + mysql/opentelemetry/version.py | 15 + paho/__init__.py | 0 paho/mqtt/__init__.py | 5 + paho/mqtt/client.py | 5004 ++++++++++ paho/mqtt/enums.py | 113 + paho/mqtt/matcher.py | 78 + paho/mqtt/packettypes.py | 43 + paho/mqtt/properties.py | 421 + paho/mqtt/publish.py | 306 + paho/mqtt/py.typed | 0 paho/mqtt/reasoncodes.py | 223 + paho/mqtt/subscribe.py | 281 + paho/mqtt/subscribeoptions.py | 113 + phaoUtils/.gitignore | 66 + phaoUtils/CONTRIBUTING.md | 114 + phaoUtils/LICENSE.txt | 3 + phaoUtils/PKG-INFO | 635 ++ phaoUtils/README.rst | 606 ++ phaoUtils/about.html | 41 + phaoUtils/edl-v10 | 31 + phaoUtils/epl-v20 | 277 + phaoUtils/examples/aws_iot.py | 129 + phaoUtils/examples/client_logger.py | 38 + .../examples/client_mqtt_clear_retain.py | 120 + phaoUtils/examples/client_pub-wait.py | 60 + phaoUtils/examples/client_pub_opts.py | 123 + phaoUtils/examples/client_rpc_math.py | 114 + phaoUtils/examples/client_session_present.py | 62 + phaoUtils/examples/client_sub-class.py | 60 + .../examples/client_sub-multiple-callback.py | 53 + phaoUtils/examples/client_sub-srv.py | 55 + phaoUtils/examples/client_sub-ws.py | 50 + phaoUtils/examples/client_sub.py | 54 + phaoUtils/examples/client_sub_opts.py | 115 + phaoUtils/examples/context.py | 28 + phaoUtils/examples/loop_asyncio.py | 111 + phaoUtils/examples/loop_select.py | 90 + phaoUtils/examples/loop_trio.py | 96 + phaoUtils/examples/publish_multiple.py | 23 + phaoUtils/examples/publish_single.py | 22 + phaoUtils/examples/publish_utf8-27.py | 24 + phaoUtils/examples/publish_utf8-3.py | 24 + phaoUtils/examples/server_rpc_math.py | 96 + phaoUtils/examples/subscribe_callback.py | 26 + phaoUtils/examples/subscribe_simple.py | 27 + phaoUtils/notice.html | 108 + phaoUtils/pyproject.toml | 146 + phaoUtils/tests/__init__.py | 0 phaoUtils/tests/consts.py | 5 + phaoUtils/tests/debug_helpers.py | 223 + phaoUtils/tests/lib/__init__.py | 0 phaoUtils/tests/lib/clients/01-asyncio.py | 89 + phaoUtils/tests/lib/clients/01-decorators.py | 42 + .../tests/lib/clients/01-keepalive-pingreq.py | 14 + .../tests/lib/clients/01-no-clean-session.py | 8 + .../lib/clients/01-reconnect-on-failure.py | 16 + .../clients/01-unpwd-empty-password-set.py | 9 + .../tests/lib/clients/01-unpwd-empty-set.py | 9 + phaoUtils/tests/lib/clients/01-unpwd-set.py | 9 + .../tests/lib/clients/01-unpwd-unicode-set.py | 12 + phaoUtils/tests/lib/clients/01-will-set.py | 9 + .../tests/lib/clients/01-will-unpwd-set.py | 10 + .../lib/clients/01-zero-length-clientid.py | 20 + .../tests/lib/clients/02-subscribe-qos0.py | 20 + .../tests/lib/clients/02-subscribe-qos1.py | 20 + .../tests/lib/clients/02-subscribe-qos2.py | 20 + phaoUtils/tests/lib/clients/02-unsubscribe.py | 20 + .../tests/lib/clients/03-publish-b2c-qos1.py | 25 + .../tests/lib/clients/03-publish-b2c-qos2.py | 28 + .../clients/03-publish-c2b-qos1-disconnect.py | 33 + .../clients/03-publish-c2b-qos2-disconnect.py | 31 + .../lib/clients/03-publish-fill-inflight.py | 39 + .../lib/clients/03-publish-helper-qos0-v5.py | 15 + .../lib/clients/03-publish-helper-qos0.py | 13 + .../03-publish-helper-qos1-disconnect.py | 13 + .../lib/clients/03-publish-qos0-no-payload.py | 24 + .../tests/lib/clients/03-publish-qos0.py | 26 + phaoUtils/tests/lib/clients/04-retain-qos0.py | 15 + .../tests/lib/clients/08-ssl-connect-alpn.py | 23 + .../clients/08-ssl-connect-cert-auth-pw.py | 23 + .../lib/clients/08-ssl-connect-cert-auth.py | 22 + .../lib/clients/08-ssl-connect-no-auth.py | 18 + .../tests/lib/clients/08-ssl-fake-cacert.py | 27 + phaoUtils/tests/lib/conftest.py | 80 + phaoUtils/tests/lib/test_01_asyncio.py | 45 + phaoUtils/tests/lib/test_01_decorators.py | 44 + .../tests/lib/test_01_keepalive_pingreq.py | 32 + .../tests/lib/test_01_no_clean_session.py | 20 + .../tests/lib/test_01_reconnect_on_failure.py | 31 + .../lib/test_01_unpwd_empty_password_set.py | 21 + .../tests/lib/test_01_unpwd_empty_set.py | 21 + phaoUtils/tests/lib/test_01_unpwd_set.py | 21 + .../tests/lib/test_01_unpwd_unicode_set.py | 25 + phaoUtils/tests/lib/test_01_will_set.py | 21 + phaoUtils/tests/lib/test_01_will_unpwd_set.py | 26 + .../tests/lib/test_01_zero_length_clientid.py | 23 + phaoUtils/tests/lib/test_02_subscribe_qos0.py | 40 + phaoUtils/tests/lib/test_02_subscribe_qos1.py | 40 + phaoUtils/tests/lib/test_02_subscribe_qos2.py | 40 + phaoUtils/tests/lib/test_02_unsubscribe.py | 30 + .../tests/lib/test_03_publish_b2c_qos1.py | 36 + .../tests/lib/test_03_publish_b2c_qos2.py | 41 + .../test_03_publish_c2b_qos1_disconnect.py | 45 + .../test_03_publish_c2b_qos2_disconnect.py | 61 + .../lib/test_03_publish_fill_inflight.py | 90 + .../tests/lib/test_03_publish_helper_qos0.py | 40 + .../lib/test_03_publish_helper_qos0_v5.py | 40 + .../test_03_publish_helper_qos1_disconnect.py | 50 + phaoUtils/tests/lib/test_03_publish_qos0.py | 33 + .../lib/test_03_publish_qos0_no_payload.py | 34 + phaoUtils/tests/lib/test_04_retain_qos0.py | 25 + phaoUtils/tests/lib/test_08_ssl_bad_cacert.py | 8 + .../tests/lib/test_08_ssl_connect_alpn.py | 38 + .../lib/test_08_ssl_connect_cert_auth.py | 29 + .../lib/test_08_ssl_connect_cert_auth_pw.py | 29 + .../tests/lib/test_08_ssl_connect_no_auth.py | 26 + .../tests/lib/test_08_ssl_fake_cacert.py | 10 + phaoUtils/tests/mqtt5_props.py | 76 + phaoUtils/tests/paho_test.py | 444 + phaoUtils/tests/ssl/all-ca.crt | 101 + phaoUtils/tests/ssl/client-expired.crt | 82 + phaoUtils/tests/ssl/client-pw.crt | 82 + phaoUtils/tests/ssl/client-pw.key | 30 + phaoUtils/tests/ssl/client-revoked.crt | 82 + phaoUtils/tests/ssl/client-revoked.key | 27 + phaoUtils/tests/ssl/client.crt | 82 + phaoUtils/tests/ssl/client.key | 27 + phaoUtils/tests/ssl/crl.pem | 12 + phaoUtils/tests/ssl/gen.sh | 82 + phaoUtils/tests/ssl/openssl.cnf | 406 + phaoUtils/tests/ssl/server-expired.crt | 82 + phaoUtils/tests/ssl/server.crt | 82 + phaoUtils/tests/ssl/server.key | 27 + phaoUtils/tests/ssl/test-alt-ca.crt | 79 + phaoUtils/tests/ssl/test-alt-ca.key | 27 + phaoUtils/tests/ssl/test-bad-root-ca.crt | 23 + phaoUtils/tests/ssl/test-bad-root-ca.key | 27 + phaoUtils/tests/ssl/test-ca.srl | 1 + phaoUtils/tests/ssl/test-fake-root-ca.crt | 22 + phaoUtils/tests/ssl/test-fake-root-ca.key | 27 + phaoUtils/tests/ssl/test-root-ca.crt | 22 + phaoUtils/tests/ssl/test-root-ca.key | 27 + phaoUtils/tests/ssl/test-signing-ca.crt | 79 + phaoUtils/tests/ssl/test-signing-ca.key | 27 + phaoUtils/tests/test_client.py | 1021 ++ phaoUtils/tests/test_matcher.py | 36 + phaoUtils/tests/test_mqttv5.py | 1410 +++ phaoUtils/tests/test_reasoncodes.py | 47 + phaoUtils/tests/test_websocket_integration.py | 257 + phaoUtils/tests/test_websockets.py | 142 + phaoUtils/tests/testsupport/__init__.py | 0 phaoUtils/tests/testsupport/broker.py | 130 + pyserial-master.zip | Bin 0 -> 202391 bytes pyserial-master/pyserial-master/.gitignore | 13 + pyserial-master/pyserial-master/.travis.yml | 16 + pyserial-master/pyserial-master/CHANGES.rst | 825 ++ pyserial-master/pyserial-master/LICENSE.txt | 39 + pyserial-master/pyserial-master/MANIFEST.in | 39 + pyserial-master/pyserial-master/README.rst | 56 + .../pyserial-master/documentation/Makefile | 88 + .../documentation/appendix.rst | 148 + .../pyserial-master/documentation/conf.py | 200 + .../documentation/examples.rst | 274 + .../pyserial-master/documentation/index.rst | 44 + .../documentation/pyserial.png | Bin 0 -> 7050 bytes .../documentation/pyserial.rst | 145 + .../documentation/pyserial_api.rst | 1309 +++ .../documentation/shortintro.rst | 123 + .../pyserial-master/documentation/tools.rst | 291 + .../documentation/url_handlers.rst | 274 + .../pyserial-master/examples/at_protocol.py | 154 + .../examples/port_publisher.py | 578 ++ .../examples/port_publisher.sh | 44 + .../examples/rfc2217_server.py | 188 + .../examples/setup-miniterm-py2exe.py | 33 + .../examples/setup-rfc2217_server-py2exe.py | 31 + .../examples/setup-wxTerminal-py2exe.py | 41 + .../examples/tcp_serial_redirect.py | 230 + .../examples/wxSerialConfigDialog.py | 293 + .../examples/wxSerialConfigDialog.wxg | 252 + .../pyserial-master/examples/wxTerminal.py | 367 + .../pyserial-master/examples/wxTerminal.wxg | 154 + pyserial-master/pyserial-master/pylintrc | 378 + .../pyserial-master/requirements.txt | 0 pyserial-master/pyserial-master/setup.cfg | 7 + pyserial-master/pyserial-master/setup.py | 108 + .../pyserial-master/test/handlers/__init__.py | 0 .../test/handlers/protocol_test.py | 202 + .../pyserial-master/test/run_all_tests.py | 55 + pyserial-master/pyserial-master/test/test.py | 233 + .../pyserial-master/test/test_advanced.py | 161 + .../pyserial-master/test/test_asyncio.py | 82 + .../pyserial-master/test/test_cancel.py | 109 + .../pyserial-master/test/test_close.py | 58 + .../pyserial-master/test/test_context.py | 49 + .../pyserial-master/test/test_exclusive.py | 59 + .../pyserial-master/test/test_high_load.py | 76 + .../pyserial-master/test/test_iolib.py | 61 + .../pyserial-master/test/test_pty.py | 54 + .../pyserial-master/test/test_readline.py | 104 + .../pyserial-master/test/test_rfc2217.py | 42 + .../pyserial-master/test/test_rs485.py | 67 + .../test/test_settings_dict.py | 80 + .../pyserial-master/test/test_threaded.py | 75 + .../test/test_timeout_class.py | 66 + .../pyserial-master/test/test_url.py | 51 + .../pyserial-master/test/test_util.py | 36 + serial/__init__.py | 91 + serial/__main__.py | 3 + serial/rfc2217.py | 1351 +++ serial/rs485.py | 94 + serial/serialcli.py | 253 + serial/serialjava.py | 251 + serial/serialposix.py | 907 ++ serial/serialutil.py | 707 ++ serial/serialwin32.py | 477 + serial/threaded/__init__.py | 297 + serial/tools/__init__.py | 0 serial/tools/hexlify_codec.py | 126 + serial/tools/list_ports.py | 110 + serial/tools/list_ports_common.py | 121 + serial/tools/list_ports_linux.py | 112 + serial/tools/list_ports_osx.py | 299 + serial/tools/list_ports_posix.py | 119 + serial/tools/list_ports_windows.py | 427 + serial/tools/miniterm.py | 1071 ++ serial/urlhandler/__init__.py | 0 serial/urlhandler/protocol_alt.py | 57 + serial/urlhandler/protocol_cp2110.py | 258 + serial/urlhandler/protocol_hwgrep.py | 91 + serial/urlhandler/protocol_loop.py | 308 + serial/urlhandler/protocol_rfc2217.py | 12 + serial/urlhandler/protocol_socket.py | 359 + serial/urlhandler/protocol_spy.py | 337 + serial/win32.py | 366 + six.py | 1003 ++ skoda_test.py | 258 + skoda_testantwort.json | 50 + skoda_testdaten.py | 183 + solarManager.py | 621 ++ startMQTTbridge.sh | 19 + startSolarServer.sh | 53 + startWattpilotMQTT.sh | 45 + startWecker.sh | 19 + sunspec2/__init__.py | 2 + sunspec2/device.py | 828 ++ sunspec2/docs/pysunspec.rst | 392 + sunspec2/file/__init__.py | 0 sunspec2/file/client.py | 100 + sunspec2/mb.py | 341 + sunspec2/mdef.py | 454 + sunspec2/modbus/__init__.py | 0 sunspec2/modbus/client.py | 435 + sunspec2/modbus/modbus.py | 403 + sunspec2/models/.clabot | 3 + sunspec2/models/.gitattributes | 22 + sunspec2/models/.gitignore | 217 + sunspec2/models/.travis.yml | 16 + sunspec2/models/LICENSE | 201 + sunspec2/models/README.md | 5 + sunspec2/models/json/Makefile | 11 + sunspec2/models/json/model_1.json | 110 + sunspec2/models/json/model_10.json | 94 + sunspec2/models/json/model_101.json | 469 + sunspec2/models/json/model_102.json | 471 + sunspec2/models/json/model_103.json | 473 + sunspec2/models/json/model_11.json | 145 + sunspec2/models/json/model_111.json | 380 + sunspec2/models/json/model_112.json | 382 + sunspec2/models/json/model_113.json | 384 + sunspec2/models/json/model_12.json | 241 + sunspec2/models/json/model_120.json | 269 + sunspec2/models/json/model_121.json | 334 + sunspec2/models/json/model_122.json | 350 + sunspec2/models/json/model_123.json | 306 + sunspec2/models/json/model_124.json | 287 + sunspec2/models/json/model_125.json | 125 + sunspec2/models/json/model_126.json | 596 ++ sunspec2/models/json/model_127.json | 129 + sunspec2/models/json/model_128.json | 173 + sunspec2/models/json/model_129.json | 564 ++ sunspec2/models/json/model_13.json | 240 + sunspec2/models/json/model_130.json | 564 ++ sunspec2/models/json/model_131.json | 600 ++ sunspec2/models/json/model_132.json | 614 ++ sunspec2/models/json/model_133.json | 615 ++ sunspec2/models/json/model_134.json | 647 ++ sunspec2/models/json/model_135.json | 564 ++ sunspec2/models/json/model_136.json | 564 ++ sunspec2/models/json/model_137.json | 564 ++ sunspec2/models/json/model_138.json | 564 ++ sunspec2/models/json/model_139.json | 571 ++ sunspec2/models/json/model_14.json | 113 + sunspec2/models/json/model_140.json | 571 ++ sunspec2/models/json/model_141.json | 564 ++ sunspec2/models/json/model_142.json | 564 ++ sunspec2/models/json/model_143.json | 571 ++ sunspec2/models/json/model_144.json | 571 ++ sunspec2/models/json/model_145.json | 107 + sunspec2/models/json/model_15.json | 120 + sunspec2/models/json/model_16.json | 159 + sunspec2/models/json/model_160.json | 380 + sunspec2/models/json/model_17.json | 162 + sunspec2/models/json/model_18.json | 70 + sunspec2/models/json/model_19.json | 160 + sunspec2/models/json/model_2.json | 231 + sunspec2/models/json/model_201.json | 707 ++ sunspec2/models/json/model_202.json | 744 ++ sunspec2/models/json/model_203.json | 748 ++ sunspec2/models/json/model_204.json | 744 ++ sunspec2/models/json/model_211.json | 613 ++ sunspec2/models/json/model_212.json | 619 ++ sunspec2/models/json/model_213.json | 623 ++ sunspec2/models/json/model_214.json | 619 ++ sunspec2/models/json/model_220.json | 403 + sunspec2/models/json/model_3.json | 474 + sunspec2/models/json/model_302.json | 79 + sunspec2/models/json/model_303.json | 49 + sunspec2/models/json/model_304.json | 67 + sunspec2/models/json/model_305.json | 80 + sunspec2/models/json/model_306.json | 62 + sunspec2/models/json/model_307.json | 108 + sunspec2/models/json/model_308.json | 62 + sunspec2/models/json/model_4.json | 463 + sunspec2/models/json/model_401.json | 325 + sunspec2/models/json/model_402.json | 403 + sunspec2/models/json/model_403.json | 337 + sunspec2/models/json/model_404.json | 435 + sunspec2/models/json/model_5.json | 689 ++ sunspec2/models/json/model_501.json | 292 + sunspec2/models/json/model_502.json | 324 + sunspec2/models/json/model_6.json | 703 ++ sunspec2/models/json/model_601.json | 292 + sunspec2/models/json/model_63001.json | 421 + sunspec2/models/json/model_63002.json | 61 + sunspec2/models/json/model_64001.json | 241 + sunspec2/models/json/model_64020.json | 300 + sunspec2/models/json/model_64101.json | 64 + sunspec2/models/json/model_64111.json | 239 + sunspec2/models/json/model_64112.json | 688 ++ sunspec2/models/json/model_7.json | 165 + sunspec2/models/json/model_701.json | 1021 ++ sunspec2/models/json/model_702.json | 742 ++ sunspec2/models/json/model_703.json | 173 + sunspec2/models/json/model_704.json | 899 ++ sunspec2/models/json/model_705.json | 425 + sunspec2/models/json/model_706.json | 322 + sunspec2/models/json/model_707.json | 356 + sunspec2/models/json/model_708.json | 356 + sunspec2/models/json/model_709.json | 356 + sunspec2/models/json/model_710.json | 356 + sunspec2/models/json/model_711.json | 299 + sunspec2/models/json/model_712.json | 336 + sunspec2/models/json/model_713.json | 126 + sunspec2/models/json/model_714.json | 356 + sunspec2/models/json/model_715.json | 107 + sunspec2/models/json/model_8.json | 77 + sunspec2/models/json/model_801.json | 38 + sunspec2/models/json/model_802.json | 767 ++ sunspec2/models/json/model_803.json | 789 ++ sunspec2/models/json/model_804.json | 795 ++ sunspec2/models/json/model_805.json | 280 + sunspec2/models/json/model_806.json | 52 + sunspec2/models/json/model_807.json | 953 ++ sunspec2/models/json/model_808.json | 52 + sunspec2/models/json/model_809.json | 52 + sunspec2/models/json/model_9.json | 763 ++ sunspec2/models/json/schema.json | 187 + sunspec2/models/smdx/CHANGELOG | 71 + sunspec2/models/smdx/Makefile | 51 + sunspec2/models/smdx/manifest.py | 183 + sunspec2/models/smdx/manifest.xml | 96 + sunspec2/models/smdx/manifest.xml.md5 | 1 + sunspec2/models/smdx/smdx.xsd | 172 + sunspec2/models/smdx/smdx_00001.xml | 56 + sunspec2/models/smdx/smdx_00002.xml | 107 + sunspec2/models/smdx/smdx_00003.xml | 138 + sunspec2/models/smdx/smdx_00004.xml | 188 + sunspec2/models/smdx/smdx_00005.xml | 173 + sunspec2/models/smdx/smdx_00006.xml | 175 + sunspec2/models/smdx/smdx_00007.xml | 134 + sunspec2/models/smdx/smdx_00008.xml | 37 + sunspec2/models/smdx/smdx_00009.xml | 210 + sunspec2/models/smdx/smdx_00010.xml | 83 + sunspec2/models/smdx/smdx_00011.xml | 137 + sunspec2/models/smdx/smdx_00012.xml | 218 + sunspec2/models/smdx/smdx_00013.xml | 218 + sunspec2/models/smdx/smdx_00014.xml | 81 + sunspec2/models/smdx/smdx_00015.xml | 87 + sunspec2/models/smdx/smdx_00016.xml | 132 + sunspec2/models/smdx/smdx_00017.xml | 151 + sunspec2/models/smdx/smdx_00018.xml | 44 + sunspec2/models/smdx/smdx_00019.xml | 139 + sunspec2/models/smdx/smdx_00101.xml | 359 + sunspec2/models/smdx/smdx_00102.xml | 358 + sunspec2/models/smdx/smdx_00103.xml | 358 + sunspec2/models/smdx/smdx_00111.xml | 346 + sunspec2/models/smdx/smdx_00112.xml | 346 + sunspec2/models/smdx/smdx_00113.xml | 346 + sunspec2/models/smdx/smdx_00120.xml | 173 + sunspec2/models/smdx/smdx_00121.xml | 204 + sunspec2/models/smdx/smdx_00122.xml | 178 + sunspec2/models/smdx/smdx_00123.xml | 187 + sunspec2/models/smdx/smdx_00124.xml | 167 + sunspec2/models/smdx/smdx_00125.xml | 80 + sunspec2/models/smdx/smdx_00126.xml | 362 + sunspec2/models/smdx/smdx_00127.xml | 73 + sunspec2/models/smdx/smdx_00128.xml | 98 + sunspec2/models/smdx/smdx_00129.xml | 334 + sunspec2/models/smdx/smdx_00130.xml | 334 + sunspec2/models/smdx/smdx_00131.xml | 358 + sunspec2/models/smdx/smdx_00132.xml | 371 + sunspec2/models/smdx/smdx_00133.xml | 302 + sunspec2/models/smdx/smdx_00134.xml | 387 + sunspec2/models/smdx/smdx_00135.xml | 334 + sunspec2/models/smdx/smdx_00136.xml | 334 + sunspec2/models/smdx/smdx_00137.xml | 334 + sunspec2/models/smdx/smdx_00138.xml | 334 + sunspec2/models/smdx/smdx_00139.xml | 336 + sunspec2/models/smdx/smdx_00140.xml | 336 + sunspec2/models/smdx/smdx_00141.xml | 334 + sunspec2/models/smdx/smdx_00142.xml | 334 + sunspec2/models/smdx/smdx_00143.xml | 336 + sunspec2/models/smdx/smdx_00144.xml | 336 + sunspec2/models/smdx/smdx_00145.xml | 62 + sunspec2/models/smdx/smdx_00160.xml | 459 + sunspec2/models/smdx/smdx_00201.xml | 538 ++ sunspec2/models/smdx/smdx_00202.xml | 546 ++ sunspec2/models/smdx/smdx_00203.xml | 546 ++ sunspec2/models/smdx/smdx_00204.xml | 546 ++ sunspec2/models/smdx/smdx_00211.xml | 526 + sunspec2/models/smdx/smdx_00212.xml | 526 + sunspec2/models/smdx/smdx_00213.xml | 526 + sunspec2/models/smdx/smdx_00214.xml | 526 + sunspec2/models/smdx/smdx_00220.xml | 315 + sunspec2/models/smdx/smdx_00302.xml | 45 + sunspec2/models/smdx/smdx_00303.xml | 22 + sunspec2/models/smdx/smdx_00304.xml | 32 + sunspec2/models/smdx/smdx_00305.xml | 50 + sunspec2/models/smdx/smdx_00306.xml | 39 + sunspec2/models/smdx/smdx_00307.xml | 80 + sunspec2/models/smdx/smdx_00308.xml | 40 + sunspec2/models/smdx/smdx_00401.xml | 140 + sunspec2/models/smdx/smdx_00402.xml | 187 + sunspec2/models/smdx/smdx_00403.xml | 144 + sunspec2/models/smdx/smdx_00404.xml | 202 + sunspec2/models/smdx/smdx_00501.xml | 304 + sunspec2/models/smdx/smdx_00502.xml | 328 + sunspec2/models/smdx/smdx_00601.xml | 105 + sunspec2/models/smdx/smdx_00801.xml | 20 + sunspec2/models/smdx/smdx_00802.xml | 755 ++ sunspec2/models/smdx/smdx_00803.xml | 822 ++ sunspec2/models/smdx/smdx_00804.xml | 805 ++ sunspec2/models/smdx/smdx_00805.xml | 204 + sunspec2/models/smdx/smdx_00806.xml | 30 + sunspec2/models/smdx/smdx_00807.xml | 1064 ++ sunspec2/models/smdx/smdx_00808.xml | 30 + sunspec2/models/smdx/smdx_00809.xml | 30 + sunspec2/models/smdx/smdx_63001.xml | 84 + sunspec2/models/smdx/smdx_63002.xml | 16 + sunspec2/models/smdx/smdx_64001.xml | 220 + sunspec2/models/smdx/smdx_64020.xml | 215 + sunspec2/models/smdx/smdx_64101.xml | 22 + sunspec2/models/smdx/smdx_64111.xml | 159 + sunspec2/models/smdx/smdx_64112.xml | 571 ++ sunspec2/models/utils/add_sunspec_comments.py | 128 + sunspec2/models/utils/add_sunspec_detail.py | 117 + .../models/utils/add_sunspec_standards.py | 140 + sunspec2/smdx.py | 393 + sunspec2/spreadsheet.py | 577 ++ sunspec2/tests/__init__.py | 0 sunspec2/tests/mock_port.py | 45 + sunspec2/tests/mock_socket.py | 52 + sunspec2/tests/test_data/__init__.py | 0 sunspec2/tests/test_data/device_1547.json | 612 ++ sunspec2/tests/test_data/inverter_123.json | 213 + sunspec2/tests/test_data/smdx_304.csv | 8 + sunspec2/tests/test_data/wb_701-705.xlsx | Bin 0 -> 45097 bytes sunspec2/tests/test_device.py | 3336 +++++++ sunspec2/tests/test_file_client.py | 2932 ++++++ sunspec2/tests/test_mb.py | 221 + sunspec2/tests/test_mdef.py | 313 + sunspec2/tests/test_modbus_client.py | 1503 +++ sunspec2/tests/test_modbus_modbus.py | 223 + sunspec2/tests/test_smdx.py | 217 + sunspec2/tests/test_spreadsheet.py | 526 + sunspec2/tests/test_xlsx.py | 210 + sunspec2/xlsx.py | 413 + suntime/__init__.py | 5 + suntime/suntime.py | 153 + webSocketServer.py | 33 + websocket/__init__.py | 26 + websocket/_abnf.py | 424 + websocket/_app.py | 429 + websocket/_cookiejar.py | 64 + websocket/_core.py | 602 ++ websocket/_exceptions.py | 80 + websocket/_handshake.py | 195 + websocket/_http.py | 336 + websocket/_logging.py | 87 + websocket/_socket.py | 179 + websocket/_ssl_compat.py | 39 + websocket/_url.py | 172 + websocket/_utils.py | 104 + websocket/_wsdump.py | 231 + websocket/data/WebSocketMain.swf | Bin 0 -> 180224 bytes websocket/data/__init__.py | 0 websocket/data/flashsocket.js | 998 ++ websocket/policyserver.py | 22 + websocket/server.py | 134 + websocket/tests/__init__.py | 0 websocket/tests/data/header01.txt | 6 + websocket/tests/data/header02.txt | 6 + websocket/tests/data/header03.txt | 8 + websocket/tests/echo-server.py | 21 + websocket/tests/test_abnf.py | 89 + websocket/tests/test_app.py | 230 + websocket/tests/test_cookiejar.py | 116 + websocket/tests/test_http.py | 176 + websocket/tests/test_url.py | 301 + websocket/tests/test_websocket.py | 455 + websockets/__init__.py | 114 + websockets/__main__.py | 159 + websockets/auth.py | 4 + websockets/client.py | 358 + websockets/connection.py | 13 + websockets/datastructures.py | 194 + websockets/exceptions.py | 404 + websockets/extensions/__init__.py | 4 + websockets/extensions/base.py | 133 + websockets/extensions/permessage_deflate.py | 660 ++ websockets/frames.py | 470 + websockets/headers.py | 587 ++ websockets/http.py | 30 + websockets/http11.py | 364 + websockets/imports.py | 99 + websockets/legacy/__init__.py | 0 websockets/legacy/async_timeout.py | 265 + websockets/legacy/auth.py | 184 + websockets/legacy/client.py | 705 ++ websockets/legacy/compatibility.py | 12 + websockets/legacy/framing.py | 176 + websockets/legacy/handshake.py | 165 + websockets/legacy/http.py | 201 + websockets/legacy/protocol.py | 1645 ++++ websockets/legacy/server.py | 1185 +++ websockets/protocol.py | 708 ++ websockets/py.typed | 0 websockets/server.py | 577 ++ websockets/speedups.c | 223 + websockets/speedups.pyi | 1 + websockets/streams.py | 151 + websockets/sync/__init__.py | 0 websockets/sync/client.py | 328 + websockets/sync/connection.py | 773 ++ websockets/sync/messages.py | 281 + websockets/sync/server.py | 530 + websockets/sync/utils.py | 46 + websockets/typing.py | 60 + websockets/uri.py | 108 + websockets/utils.py | 51 + websockets/version.py | 82 + wecker.py | 129 + wsMQTTbridge.py | 139 + yarl/__init__.py | 5 + yarl/__init__.pyi | 121 + yarl/_quoting.py | 18 + yarl/_quoting_c.pyi | 16 + yarl/_quoting_c.pyx | 371 + yarl/_quoting_py.py | 197 + yarl/_url.py | 1198 +++ yarl/py.typed | 1 + zeit.py | 98 + 968 files changed, 261182 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Saldierung.txt create mode 100644 Wattpilot/Wattpilot.py create mode 100644 aiohttp/__init__.py create mode 100644 aiohttp/_cparser.pxd create mode 100644 aiohttp/_find_header.h create mode 100644 aiohttp/_find_header.pxd create mode 100644 aiohttp/_helpers.pyi create mode 100644 aiohttp/_helpers.pyx create mode 100644 aiohttp/_http_parser.pyx create mode 100644 aiohttp/_http_writer.pyx create mode 100644 aiohttp/_websocket.pyx create mode 100644 aiohttp/abc.py create mode 100644 aiohttp/base_protocol.py create mode 100644 aiohttp/client.py create mode 100644 aiohttp/client_exceptions.py create mode 100644 aiohttp/client_proto.py create mode 100644 aiohttp/client_reqrep.py create mode 100644 aiohttp/client_ws.py create mode 100644 aiohttp/compression_utils.py create mode 100644 aiohttp/connector.py create mode 100644 aiohttp/cookiejar.py create mode 100644 aiohttp/formdata.py create mode 100644 aiohttp/hdrs.py create mode 100644 aiohttp/helpers.py create mode 100644 aiohttp/http.py create mode 100644 aiohttp/http_exceptions.py create mode 100644 aiohttp/http_parser.py create mode 100644 aiohttp/http_websocket.py create mode 100644 aiohttp/http_writer.py create mode 100644 aiohttp/locks.py create mode 100644 aiohttp/log.py create mode 100644 aiohttp/multipart.py create mode 100644 aiohttp/payload.py create mode 100644 aiohttp/py.typed create mode 100644 aiohttp/pytest_plugin.py create mode 100644 aiohttp/resolver.py create mode 100644 aiohttp/streams.py create mode 100644 aiohttp/tcp_helpers.py create mode 100644 aiohttp/test_utils.py create mode 100644 aiohttp/tracing.py create mode 100644 aiohttp/typedefs.py create mode 100644 aiohttp/web.py create mode 100644 aiohttp/web_app.py create mode 100644 aiohttp/web_exceptions.py create mode 100644 aiohttp/web_fileresponse.py create mode 100644 aiohttp/web_log.py create mode 100644 aiohttp/web_middlewares.py create mode 100644 aiohttp/web_protocol.py create mode 100644 aiohttp/web_request.py create mode 100644 aiohttp/web_response.py create mode 100644 aiohttp/web_routedef.py create mode 100644 aiohttp/web_runner.py create mode 100644 aiohttp/web_server.py create mode 100644 aiohttp/web_urldispatcher.py create mode 100644 aiohttp/web_ws.py create mode 100644 aiohttp/worker.py create mode 100644 aiosignal/__init__.py create mode 100644 aiosignal/__init__.pyi create mode 100644 aiosignal/py.typed create mode 100644 async_timeout/__init__.py create mode 100644 async_timeout/py.typed create mode 100644 autoActions/README.md create mode 100644 autoActions/autoaction_runner.py create mode 100644 autoActions/config.ini.example create mode 100644 autoActions/fetch_calendar.py create mode 100644 autoActions/transports.py create mode 100644 charger_goE.py create mode 100644 config.ini.example create mode 100644 crcmod/__init__.py create mode 100644 crcmod/_crcfunpy.py create mode 100644 crcmod/crcmod.py create mode 100644 crcmod/predefined.py create mode 100644 crcmod/test.py create mode 100644 dateutil/__init__.py create mode 100644 dateutil/_common.py create mode 100644 dateutil/_version.py create mode 100644 dateutil/easter.py create mode 100644 dateutil/parser/__init__.py create mode 100644 dateutil/parser/_parser.py create mode 100644 dateutil/parser/isoparser.py create mode 100644 dateutil/relativedelta.py create mode 100644 dateutil/rrule.py create mode 100644 dateutil/tz/__init__.py create mode 100644 dateutil/tz/_common.py create mode 100644 dateutil/tz/_factories.py create mode 100644 dateutil/tz/tz.py create mode 100644 dateutil/tz/win.py create mode 100644 dateutil/tzwin.py create mode 100644 dateutil/utils.py create mode 100644 dateutil/zoneinfo/__init__.py create mode 100644 dateutil/zoneinfo/dateutil-zoneinfo.tar.gz create mode 100644 dateutil/zoneinfo/rebuild.py create mode 100644 frozenlist/__init__.py create mode 100644 frozenlist/__init__.pyi create mode 100644 frozenlist/_frozenlist.pyx create mode 100644 frozenlist/py.typed create mode 100644 gatherDTUBIData.py create mode 100644 gatherHeaterData.py create mode 100644 gatherModbusData.py create mode 100644 gatherOpenDTUData.py create mode 100644 gatherShellyEM3DataEG.py create mode 100644 gatherShellyEM3DataUG.py create mode 100644 gatherSkodaData.py create mode 100644 gatherWaterData.py create mode 100644 goodwe/__init__.py create mode 100644 goodwe/const.py create mode 100644 goodwe/dt.py create mode 100644 goodwe/es.py create mode 100644 goodwe/et.py create mode 100644 goodwe/exceptions.py create mode 100644 goodwe/goodwe.py create mode 100644 goodwe/inverter.py create mode 100644 goodwe/modbus.py create mode 100644 goodwe/model.py create mode 100644 goodwe/processor.py create mode 100644 goodwe/protocol.py create mode 100644 goodwe/sensor.py create mode 100644 goodwe/xs.py create mode 100644 google/__init__.py create mode 100644 google/protobuf/__init__.py create mode 100644 google/protobuf/any_pb2.py create mode 100644 google/protobuf/api_pb2.py create mode 100644 google/protobuf/compiler/__init__.py create mode 100644 google/protobuf/compiler/plugin_pb2.py create mode 100644 google/protobuf/descriptor.py create mode 100644 google/protobuf/descriptor.upb.c create mode 100644 google/protobuf/descriptor.upb.h create mode 100644 google/protobuf/descriptor.upb_minitable.c create mode 100644 google/protobuf/descriptor.upb_minitable.h create mode 100644 google/protobuf/descriptor.upbdefs.c create mode 100644 google/protobuf/descriptor.upbdefs.h create mode 100644 google/protobuf/descriptor_database.py create mode 100644 google/protobuf/descriptor_pb2.py create mode 100644 google/protobuf/descriptor_pool.py create mode 100644 google/protobuf/duration_pb2.py create mode 100644 google/protobuf/empty_pb2.py create mode 100644 google/protobuf/field_mask_pb2.py create mode 100644 google/protobuf/internal/__init__.py create mode 100644 google/protobuf/internal/_parameterized.py create mode 100644 google/protobuf/internal/api_implementation.py create mode 100644 google/protobuf/internal/builder.py create mode 100644 google/protobuf/internal/containers.py create mode 100644 google/protobuf/internal/decoder.py create mode 100644 google/protobuf/internal/encoder.py create mode 100644 google/protobuf/internal/enum_type_wrapper.py create mode 100644 google/protobuf/internal/extension_dict.py create mode 100644 google/protobuf/internal/field_mask.py create mode 100644 google/protobuf/internal/message_listener.py create mode 100644 google/protobuf/internal/python_edition_defaults.py create mode 100644 google/protobuf/internal/python_message.py create mode 100644 google/protobuf/internal/testing_refleaks.py create mode 100644 google/protobuf/internal/type_checkers.py create mode 100644 google/protobuf/internal/well_known_types.py create mode 100644 google/protobuf/internal/wire_format.py create mode 100644 google/protobuf/json_format.py create mode 100644 google/protobuf/message.py create mode 100644 google/protobuf/message_factory.py create mode 100644 google/protobuf/proto_builder.py create mode 100644 google/protobuf/pyext/__init__.py create mode 100644 google/protobuf/pyext/cpp_message.py create mode 100644 google/protobuf/reflection.py create mode 100644 google/protobuf/runtime_version.py create mode 100644 google/protobuf/service.py create mode 100644 google/protobuf/service_reflection.py create mode 100644 google/protobuf/source_context_pb2.py create mode 100644 google/protobuf/struct_pb2.py create mode 100644 google/protobuf/symbol_database.py create mode 100644 google/protobuf/testdata/__init__.py create mode 100644 google/protobuf/text_encoding.py create mode 100644 google/protobuf/text_format.py create mode 100644 google/protobuf/timestamp_pb2.py create mode 100644 google/protobuf/type_pb2.py create mode 100644 google/protobuf/unknown_fields.py create mode 100644 google/protobuf/util/__init__.py create mode 100644 google/protobuf/wrappers_pb2.py create mode 100644 hoymiles_wifi/__init__.py create mode 100644 hoymiles_wifi/__main__.py create mode 100644 hoymiles_wifi/const.py create mode 100644 hoymiles_wifi/dtu.py create mode 100644 hoymiles_wifi/hoymiles.py create mode 100644 hoymiles_wifi/protobuf/APPHeartbeatPB.proto create mode 100644 hoymiles_wifi/protobuf/APPHeartbeatPB_pb2.py create mode 100644 hoymiles_wifi/protobuf/APPInfomationData.proto create mode 100644 hoymiles_wifi/protobuf/APPInfomationData_pb2.py create mode 100644 hoymiles_wifi/protobuf/AlarmData.proto create mode 100644 hoymiles_wifi/protobuf/AlarmData_pb2.py create mode 100644 hoymiles_wifi/protobuf/AppGetHistED.proto create mode 100644 hoymiles_wifi/protobuf/AppGetHistED_pb2.py create mode 100644 hoymiles_wifi/protobuf/AppGetHistPower.proto create mode 100644 hoymiles_wifi/protobuf/AppGetHistPower_pb2.py create mode 100644 hoymiles_wifi/protobuf/AutoSearch.proto create mode 100644 hoymiles_wifi/protobuf/AutoSearch_pb2.py create mode 100644 hoymiles_wifi/protobuf/CommandPB.proto create mode 100644 hoymiles_wifi/protobuf/CommandPB_pb2.py create mode 100644 hoymiles_wifi/protobuf/DevConfig.proto create mode 100644 hoymiles_wifi/protobuf/DevConfig_pb2.py create mode 100644 hoymiles_wifi/protobuf/EventData.proto create mode 100644 hoymiles_wifi/protobuf/EventData_pb2.py create mode 100644 hoymiles_wifi/protobuf/GPSTData.proto create mode 100644 hoymiles_wifi/protobuf/GPSTData_pb2.py create mode 100644 hoymiles_wifi/protobuf/GetConfig.proto create mode 100644 hoymiles_wifi/protobuf/GetConfig_pb2.py create mode 100644 hoymiles_wifi/protobuf/InfomationData.proto create mode 100644 hoymiles_wifi/protobuf/InfomationData_pb2.py create mode 100644 hoymiles_wifi/protobuf/NetworkInfo.proto create mode 100644 hoymiles_wifi/protobuf/NetworkInfo_pb2.py create mode 100644 hoymiles_wifi/protobuf/RealData.proto create mode 100644 hoymiles_wifi/protobuf/RealDataNew.proto create mode 100644 hoymiles_wifi/protobuf/RealDataNew_pb2.py create mode 100644 hoymiles_wifi/protobuf/RealData_pb2.py create mode 100644 hoymiles_wifi/protobuf/SetConfig.proto create mode 100644 hoymiles_wifi/protobuf/SetConfig_pb2.py create mode 100644 hoymiles_wifi/protobuf/WarnData.proto create mode 100644 hoymiles_wifi/protobuf/WarnData_pb2.py create mode 100644 hoymiles_wifi/protobuf/__init__.py create mode 100644 hoymiles_wifi/protobuf/compile_proto.sh create mode 100644 hoymiles_wifi/utils.py create mode 100644 idna/__init__.py create mode 100644 idna/codec.py create mode 100644 idna/compat.py create mode 100644 idna/core.py create mode 100644 idna/idnadata.py create mode 100644 idna/intranges.py create mode 100644 idna/package_data.py create mode 100644 idna/py.typed create mode 100644 idna/uts46data.py create mode 100644 konfig.py create mode 100644 logging.ini create mode 100644 mqttClient.py create mode 100644 multidict/__init__.py create mode 100644 multidict/__init__.pyi create mode 100644 multidict/_abc.py create mode 100644 multidict/_compat.py create mode 100644 multidict/_multidict.c create mode 100644 multidict/_multidict_base.py create mode 100644 multidict/_multidict_py.py create mode 100644 multidict/_multilib/defs.h create mode 100644 multidict/_multilib/dict.h create mode 100644 multidict/_multilib/istr.h create mode 100644 multidict/_multilib/iter.h create mode 100644 multidict/_multilib/pair_list.h create mode 100644 multidict/_multilib/views.h create mode 100644 multidict/py.typed create mode 100644 mysql/__init__.py create mode 100644 mysql/connector/__init__.py create mode 100644 mysql/connector/abstracts.py create mode 100644 mysql/connector/authentication.py create mode 100644 mysql/connector/charsets.py create mode 100644 mysql/connector/connection.py create mode 100644 mysql/connector/connection_cext.py create mode 100644 mysql/connector/constants.py create mode 100644 mysql/connector/conversion.py create mode 100644 mysql/connector/cursor.py create mode 100644 mysql/connector/cursor_cext.py create mode 100644 mysql/connector/custom_types.py create mode 100644 mysql/connector/dbapi.py create mode 100644 mysql/connector/django/__init__.py create mode 100644 mysql/connector/django/base.py create mode 100644 mysql/connector/django/client.py create mode 100644 mysql/connector/django/compiler.py create mode 100644 mysql/connector/django/creation.py create mode 100644 mysql/connector/django/features.py create mode 100644 mysql/connector/django/introspection.py create mode 100644 mysql/connector/django/operations.py create mode 100644 mysql/connector/django/schema.py create mode 100644 mysql/connector/django/validation.py create mode 100644 mysql/connector/errorcode.py create mode 100644 mysql/connector/errors.py create mode 100644 mysql/connector/locales/__init__.py create mode 100644 mysql/connector/locales/eng/__init__.py create mode 100644 mysql/connector/locales/eng/client_error.py create mode 100644 mysql/connector/logger.py create mode 100644 mysql/connector/network.py create mode 100644 mysql/connector/opentelemetry/__init__.py create mode 100644 mysql/connector/opentelemetry/constants.py create mode 100644 mysql/connector/opentelemetry/context_propagation.py create mode 100644 mysql/connector/opentelemetry/instrumentation.py create mode 100644 mysql/connector/optionfiles.py create mode 100644 mysql/connector/plugins/__init__.py create mode 100644 mysql/connector/plugins/authentication_kerberos_client.py create mode 100644 mysql/connector/plugins/authentication_ldap_sasl_client.py create mode 100644 mysql/connector/plugins/authentication_oci_client.py create mode 100644 mysql/connector/plugins/caching_sha2_password.py create mode 100644 mysql/connector/plugins/mysql_clear_password.py create mode 100644 mysql/connector/plugins/mysql_native_password.py create mode 100644 mysql/connector/plugins/sha256_password.py create mode 100644 mysql/connector/pooling.py create mode 100644 mysql/connector/protocol.py create mode 100644 mysql/connector/py.typed create mode 100644 mysql/connector/types.py create mode 100644 mysql/connector/utils.py create mode 100644 mysql/connector/version.py create mode 100644 mysql/opentelemetry/__init__.py create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_api-1.18.0.dist-info/INSTALLER create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_api-1.18.0.dist-info/METADATA create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_api-1.18.0.dist-info/RECORD create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_api-1.18.0.dist-info/WHEEL create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_api-1.18.0.dist-info/entry_points.txt create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_sdk-1.18.0.dist-info/INSTALLER create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_sdk-1.18.0.dist-info/METADATA create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_sdk-1.18.0.dist-info/RECORD create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_sdk-1.18.0.dist-info/WHEEL create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_sdk-1.18.0.dist-info/entry_points.txt create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_semantic_conventions-0.39b0.dist-info/INSTALLER create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_semantic_conventions-0.39b0.dist-info/METADATA create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_semantic_conventions-0.39b0.dist-info/RECORD create mode 100644 mysql/opentelemetry/_dist_info/opentelemetry_semantic_conventions-0.39b0.dist-info/WHEEL create mode 100644 mysql/opentelemetry/_logs/__init__.py create mode 100644 mysql/opentelemetry/_logs/_internal/__init__.py create mode 100644 mysql/opentelemetry/_logs/severity/__init__.py create mode 100644 mysql/opentelemetry/attributes/__init__.py create mode 100644 mysql/opentelemetry/baggage/__init__.py create mode 100644 mysql/opentelemetry/baggage/propagation/__init__.py create mode 100644 mysql/opentelemetry/context/__init__.py create mode 100644 mysql/opentelemetry/context/context.py create mode 100644 mysql/opentelemetry/context/contextvars_context.py create mode 100644 mysql/opentelemetry/environment_variables.py create mode 100644 mysql/opentelemetry/importlib_metadata/__init__.py create mode 100644 mysql/opentelemetry/importlib_metadata/_adapters.py create mode 100644 mysql/opentelemetry/importlib_metadata/_collections.py create mode 100644 mysql/opentelemetry/importlib_metadata/_compat.py create mode 100644 mysql/opentelemetry/importlib_metadata/_functools.py create mode 100644 mysql/opentelemetry/importlib_metadata/_itertools.py create mode 100644 mysql/opentelemetry/importlib_metadata/_meta.py create mode 100644 mysql/opentelemetry/importlib_metadata/_py39compat.py create mode 100644 mysql/opentelemetry/importlib_metadata/_text.py create mode 100644 mysql/opentelemetry/importlib_metadata/py.typed create mode 100644 mysql/opentelemetry/metrics/__init__.py create mode 100644 mysql/opentelemetry/metrics/_internal/__init__.py create mode 100644 mysql/opentelemetry/metrics/_internal/instrument.py create mode 100644 mysql/opentelemetry/metrics/_internal/observation.py create mode 100644 mysql/opentelemetry/propagate/__init__.py create mode 100644 mysql/opentelemetry/propagators/__init__.py create mode 100644 mysql/opentelemetry/propagators/composite.py create mode 100644 mysql/opentelemetry/propagators/textmap.py create mode 100644 mysql/opentelemetry/py.typed create mode 100644 mysql/opentelemetry/sdk/__init__.py create mode 100644 mysql/opentelemetry/sdk/_configuration/__init__.py create mode 100644 mysql/opentelemetry/sdk/_logs/__init__.py create mode 100644 mysql/opentelemetry/sdk/_logs/_internal/__init__.py create mode 100644 mysql/opentelemetry/sdk/_logs/_internal/export/__init__.py create mode 100644 mysql/opentelemetry/sdk/_logs/_internal/export/in_memory_log_exporter.py create mode 100644 mysql/opentelemetry/sdk/_logs/export/__init__.py create mode 100644 mysql/opentelemetry/sdk/environment_variables.py create mode 100644 mysql/opentelemetry/sdk/error_handler/__init__.py create mode 100644 mysql/opentelemetry/sdk/metrics/__init__.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/__init__.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/aggregation.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/exceptions.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/exponential_histogram/__init__.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/exponential_histogram/buckets.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/ieee_754.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/logarithm_mapping.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/export/__init__.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/instrument.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/measurement.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/measurement_consumer.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/metric_reader_storage.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/point.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/sdk_configuration.py create mode 100644 mysql/opentelemetry/sdk/metrics/_internal/view.py create mode 100644 mysql/opentelemetry/sdk/metrics/export/__init__.py create mode 100644 mysql/opentelemetry/sdk/metrics/view/__init__.py create mode 100644 mysql/opentelemetry/sdk/py.typed create mode 100644 mysql/opentelemetry/sdk/resources/__init__.py create mode 100644 mysql/opentelemetry/sdk/trace/__init__.py create mode 100644 mysql/opentelemetry/sdk/trace/export/__init__.py create mode 100644 mysql/opentelemetry/sdk/trace/export/in_memory_span_exporter.py create mode 100644 mysql/opentelemetry/sdk/trace/id_generator.py create mode 100644 mysql/opentelemetry/sdk/trace/sampling.py create mode 100644 mysql/opentelemetry/sdk/util/__init__.py create mode 100644 mysql/opentelemetry/sdk/util/instrumentation.py create mode 100644 mysql/opentelemetry/sdk/version.py create mode 100644 mysql/opentelemetry/semconv/__init__.py create mode 100644 mysql/opentelemetry/semconv/metrics/__init__.py create mode 100644 mysql/opentelemetry/semconv/resource/__init__.py create mode 100644 mysql/opentelemetry/semconv/trace/__init__.py create mode 100644 mysql/opentelemetry/semconv/version.py create mode 100644 mysql/opentelemetry/trace/__init__.py create mode 100644 mysql/opentelemetry/trace/propagation/__init__.py create mode 100644 mysql/opentelemetry/trace/propagation/tracecontext.py create mode 100644 mysql/opentelemetry/trace/span.py create mode 100644 mysql/opentelemetry/trace/status.py create mode 100644 mysql/opentelemetry/util/__init__.py create mode 100644 mysql/opentelemetry/util/_importlib_metadata.py create mode 100644 mysql/opentelemetry/util/_once.py create mode 100644 mysql/opentelemetry/util/_providers.py create mode 100644 mysql/opentelemetry/util/re.py create mode 100644 mysql/opentelemetry/util/types.py create mode 100644 mysql/opentelemetry/version.py create mode 100644 paho/__init__.py create mode 100644 paho/mqtt/__init__.py create mode 100644 paho/mqtt/client.py create mode 100644 paho/mqtt/enums.py create mode 100644 paho/mqtt/matcher.py create mode 100644 paho/mqtt/packettypes.py create mode 100644 paho/mqtt/properties.py create mode 100644 paho/mqtt/publish.py create mode 100644 paho/mqtt/py.typed create mode 100644 paho/mqtt/reasoncodes.py create mode 100644 paho/mqtt/subscribe.py create mode 100644 paho/mqtt/subscribeoptions.py create mode 100644 phaoUtils/.gitignore create mode 100644 phaoUtils/CONTRIBUTING.md create mode 100644 phaoUtils/LICENSE.txt create mode 100644 phaoUtils/PKG-INFO create mode 100644 phaoUtils/README.rst create mode 100644 phaoUtils/about.html create mode 100644 phaoUtils/edl-v10 create mode 100644 phaoUtils/epl-v20 create mode 100644 phaoUtils/examples/aws_iot.py create mode 100644 phaoUtils/examples/client_logger.py create mode 100644 phaoUtils/examples/client_mqtt_clear_retain.py create mode 100644 phaoUtils/examples/client_pub-wait.py create mode 100644 phaoUtils/examples/client_pub_opts.py create mode 100644 phaoUtils/examples/client_rpc_math.py create mode 100644 phaoUtils/examples/client_session_present.py create mode 100644 phaoUtils/examples/client_sub-class.py create mode 100644 phaoUtils/examples/client_sub-multiple-callback.py create mode 100644 phaoUtils/examples/client_sub-srv.py create mode 100644 phaoUtils/examples/client_sub-ws.py create mode 100644 phaoUtils/examples/client_sub.py create mode 100644 phaoUtils/examples/client_sub_opts.py create mode 100644 phaoUtils/examples/context.py create mode 100644 phaoUtils/examples/loop_asyncio.py create mode 100644 phaoUtils/examples/loop_select.py create mode 100644 phaoUtils/examples/loop_trio.py create mode 100644 phaoUtils/examples/publish_multiple.py create mode 100644 phaoUtils/examples/publish_single.py create mode 100644 phaoUtils/examples/publish_utf8-27.py create mode 100644 phaoUtils/examples/publish_utf8-3.py create mode 100644 phaoUtils/examples/server_rpc_math.py create mode 100644 phaoUtils/examples/subscribe_callback.py create mode 100644 phaoUtils/examples/subscribe_simple.py create mode 100644 phaoUtils/notice.html create mode 100644 phaoUtils/pyproject.toml create mode 100644 phaoUtils/tests/__init__.py create mode 100644 phaoUtils/tests/consts.py create mode 100644 phaoUtils/tests/debug_helpers.py create mode 100644 phaoUtils/tests/lib/__init__.py create mode 100644 phaoUtils/tests/lib/clients/01-asyncio.py create mode 100644 phaoUtils/tests/lib/clients/01-decorators.py create mode 100644 phaoUtils/tests/lib/clients/01-keepalive-pingreq.py create mode 100644 phaoUtils/tests/lib/clients/01-no-clean-session.py create mode 100644 phaoUtils/tests/lib/clients/01-reconnect-on-failure.py create mode 100644 phaoUtils/tests/lib/clients/01-unpwd-empty-password-set.py create mode 100644 phaoUtils/tests/lib/clients/01-unpwd-empty-set.py create mode 100644 phaoUtils/tests/lib/clients/01-unpwd-set.py create mode 100644 phaoUtils/tests/lib/clients/01-unpwd-unicode-set.py create mode 100644 phaoUtils/tests/lib/clients/01-will-set.py create mode 100644 phaoUtils/tests/lib/clients/01-will-unpwd-set.py create mode 100644 phaoUtils/tests/lib/clients/01-zero-length-clientid.py create mode 100644 phaoUtils/tests/lib/clients/02-subscribe-qos0.py create mode 100644 phaoUtils/tests/lib/clients/02-subscribe-qos1.py create mode 100644 phaoUtils/tests/lib/clients/02-subscribe-qos2.py create mode 100644 phaoUtils/tests/lib/clients/02-unsubscribe.py create mode 100644 phaoUtils/tests/lib/clients/03-publish-b2c-qos1.py create mode 100644 phaoUtils/tests/lib/clients/03-publish-b2c-qos2.py create mode 100644 phaoUtils/tests/lib/clients/03-publish-c2b-qos1-disconnect.py create mode 100644 phaoUtils/tests/lib/clients/03-publish-c2b-qos2-disconnect.py create mode 100644 phaoUtils/tests/lib/clients/03-publish-fill-inflight.py create mode 100644 phaoUtils/tests/lib/clients/03-publish-helper-qos0-v5.py create mode 100644 phaoUtils/tests/lib/clients/03-publish-helper-qos0.py create mode 100644 phaoUtils/tests/lib/clients/03-publish-helper-qos1-disconnect.py create mode 100644 phaoUtils/tests/lib/clients/03-publish-qos0-no-payload.py create mode 100644 phaoUtils/tests/lib/clients/03-publish-qos0.py create mode 100644 phaoUtils/tests/lib/clients/04-retain-qos0.py create mode 100644 phaoUtils/tests/lib/clients/08-ssl-connect-alpn.py create mode 100644 phaoUtils/tests/lib/clients/08-ssl-connect-cert-auth-pw.py create mode 100644 phaoUtils/tests/lib/clients/08-ssl-connect-cert-auth.py create mode 100644 phaoUtils/tests/lib/clients/08-ssl-connect-no-auth.py create mode 100644 phaoUtils/tests/lib/clients/08-ssl-fake-cacert.py create mode 100644 phaoUtils/tests/lib/conftest.py create mode 100644 phaoUtils/tests/lib/test_01_asyncio.py create mode 100644 phaoUtils/tests/lib/test_01_decorators.py create mode 100644 phaoUtils/tests/lib/test_01_keepalive_pingreq.py create mode 100644 phaoUtils/tests/lib/test_01_no_clean_session.py create mode 100644 phaoUtils/tests/lib/test_01_reconnect_on_failure.py create mode 100644 phaoUtils/tests/lib/test_01_unpwd_empty_password_set.py create mode 100644 phaoUtils/tests/lib/test_01_unpwd_empty_set.py create mode 100644 phaoUtils/tests/lib/test_01_unpwd_set.py create mode 100644 phaoUtils/tests/lib/test_01_unpwd_unicode_set.py create mode 100644 phaoUtils/tests/lib/test_01_will_set.py create mode 100644 phaoUtils/tests/lib/test_01_will_unpwd_set.py create mode 100644 phaoUtils/tests/lib/test_01_zero_length_clientid.py create mode 100644 phaoUtils/tests/lib/test_02_subscribe_qos0.py create mode 100644 phaoUtils/tests/lib/test_02_subscribe_qos1.py create mode 100644 phaoUtils/tests/lib/test_02_subscribe_qos2.py create mode 100644 phaoUtils/tests/lib/test_02_unsubscribe.py create mode 100644 phaoUtils/tests/lib/test_03_publish_b2c_qos1.py create mode 100644 phaoUtils/tests/lib/test_03_publish_b2c_qos2.py create mode 100644 phaoUtils/tests/lib/test_03_publish_c2b_qos1_disconnect.py create mode 100644 phaoUtils/tests/lib/test_03_publish_c2b_qos2_disconnect.py create mode 100644 phaoUtils/tests/lib/test_03_publish_fill_inflight.py create mode 100644 phaoUtils/tests/lib/test_03_publish_helper_qos0.py create mode 100644 phaoUtils/tests/lib/test_03_publish_helper_qos0_v5.py create mode 100644 phaoUtils/tests/lib/test_03_publish_helper_qos1_disconnect.py create mode 100644 phaoUtils/tests/lib/test_03_publish_qos0.py create mode 100644 phaoUtils/tests/lib/test_03_publish_qos0_no_payload.py create mode 100644 phaoUtils/tests/lib/test_04_retain_qos0.py create mode 100644 phaoUtils/tests/lib/test_08_ssl_bad_cacert.py create mode 100644 phaoUtils/tests/lib/test_08_ssl_connect_alpn.py create mode 100644 phaoUtils/tests/lib/test_08_ssl_connect_cert_auth.py create mode 100644 phaoUtils/tests/lib/test_08_ssl_connect_cert_auth_pw.py create mode 100644 phaoUtils/tests/lib/test_08_ssl_connect_no_auth.py create mode 100644 phaoUtils/tests/lib/test_08_ssl_fake_cacert.py create mode 100644 phaoUtils/tests/mqtt5_props.py create mode 100644 phaoUtils/tests/paho_test.py create mode 100644 phaoUtils/tests/ssl/all-ca.crt create mode 100644 phaoUtils/tests/ssl/client-expired.crt create mode 100644 phaoUtils/tests/ssl/client-pw.crt create mode 100644 phaoUtils/tests/ssl/client-pw.key create mode 100644 phaoUtils/tests/ssl/client-revoked.crt create mode 100644 phaoUtils/tests/ssl/client-revoked.key create mode 100644 phaoUtils/tests/ssl/client.crt create mode 100644 phaoUtils/tests/ssl/client.key create mode 100644 phaoUtils/tests/ssl/crl.pem create mode 100644 phaoUtils/tests/ssl/gen.sh create mode 100644 phaoUtils/tests/ssl/openssl.cnf create mode 100644 phaoUtils/tests/ssl/server-expired.crt create mode 100644 phaoUtils/tests/ssl/server.crt create mode 100644 phaoUtils/tests/ssl/server.key create mode 100644 phaoUtils/tests/ssl/test-alt-ca.crt create mode 100644 phaoUtils/tests/ssl/test-alt-ca.key create mode 100644 phaoUtils/tests/ssl/test-bad-root-ca.crt create mode 100644 phaoUtils/tests/ssl/test-bad-root-ca.key create mode 100644 phaoUtils/tests/ssl/test-ca.srl create mode 100644 phaoUtils/tests/ssl/test-fake-root-ca.crt create mode 100644 phaoUtils/tests/ssl/test-fake-root-ca.key create mode 100644 phaoUtils/tests/ssl/test-root-ca.crt create mode 100644 phaoUtils/tests/ssl/test-root-ca.key create mode 100644 phaoUtils/tests/ssl/test-signing-ca.crt create mode 100644 phaoUtils/tests/ssl/test-signing-ca.key create mode 100644 phaoUtils/tests/test_client.py create mode 100644 phaoUtils/tests/test_matcher.py create mode 100644 phaoUtils/tests/test_mqttv5.py create mode 100644 phaoUtils/tests/test_reasoncodes.py create mode 100644 phaoUtils/tests/test_websocket_integration.py create mode 100644 phaoUtils/tests/test_websockets.py create mode 100644 phaoUtils/tests/testsupport/__init__.py create mode 100644 phaoUtils/tests/testsupport/broker.py create mode 100644 pyserial-master.zip create mode 100644 pyserial-master/pyserial-master/.gitignore create mode 100644 pyserial-master/pyserial-master/.travis.yml create mode 100644 pyserial-master/pyserial-master/CHANGES.rst create mode 100644 pyserial-master/pyserial-master/LICENSE.txt create mode 100644 pyserial-master/pyserial-master/MANIFEST.in create mode 100644 pyserial-master/pyserial-master/README.rst create mode 100644 pyserial-master/pyserial-master/documentation/Makefile create mode 100644 pyserial-master/pyserial-master/documentation/appendix.rst create mode 100644 pyserial-master/pyserial-master/documentation/conf.py create mode 100644 pyserial-master/pyserial-master/documentation/examples.rst create mode 100644 pyserial-master/pyserial-master/documentation/index.rst create mode 100644 pyserial-master/pyserial-master/documentation/pyserial.png create mode 100644 pyserial-master/pyserial-master/documentation/pyserial.rst create mode 100644 pyserial-master/pyserial-master/documentation/pyserial_api.rst create mode 100644 pyserial-master/pyserial-master/documentation/shortintro.rst create mode 100644 pyserial-master/pyserial-master/documentation/tools.rst create mode 100644 pyserial-master/pyserial-master/documentation/url_handlers.rst create mode 100644 pyserial-master/pyserial-master/examples/at_protocol.py create mode 100644 pyserial-master/pyserial-master/examples/port_publisher.py create mode 100644 pyserial-master/pyserial-master/examples/port_publisher.sh create mode 100644 pyserial-master/pyserial-master/examples/rfc2217_server.py create mode 100644 pyserial-master/pyserial-master/examples/setup-miniterm-py2exe.py create mode 100644 pyserial-master/pyserial-master/examples/setup-rfc2217_server-py2exe.py create mode 100644 pyserial-master/pyserial-master/examples/setup-wxTerminal-py2exe.py create mode 100644 pyserial-master/pyserial-master/examples/tcp_serial_redirect.py create mode 100644 pyserial-master/pyserial-master/examples/wxSerialConfigDialog.py create mode 100644 pyserial-master/pyserial-master/examples/wxSerialConfigDialog.wxg create mode 100644 pyserial-master/pyserial-master/examples/wxTerminal.py create mode 100644 pyserial-master/pyserial-master/examples/wxTerminal.wxg create mode 100644 pyserial-master/pyserial-master/pylintrc create mode 100644 pyserial-master/pyserial-master/requirements.txt create mode 100644 pyserial-master/pyserial-master/setup.cfg create mode 100644 pyserial-master/pyserial-master/setup.py create mode 100644 pyserial-master/pyserial-master/test/handlers/__init__.py create mode 100644 pyserial-master/pyserial-master/test/handlers/protocol_test.py create mode 100644 pyserial-master/pyserial-master/test/run_all_tests.py create mode 100644 pyserial-master/pyserial-master/test/test.py create mode 100644 pyserial-master/pyserial-master/test/test_advanced.py create mode 100644 pyserial-master/pyserial-master/test/test_asyncio.py create mode 100644 pyserial-master/pyserial-master/test/test_cancel.py create mode 100644 pyserial-master/pyserial-master/test/test_close.py create mode 100644 pyserial-master/pyserial-master/test/test_context.py create mode 100644 pyserial-master/pyserial-master/test/test_exclusive.py create mode 100644 pyserial-master/pyserial-master/test/test_high_load.py create mode 100644 pyserial-master/pyserial-master/test/test_iolib.py create mode 100644 pyserial-master/pyserial-master/test/test_pty.py create mode 100644 pyserial-master/pyserial-master/test/test_readline.py create mode 100644 pyserial-master/pyserial-master/test/test_rfc2217.py create mode 100644 pyserial-master/pyserial-master/test/test_rs485.py create mode 100644 pyserial-master/pyserial-master/test/test_settings_dict.py create mode 100644 pyserial-master/pyserial-master/test/test_threaded.py create mode 100644 pyserial-master/pyserial-master/test/test_timeout_class.py create mode 100644 pyserial-master/pyserial-master/test/test_url.py create mode 100644 pyserial-master/pyserial-master/test/test_util.py create mode 100644 serial/__init__.py create mode 100644 serial/__main__.py create mode 100644 serial/rfc2217.py create mode 100644 serial/rs485.py create mode 100644 serial/serialcli.py create mode 100644 serial/serialjava.py create mode 100644 serial/serialposix.py create mode 100644 serial/serialutil.py create mode 100644 serial/serialwin32.py create mode 100644 serial/threaded/__init__.py create mode 100644 serial/tools/__init__.py create mode 100644 serial/tools/hexlify_codec.py create mode 100644 serial/tools/list_ports.py create mode 100644 serial/tools/list_ports_common.py create mode 100644 serial/tools/list_ports_linux.py create mode 100644 serial/tools/list_ports_osx.py create mode 100644 serial/tools/list_ports_posix.py create mode 100644 serial/tools/list_ports_windows.py create mode 100644 serial/tools/miniterm.py create mode 100644 serial/urlhandler/__init__.py create mode 100644 serial/urlhandler/protocol_alt.py create mode 100644 serial/urlhandler/protocol_cp2110.py create mode 100644 serial/urlhandler/protocol_hwgrep.py create mode 100644 serial/urlhandler/protocol_loop.py create mode 100644 serial/urlhandler/protocol_rfc2217.py create mode 100644 serial/urlhandler/protocol_socket.py create mode 100644 serial/urlhandler/protocol_spy.py create mode 100644 serial/win32.py create mode 100644 six.py create mode 100644 skoda_test.py create mode 100644 skoda_testantwort.json create mode 100644 skoda_testdaten.py create mode 100644 solarManager.py create mode 100644 startMQTTbridge.sh create mode 100644 startSolarServer.sh create mode 100644 startWattpilotMQTT.sh create mode 100644 startWecker.sh create mode 100644 sunspec2/__init__.py create mode 100644 sunspec2/device.py create mode 100644 sunspec2/docs/pysunspec.rst create mode 100644 sunspec2/file/__init__.py create mode 100644 sunspec2/file/client.py create mode 100644 sunspec2/mb.py create mode 100644 sunspec2/mdef.py create mode 100644 sunspec2/modbus/__init__.py create mode 100644 sunspec2/modbus/client.py create mode 100644 sunspec2/modbus/modbus.py create mode 100644 sunspec2/models/.clabot create mode 100644 sunspec2/models/.gitattributes create mode 100644 sunspec2/models/.gitignore create mode 100644 sunspec2/models/.travis.yml create mode 100644 sunspec2/models/LICENSE create mode 100644 sunspec2/models/README.md create mode 100644 sunspec2/models/json/Makefile create mode 100644 sunspec2/models/json/model_1.json create mode 100644 sunspec2/models/json/model_10.json create mode 100644 sunspec2/models/json/model_101.json create mode 100644 sunspec2/models/json/model_102.json create mode 100644 sunspec2/models/json/model_103.json create mode 100644 sunspec2/models/json/model_11.json create mode 100644 sunspec2/models/json/model_111.json create mode 100644 sunspec2/models/json/model_112.json create mode 100644 sunspec2/models/json/model_113.json create mode 100644 sunspec2/models/json/model_12.json create mode 100644 sunspec2/models/json/model_120.json create mode 100644 sunspec2/models/json/model_121.json create mode 100644 sunspec2/models/json/model_122.json create mode 100644 sunspec2/models/json/model_123.json create mode 100644 sunspec2/models/json/model_124.json create mode 100644 sunspec2/models/json/model_125.json create mode 100644 sunspec2/models/json/model_126.json create mode 100644 sunspec2/models/json/model_127.json create mode 100644 sunspec2/models/json/model_128.json create mode 100644 sunspec2/models/json/model_129.json create mode 100644 sunspec2/models/json/model_13.json create mode 100644 sunspec2/models/json/model_130.json create mode 100644 sunspec2/models/json/model_131.json create mode 100644 sunspec2/models/json/model_132.json create mode 100644 sunspec2/models/json/model_133.json create mode 100644 sunspec2/models/json/model_134.json create mode 100644 sunspec2/models/json/model_135.json create mode 100644 sunspec2/models/json/model_136.json create mode 100644 sunspec2/models/json/model_137.json create mode 100644 sunspec2/models/json/model_138.json create mode 100644 sunspec2/models/json/model_139.json create mode 100644 sunspec2/models/json/model_14.json create mode 100644 sunspec2/models/json/model_140.json create mode 100644 sunspec2/models/json/model_141.json create mode 100644 sunspec2/models/json/model_142.json create mode 100644 sunspec2/models/json/model_143.json create mode 100644 sunspec2/models/json/model_144.json create mode 100644 sunspec2/models/json/model_145.json create mode 100644 sunspec2/models/json/model_15.json create mode 100644 sunspec2/models/json/model_16.json create mode 100644 sunspec2/models/json/model_160.json create mode 100644 sunspec2/models/json/model_17.json create mode 100644 sunspec2/models/json/model_18.json create mode 100644 sunspec2/models/json/model_19.json create mode 100644 sunspec2/models/json/model_2.json create mode 100644 sunspec2/models/json/model_201.json create mode 100644 sunspec2/models/json/model_202.json create mode 100644 sunspec2/models/json/model_203.json create mode 100644 sunspec2/models/json/model_204.json create mode 100644 sunspec2/models/json/model_211.json create mode 100644 sunspec2/models/json/model_212.json create mode 100644 sunspec2/models/json/model_213.json create mode 100644 sunspec2/models/json/model_214.json create mode 100644 sunspec2/models/json/model_220.json create mode 100644 sunspec2/models/json/model_3.json create mode 100644 sunspec2/models/json/model_302.json create mode 100644 sunspec2/models/json/model_303.json create mode 100644 sunspec2/models/json/model_304.json create mode 100644 sunspec2/models/json/model_305.json create mode 100644 sunspec2/models/json/model_306.json create mode 100644 sunspec2/models/json/model_307.json create mode 100644 sunspec2/models/json/model_308.json create mode 100644 sunspec2/models/json/model_4.json create mode 100644 sunspec2/models/json/model_401.json create mode 100644 sunspec2/models/json/model_402.json create mode 100644 sunspec2/models/json/model_403.json create mode 100644 sunspec2/models/json/model_404.json create mode 100644 sunspec2/models/json/model_5.json create mode 100644 sunspec2/models/json/model_501.json create mode 100644 sunspec2/models/json/model_502.json create mode 100644 sunspec2/models/json/model_6.json create mode 100644 sunspec2/models/json/model_601.json create mode 100644 sunspec2/models/json/model_63001.json create mode 100644 sunspec2/models/json/model_63002.json create mode 100644 sunspec2/models/json/model_64001.json create mode 100644 sunspec2/models/json/model_64020.json create mode 100644 sunspec2/models/json/model_64101.json create mode 100644 sunspec2/models/json/model_64111.json create mode 100644 sunspec2/models/json/model_64112.json create mode 100644 sunspec2/models/json/model_7.json create mode 100644 sunspec2/models/json/model_701.json create mode 100644 sunspec2/models/json/model_702.json create mode 100644 sunspec2/models/json/model_703.json create mode 100644 sunspec2/models/json/model_704.json create mode 100644 sunspec2/models/json/model_705.json create mode 100644 sunspec2/models/json/model_706.json create mode 100644 sunspec2/models/json/model_707.json create mode 100644 sunspec2/models/json/model_708.json create mode 100644 sunspec2/models/json/model_709.json create mode 100644 sunspec2/models/json/model_710.json create mode 100644 sunspec2/models/json/model_711.json create mode 100644 sunspec2/models/json/model_712.json create mode 100644 sunspec2/models/json/model_713.json create mode 100644 sunspec2/models/json/model_714.json create mode 100644 sunspec2/models/json/model_715.json create mode 100644 sunspec2/models/json/model_8.json create mode 100644 sunspec2/models/json/model_801.json create mode 100644 sunspec2/models/json/model_802.json create mode 100644 sunspec2/models/json/model_803.json create mode 100644 sunspec2/models/json/model_804.json create mode 100644 sunspec2/models/json/model_805.json create mode 100644 sunspec2/models/json/model_806.json create mode 100644 sunspec2/models/json/model_807.json create mode 100644 sunspec2/models/json/model_808.json create mode 100644 sunspec2/models/json/model_809.json create mode 100644 sunspec2/models/json/model_9.json create mode 100644 sunspec2/models/json/schema.json create mode 100644 sunspec2/models/smdx/CHANGELOG create mode 100644 sunspec2/models/smdx/Makefile create mode 100644 sunspec2/models/smdx/manifest.py create mode 100644 sunspec2/models/smdx/manifest.xml create mode 100644 sunspec2/models/smdx/manifest.xml.md5 create mode 100644 sunspec2/models/smdx/smdx.xsd create mode 100644 sunspec2/models/smdx/smdx_00001.xml create mode 100644 sunspec2/models/smdx/smdx_00002.xml create mode 100644 sunspec2/models/smdx/smdx_00003.xml create mode 100644 sunspec2/models/smdx/smdx_00004.xml create mode 100644 sunspec2/models/smdx/smdx_00005.xml create mode 100644 sunspec2/models/smdx/smdx_00006.xml create mode 100644 sunspec2/models/smdx/smdx_00007.xml create mode 100644 sunspec2/models/smdx/smdx_00008.xml create mode 100644 sunspec2/models/smdx/smdx_00009.xml create mode 100644 sunspec2/models/smdx/smdx_00010.xml create mode 100644 sunspec2/models/smdx/smdx_00011.xml create mode 100644 sunspec2/models/smdx/smdx_00012.xml create mode 100644 sunspec2/models/smdx/smdx_00013.xml create mode 100644 sunspec2/models/smdx/smdx_00014.xml create mode 100644 sunspec2/models/smdx/smdx_00015.xml create mode 100644 sunspec2/models/smdx/smdx_00016.xml create mode 100644 sunspec2/models/smdx/smdx_00017.xml create mode 100644 sunspec2/models/smdx/smdx_00018.xml create mode 100644 sunspec2/models/smdx/smdx_00019.xml create mode 100644 sunspec2/models/smdx/smdx_00101.xml create mode 100644 sunspec2/models/smdx/smdx_00102.xml create mode 100644 sunspec2/models/smdx/smdx_00103.xml create mode 100644 sunspec2/models/smdx/smdx_00111.xml create mode 100644 sunspec2/models/smdx/smdx_00112.xml create mode 100644 sunspec2/models/smdx/smdx_00113.xml create mode 100644 sunspec2/models/smdx/smdx_00120.xml create mode 100644 sunspec2/models/smdx/smdx_00121.xml create mode 100644 sunspec2/models/smdx/smdx_00122.xml create mode 100644 sunspec2/models/smdx/smdx_00123.xml create mode 100644 sunspec2/models/smdx/smdx_00124.xml create mode 100644 sunspec2/models/smdx/smdx_00125.xml create mode 100644 sunspec2/models/smdx/smdx_00126.xml create mode 100644 sunspec2/models/smdx/smdx_00127.xml create mode 100644 sunspec2/models/smdx/smdx_00128.xml create mode 100644 sunspec2/models/smdx/smdx_00129.xml create mode 100644 sunspec2/models/smdx/smdx_00130.xml create mode 100644 sunspec2/models/smdx/smdx_00131.xml create mode 100644 sunspec2/models/smdx/smdx_00132.xml create mode 100644 sunspec2/models/smdx/smdx_00133.xml create mode 100644 sunspec2/models/smdx/smdx_00134.xml create mode 100644 sunspec2/models/smdx/smdx_00135.xml create mode 100644 sunspec2/models/smdx/smdx_00136.xml create mode 100644 sunspec2/models/smdx/smdx_00137.xml create mode 100644 sunspec2/models/smdx/smdx_00138.xml create mode 100644 sunspec2/models/smdx/smdx_00139.xml create mode 100644 sunspec2/models/smdx/smdx_00140.xml create mode 100644 sunspec2/models/smdx/smdx_00141.xml create mode 100644 sunspec2/models/smdx/smdx_00142.xml create mode 100644 sunspec2/models/smdx/smdx_00143.xml create mode 100644 sunspec2/models/smdx/smdx_00144.xml create mode 100644 sunspec2/models/smdx/smdx_00145.xml create mode 100644 sunspec2/models/smdx/smdx_00160.xml create mode 100644 sunspec2/models/smdx/smdx_00201.xml create mode 100644 sunspec2/models/smdx/smdx_00202.xml create mode 100644 sunspec2/models/smdx/smdx_00203.xml create mode 100644 sunspec2/models/smdx/smdx_00204.xml create mode 100644 sunspec2/models/smdx/smdx_00211.xml create mode 100644 sunspec2/models/smdx/smdx_00212.xml create mode 100644 sunspec2/models/smdx/smdx_00213.xml create mode 100644 sunspec2/models/smdx/smdx_00214.xml create mode 100644 sunspec2/models/smdx/smdx_00220.xml create mode 100644 sunspec2/models/smdx/smdx_00302.xml create mode 100644 sunspec2/models/smdx/smdx_00303.xml create mode 100644 sunspec2/models/smdx/smdx_00304.xml create mode 100644 sunspec2/models/smdx/smdx_00305.xml create mode 100644 sunspec2/models/smdx/smdx_00306.xml create mode 100644 sunspec2/models/smdx/smdx_00307.xml create mode 100644 sunspec2/models/smdx/smdx_00308.xml create mode 100644 sunspec2/models/smdx/smdx_00401.xml create mode 100644 sunspec2/models/smdx/smdx_00402.xml create mode 100644 sunspec2/models/smdx/smdx_00403.xml create mode 100644 sunspec2/models/smdx/smdx_00404.xml create mode 100644 sunspec2/models/smdx/smdx_00501.xml create mode 100644 sunspec2/models/smdx/smdx_00502.xml create mode 100644 sunspec2/models/smdx/smdx_00601.xml create mode 100644 sunspec2/models/smdx/smdx_00801.xml create mode 100644 sunspec2/models/smdx/smdx_00802.xml create mode 100644 sunspec2/models/smdx/smdx_00803.xml create mode 100644 sunspec2/models/smdx/smdx_00804.xml create mode 100644 sunspec2/models/smdx/smdx_00805.xml create mode 100644 sunspec2/models/smdx/smdx_00806.xml create mode 100644 sunspec2/models/smdx/smdx_00807.xml create mode 100644 sunspec2/models/smdx/smdx_00808.xml create mode 100644 sunspec2/models/smdx/smdx_00809.xml create mode 100644 sunspec2/models/smdx/smdx_63001.xml create mode 100644 sunspec2/models/smdx/smdx_63002.xml create mode 100644 sunspec2/models/smdx/smdx_64001.xml create mode 100644 sunspec2/models/smdx/smdx_64020.xml create mode 100644 sunspec2/models/smdx/smdx_64101.xml create mode 100644 sunspec2/models/smdx/smdx_64111.xml create mode 100644 sunspec2/models/smdx/smdx_64112.xml create mode 100644 sunspec2/models/utils/add_sunspec_comments.py create mode 100644 sunspec2/models/utils/add_sunspec_detail.py create mode 100644 sunspec2/models/utils/add_sunspec_standards.py create mode 100644 sunspec2/smdx.py create mode 100644 sunspec2/spreadsheet.py create mode 100644 sunspec2/tests/__init__.py create mode 100644 sunspec2/tests/mock_port.py create mode 100644 sunspec2/tests/mock_socket.py create mode 100644 sunspec2/tests/test_data/__init__.py create mode 100644 sunspec2/tests/test_data/device_1547.json create mode 100644 sunspec2/tests/test_data/inverter_123.json create mode 100644 sunspec2/tests/test_data/smdx_304.csv create mode 100644 sunspec2/tests/test_data/wb_701-705.xlsx create mode 100644 sunspec2/tests/test_device.py create mode 100644 sunspec2/tests/test_file_client.py create mode 100644 sunspec2/tests/test_mb.py create mode 100644 sunspec2/tests/test_mdef.py create mode 100644 sunspec2/tests/test_modbus_client.py create mode 100644 sunspec2/tests/test_modbus_modbus.py create mode 100644 sunspec2/tests/test_smdx.py create mode 100644 sunspec2/tests/test_spreadsheet.py create mode 100644 sunspec2/tests/test_xlsx.py create mode 100644 sunspec2/xlsx.py create mode 100644 suntime/__init__.py create mode 100644 suntime/suntime.py create mode 100644 webSocketServer.py create mode 100644 websocket/__init__.py create mode 100644 websocket/_abnf.py create mode 100644 websocket/_app.py create mode 100644 websocket/_cookiejar.py create mode 100644 websocket/_core.py create mode 100644 websocket/_exceptions.py create mode 100644 websocket/_handshake.py create mode 100644 websocket/_http.py create mode 100644 websocket/_logging.py create mode 100644 websocket/_socket.py create mode 100644 websocket/_ssl_compat.py create mode 100644 websocket/_url.py create mode 100644 websocket/_utils.py create mode 100644 websocket/_wsdump.py create mode 100644 websocket/data/WebSocketMain.swf create mode 100644 websocket/data/__init__.py create mode 100644 websocket/data/flashsocket.js create mode 100644 websocket/policyserver.py create mode 100644 websocket/server.py create mode 100644 websocket/tests/__init__.py create mode 100644 websocket/tests/data/header01.txt create mode 100644 websocket/tests/data/header02.txt create mode 100644 websocket/tests/data/header03.txt create mode 100644 websocket/tests/echo-server.py create mode 100644 websocket/tests/test_abnf.py create mode 100644 websocket/tests/test_app.py create mode 100644 websocket/tests/test_cookiejar.py create mode 100644 websocket/tests/test_http.py create mode 100644 websocket/tests/test_url.py create mode 100644 websocket/tests/test_websocket.py create mode 100644 websockets/__init__.py create mode 100644 websockets/__main__.py create mode 100644 websockets/auth.py create mode 100644 websockets/client.py create mode 100644 websockets/connection.py create mode 100644 websockets/datastructures.py create mode 100644 websockets/exceptions.py create mode 100644 websockets/extensions/__init__.py create mode 100644 websockets/extensions/base.py create mode 100644 websockets/extensions/permessage_deflate.py create mode 100644 websockets/frames.py create mode 100644 websockets/headers.py create mode 100644 websockets/http.py create mode 100644 websockets/http11.py create mode 100644 websockets/imports.py create mode 100644 websockets/legacy/__init__.py create mode 100644 websockets/legacy/async_timeout.py create mode 100644 websockets/legacy/auth.py create mode 100644 websockets/legacy/client.py create mode 100644 websockets/legacy/compatibility.py create mode 100644 websockets/legacy/framing.py create mode 100644 websockets/legacy/handshake.py create mode 100644 websockets/legacy/http.py create mode 100644 websockets/legacy/protocol.py create mode 100644 websockets/legacy/server.py create mode 100644 websockets/protocol.py create mode 100644 websockets/py.typed create mode 100644 websockets/server.py create mode 100644 websockets/speedups.c create mode 100644 websockets/speedups.pyi create mode 100644 websockets/streams.py create mode 100644 websockets/sync/__init__.py create mode 100644 websockets/sync/client.py create mode 100644 websockets/sync/connection.py create mode 100644 websockets/sync/messages.py create mode 100644 websockets/sync/server.py create mode 100644 websockets/sync/utils.py create mode 100644 websockets/typing.py create mode 100644 websockets/uri.py create mode 100644 websockets/utils.py create mode 100644 websockets/version.py create mode 100644 wecker.py create mode 100644 wsMQTTbridge.py create mode 100644 yarl/__init__.py create mode 100644 yarl/__init__.pyi create mode 100644 yarl/_quoting.py create mode 100644 yarl/_quoting_c.pyi create mode 100644 yarl/_quoting_c.pyx create mode 100644 yarl/_quoting_py.py create mode 100644 yarl/_url.py create mode 100644 yarl/py.typed create mode 100644 zeit.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9829860 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Alles mit LF im Repository. Die Start-Skripte laufen auf der Synology, und +# ein CRLF im Shebang macht daraus ein "bad interpreter: no such file or +# directory" - ein Fehler, der sich schlecht suchen laesst. +* text=auto eol=lf + +*.zip binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7cb6e98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Zugangsdaten - nichts davon gehoert ins Repository +config.ini +skoda.conf +*.conf +secrets.conf + +# Protokolle. wattpilotshell.log ist allein 329 MB gross, die Logs machen +# 93 Prozent des Verzeichnisses aus. +*.log + +# Python +__pycache__/ +*.py[cod] +*$py.class + +# IDE +.vscode/ +.idea/ +*.swp +*~ + +# Betriebssystem +.DS_Store +Thumbs.db +@eaDir/ + +# Temporaeres +*.tmp +*.bak +*.backup diff --git a/Saldierung.txt b/Saldierung.txt new file mode 100644 index 0000000..c584d46 --- /dev/null +++ b/Saldierung.txt @@ -0,0 +1,4 @@ +Bi-directional table update for better meter matching: +ALTER TABLE `EnergyFlow` ADD `gridPcons` FLOAT NOT NULL AFTER `gridP`, ADD `gridPfeed` FLOAT NOT NULL AFTER `gridPcons`; +UPDATE EnergyFlow SET gridPcons = gridP WHERE gridP > 0 AND gridPcons = 0; +UPDATE EnergyFlow SET gridPfeed = -gridP WHERE gridP < 0 AND gridPfeed = 0; \ No newline at end of file diff --git a/Wattpilot/Wattpilot.py b/Wattpilot/Wattpilot.py new file mode 100644 index 0000000..e311839 --- /dev/null +++ b/Wattpilot/Wattpilot.py @@ -0,0 +1,673 @@ +import sys +sys.path.append("./") +import websocket +import json +import hashlib +import random +import threading +import hmac +import logging + +import base64 +import subprocess + +from time import sleep +from types import SimpleNamespace + + + +_LOGGER = logging.getLogger(__name__) + + + +class LoadMode(): + """Wrapper Class to represent the Load Mode of the Wattpilot""" + DEFAULT=3 + ECO=4 + NEXTTRIP=5 + + +class Wattpilot(object): + + supported_events = [ + # Wattpilot events: + "wp_auth", + "wp_authError", + "wp_authSuccess", + "wp_clearInverters", + "wp_connect", + "wp_deltaStatus", + "wp_disconnect", + "wp_fullStatus", + "wp_fullStatus_finished", + "wp_hello", + "wp_init", + "wp_property", + "wp_response", + "wp_updateInverter", + # WebSocketApp events: + "ws_close", + "ws_error", + "ws_message", + "ws_open", + ] + + carValues = {} + alwValues = {} + astValues = {} + lmoValues = {} + ustValues = {} + errValues = {} + acsValues = {} + + lmoValues[3] = "Default" + lmoValues[4] = "Eco" + lmoValues[5] = "Next Trip" + + astValues[0] = "open" + astValues[1] = "locked" + astValues[2] = "auto" + + carValues[1] = "no car" + carValues[2] = "charging" + carValues[3] = "ready" + carValues[4] = "complete" + + alwValues[0] = False + alwValues[1] = True + + ustValues[0] = "Normal" + ustValues[1] = "AutoUnlock" + ustValues[2] = "AlwaysLock" + + errValues[0] = "Unknown Error" + errValues[1] = "Idle" + errValues[2] = "Charging" + errValues[3] = "Wait Car" + errValues[4] = "Complete" + errValues[5] = "Error" + + acsValues[0] = "Open" + acsValues[1] = "Wait" + + + @property + def allProps(self): + """Returns a dictionary with all properties""" + return self._allProps + + @property + def allPropsInitialized(self): + """Returns true, if all properties have been initialized""" + return self._allPropsInitialized + + @property + def cableType(self): + """Returns the Cable Type (Ampere) of the connected cable""" + return self._cableType + + @property + def frequency(self): + """Returns the power frequency""" + return self._frequency + + @property + def phases(self): + """returns the phases""" + return self._phases + + @property + def energyCounterSinceStart(self): + """Returns used kwh since start of charging""" + return self._energyCounterSinceStart + + @property + def errorState(self): + """Returns error State""" + return self._errorState + + @property + def cableLock(self): + return self._cableLock + + @property + def energyCounterTotal(self): + return self._energyCounterTotal + + @property + def serial(self): + """Returns the serial number of Wattpilot Device (read only)""" + return self._serial + @serial.setter + def serial(self,value): + self._serial = value + if (self._password is not None) & (self._serial is not None): + self._hashedpassword = base64.b64encode(hashlib.pbkdf2_hmac('sha512',self._password.encode(),self._serial.encode(),100000,256))[:32] + + @property + def name(self): + """Returns the name of Wattpilot Device (read only)""" + return self._name + + + @property + def hostname(self): + """Returns the DNS Hostname of Wattpilot Device (read only)""" + return self._hostname + + @property + def friendlyName(self): + """Returns the friendly name of Wattpilot Device (read only)""" + return self._friendlyName + + @property + def manufacturer(self): + """Returns the Manufacturer of Wattpilot Device (read only)""" + return self._manufacturer + + @property + def devicetype(self): + return self._devicetype + + @property + def protocol(self): + return self._protocol + + @property + def secured(self): + return self._secured + + @property + def password(self): + return self._password + @password.setter + def password(self,value): + self._password = value + if (self._password is not None) & (self._serial is not None): + self._hashedpassword = base64.b64encode(hashlib.pbkdf2_hmac('sha512',self._password.encode(),self._serial.encode(),100000,256))[:32] + + + @property + def url(self): + return self._url + @url.setter + def url(self,value): + self._url = value + + @property + def connected(self): + return self._connected + + @property + def voltage1(self): + return self._voltage1 + + @property + def voltage2(self): + return self._voltage2 + + @property + def voltage3(self): + return self._voltage3 + + @property + def voltageN(self): + return self._voltageN + + @property + def amps1(self): + return self._amps1 + + @property + def amps2(self): + return self._amps2 + + @property + def amps3(self): + return self._amps3 + + @property + def power1(self): + return self._power1 + + @property + def power2(self): + return self._power2 + + @property + def power3(self): + return self._power3 + + @property + def powerN(self): + return self._powerN + + @property + def power(self): + return self._power + + @property + def version(self): + return self._version + + @property + def amp(self): + return self._amp + + @property + def AccessState(self): + return self._AccessState + + @property + def firmware(self): + """Returns the Firmwareversion of Wattpilot Device (read only)""" + return self._firmware + + @property + def ftt(self): + """Returns the The desired charged time for NextTrip""" + return self._ftt + + @property + def WifiSSID(self): + """Returns the SSID of the Wifi network currently connected (read only)""" + return self._WifiSSID + + @property + def AllowCharging(self): + return self._AllowCharging + + @property + def mode(self): + return self._mode + + @property + def carConnected(self): + return self._carConnected + + @property + def cae(self): + """Returns true if Cloud API Access is enabled (read only)""" + return self._cae + + @property + def cak(self): + """Returns the API Key for Cloud API Access (read only)""" + return self._cak + + + def __str__(self): + """Returns a String representation of the core Wattpilot attributes""" + if self.connected: + ret = "Wattpilot: " + str(self.name) + "\n" + ret = ret + "Serial: " + str(self.serial) + "\n" + ret = ret + "Connected: " + str(self.connected) + "\n" + ret = ret + "Car Connected: " + str(self.carConnected) + "\n" + ret = ret + "Charge Status " + str(self.AllowCharging) + "\n" + ret = ret + "Mode: " + str(self.mode) + "\n" + ret = ret + "Power: " + str(self.amp) + "\n" + ret = ret + "Charge: " + "%.2f" % self.power + "kW" + " ---- " + str(self.voltage1) + "V/" + str(self.voltage2) + "V/" + str(self.voltage3) + "V" + " -- " + ret = ret + "%.2f" % self.amps1 + "A/" + "%.2f" % self.amps2 + "A/" + "%.2f" % self.amps3 + "A" + " -- " + ret = ret + "%.2f" % self.power1 + "kW/" + "%.2f" % self.power2 + "kW/" + "%.2f" % self.power3 + "kW" + "\n" + else: + ret = "Not connected" + + return ret + def connect(self): + self._wst = threading.Thread(target=self._wsapp.run_forever) + self._wst.daemon = True + self._wst.start() + self.__call_event_handler("wp_connect") + _LOGGER.info("Wattpilot connected") + + def disconnect(self): + self._wsapp.close() + self._connected=False + self._auto_reconnect=False # Do not reconnect on explicit disconnect + self.__call_event_handler("wp_disconnect") + _LOGGER.info("Wattpilot disconnected") + + # Wattpilot Event Handling + + def add_event_handler(self,event_type,callback_fn): + if event_type not in self._event_handler: + self._event_handler[event_type] = [] + self._event_handler[event_type].append(callback_fn) + + def remove_event_handler(self,event_type,callback_fn): + if event_type in self._event_handler and callback_fn in self._event_handler[event_type]: + self._event_handler[event_type].remove(callback_fn) + + def __call_event_handler(self, event_type, *args): + _LOGGER.debug(f"Calling event handler for event type '{event_type} ...") + for callback_fn in self._event_handler[event_type]: + event = { + "type": event_type, + "wp": self, + } + callback_fn(event,*args) + + + def set_power(self,power): + self.send_update("amp",power) + + def set_mode(self,mode): + self.send_update("lmo",mode) + + + def send_update(self,name,value): + message = {} + message["type"]="setValue" + self.__requestid = self.__requestid+1 + message["requestId"]=self.__requestid + message["key"]=name + message["value"]=value + if (self._secured is not None): + if (self._secured > 0): + self.__send(message,True) + else: + self.__send(message) + else: + self.__send(message) + + def unpairInverter(self,InverterID): + message = {} + message["type"]="unpairInverter" + self.__requestid = self.__requestid+1 + message["requestId"]=self.__requestid + message["inverterId"]=InverterID + if (self._secured is not None): + if (self._secured > 0): + self.__send(message,True) + else: + self.__send(message) + else: + self.__send(message) + + def pairInverter(self,InverterID): + message = {} + message["type"]="pairInverter" + self.__requestid = self.__requestid+1 + message["requestId"]=self.__requestid + message["inverterId"]=InverterID + if (self._secured is not None): + if (self._secured > 0): + self.__send(message,True) + else: + self.__send(message) + else: + self.__send(message) + + def __update_property(self,name,value): + + self._allProps[name] = value + if name=="acs": + self._AccessState = Wattpilot.acsValues[value] + + if name=="cbl": + self._cableType = value + + if name=="fhz": + self._frequency = value + + if name=="pha": + self._phases = value + + if name=="wh": + self._energyCounterSinceStart = value + + if name=="err": + self._errorState = Wattpilot.errValues[value] + + if name=="ust": + self._cableLock = Wattpilot.ustValues[value] + + if name=="eto": + self._energyCounterTotal = value + + if name=="cae": + self._cae = value + if name=="cak": + self._cak = value + if name=="lmo": + self._mode = Wattpilot.lmoValues[value] + if name=="car": + self._carConnected = Wattpilot.carValues[value] + if name=="alw": + self._AllowCharging = Wattpilot.alwValues[value] + if name=="nrg": + self._voltage1=value[0] + self._voltage2=value[1] + self._voltage3=value[2] + self._voltageN=value[3] + self._amps1=value[4] + self._amps2=value[5] + self._amps3=value[6] + self._power1=value[7]*0.001 + self._power2=value[8]*0.001 + self._power3=value[9]*0.001 + self._powerN=value[10]*0.001 + self._power=value[11]*0.001 + if name=="amp": + self._amp = value + if name=="version": + self._version = value + if name=="ast": + self._AllowCharging = self._astValues[value] + if name=="fwv": + self._firmware = value + if name=="wss": + self._WifiSSID=value + if name=="ftt": + self._ftt=value + if name=="upd": + if value=="0": + self._updateAvailable = False + else: + self._updateAvailable = True + self.__call_event_handler("wp_property",name,value) + + def __on_hello(self,message): + _LOGGER.info("Connected to WattPilot Serial %s",message.serial) + if hasattr(message,"hostname"): + self._name=message.hostname + self.serial = message.serial + if hasattr(message,"hostname"): + self._hostname=message.hostname + if hasattr(message,"version"): + self._version=message.version + self._manufacturer=message.manufacturer + self._devicetype=message.devicetype + self._protocol=message.protocol + if hasattr(message,"secured"): + self._secured=message.secured + self.__call_event_handler("wp_hello",message) + + def __on_auth(self,wsapp,message): + ran = random.randrange(10**80) + self._token3 = "%064x" % ran + self._token3 = self._token3[:32] + hash1 = hashlib.sha256((message.token1.encode()+self._hashedpassword)).hexdigest() + hash = hashlib.sha256((self._token3 + message.token2+hash1).encode()).hexdigest() + response = {} + response["type"] = "auth" + response["token3"] = self._token3 + response["hash"] = hash + self.__send(response) + self.__call_event_handler("wp_auth",message) + + def __send(self,message,secure=False): + # If the connection to wattpilot is over a unsecure channel (http) all send messages are wrapped in + # a "securedMsg" Message which contains the original messageobject and a sha256 HMAC Hashed created + # using the password + if secure: + messageid=message["requestId"] + payload=json.dumps(message) + h = hmac.new(bytearray(self._hashedpassword), bytearray(payload.encode()), hashlib.sha256 ) + message={} + message["type"]="securedMsg" + message["data"]=payload + message["requestId"]=str(messageid)+"sm" + message["hmac"]=h.hexdigest() + + _LOGGER.debug("Message send: %s",json.dumps(message) ) + self._wsapp.send(json.dumps(message)) + + def __on_AuthSuccess(self,message): + self._connected = True + self.__call_event_handler("wp_authSuccess",message) + _LOGGER.info("Authentication successful") + + def __on_FullStatus(self,message): + props = message.status.__dict__ + for key in props: + self.__update_property(key,props[key]) + self.__call_event_handler("wp_fullStatus",message) + self._allPropsInitialized = not message.partial + _LOGGER.info("Status part") + if message.partial == False: + self.__call_event_handler("wp_fullStatus_finished",message) + _LOGGER.info("Status complete") + + def __on_AuthError(self,message): + if message.message=="Wrong password": + self._wsapp.close() + _LOGGER.error("Authentication failed: %s" , message.message) + self.__call_event_handler("wp_authError",message) + + def __on_DeltaStatus(self,message): + props = message.status.__dict__ + for key in props: + self.__update_property(key,props[key]) + self.__call_event_handler("wp_deltaStatus",message) + + + def __on_clearInverters(self,message): + self.__call_event_handler("wp_clearInverters",message) + + def __on_updateInverter(self,message): + self.__call_event_handler("wp_updateInverter",message) + + def __on_response(self,message): + if message.success: + if hasattr(message,"status"): + props = message.status.__dict__ + for key in props: + self.__update_property(key,props[key]) + else: + _LOGGER.error("Error Sending Request %s. Message: %s" ,message.requestId,message.message) + self.__call_event_handler("wp_response",message) + + def __on_open(self,wsapp): + self.__call_event_handler("ws_open",wsapp) + + def __on_error(self,wsapp,err): + self.__call_event_handler("ws_error",wsapp,err) + _LOGGER.error(f"Error received from WebSocketApp: {err}") + + def __on_close(self,wsapp,code,msg): + self._connected=False + self.__call_event_handler("ws_close",wsapp,code,msg) + if (self._auto_reconnect): + sleep(self._reconnect_interval) + self._wsapp.run_forever() + + def __on_message(self, wsapp, message): + ## called whenever a message through websocket is received + _LOGGER.debug("Message received: %s", message) + msg=json.loads(message, object_hook=lambda d: SimpleNamespace(**d)) + self.__call_event_handler("ws_message",message) + if (msg.type == 'hello'): # Hello Message -> Received upon connection before auth + self.__on_hello(msg) + if (msg.type == 'authRequired'): # Auth Required -> Received after hello + self.__on_auth(wsapp,msg) + if (msg.type == 'response'): # Response Message -> Received after sending a update and contains result of update + self.__on_response(msg) + if (msg.type == 'authSuccess'): # Auth Success -> Received after sending correct authentication message + self.__on_AuthSuccess(msg) + if (msg.type == 'authError'): # Auth Error -> Received after sending incorrect authentication message (e.g. wrong password) + self.__on_AuthError(msg) + if (msg.type == 'fullStatus'): # Full Status -> Received after successful connection. Contains all properties of Wattpilot + self.__on_FullStatus(msg) + if (msg.type == 'deltaStatus'): # Delta Status -> Whenever a property changes a Delta Status is send + self.__on_DeltaStatus(msg) + if (msg.type == 'clearInverters'): # Unknown + self.__on_clearInverters(msg) + if (msg.type == 'updateInverter'): # Contains information of connected Photovoltaik inverter / powermeter + self.__on_updateInverter(msg) + + + def __init__(self, ip ,password,serial=None,cloud=False): + self._auto_reconnect = True + self._reconnect_interval = 30 + self._websocket_default_timeout = 10 + self.__requestid = 0 + self._name = None + self._hostname = None + self._friendlyName = None + self._manufacturer = None + self._devicetype = None + self._protocol = None + self._secured = None + self._serial = None + self._password = None + + self.password = password + + if(cloud): + self._url= "wss://app.wattpilot.io/app/" + serial + "?version=1.2.9" + else: + self._url = "ws://"+ip+"/ws" + _LOGGER.info("URL: %s",self._url); + self.serial = None + self._connected = False + self._allProps={} + self._allPropsInitialized=False + self._voltage1=None + self._voltage2=None + self._voltage3=None + self._voltageN=None + self._amps1=None + self._amps2=None + self._amps3=None + self._power1=None + self._power2=None + self._power3=None + self._powerN=None + self._power=None + self._version = None + self._amp = None + self._AccessState = None + self._firmware = None + self._ftt = None + self._WifiSSID = None + self._AllowCharging = None + self._mode=None + self._carConnected=None + self._cae=None + self._cak=None + # Initialize callback lists: + self._event_handler = {} + for event_type in self.supported_events: + self._event_handler[event_type] = [] + + self._wst=threading.Thread() + + websocket.setdefaulttimeout(self._websocket_default_timeout) + self._wsapp = websocket.WebSocketApp( + self.url, + on_close=self.__on_close, + on_error=self.__on_error, + on_message=self.__on_message, + on_open=self.__on_open, + ) + self.__call_event_handler("wp_init") + _LOGGER.info ("Wattpilot %s initialized",self.serial) + + +def wp_handle_events(event, *args): + _LOGGER.debug(f"wp_handle_events(event={event},{args})") + _LOGGER.debug(f"wp_handle_events(): MQTT client not yet initialized - status publishing skipped.") + if event['type'] == 'fullStatus': + _LOGGER.debug(f"wp_handle_events(Hz={wp.frequency})") + return diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py new file mode 100644 index 0000000..6ffafdf --- /dev/null +++ b/aiohttp/__init__.py @@ -0,0 +1,230 @@ +__version__ = "4.0.0a2.dev0" + +from typing import TYPE_CHECKING, Tuple + +from . import hdrs +from .client import ( + BaseConnector, + ClientConnectionError, + ClientConnectorCertificateError, + ClientConnectorError, + ClientConnectorSSLError, + ClientError, + ClientHttpProxyError, + ClientOSError, + ClientPayloadError, + ClientProxyConnectionError, + ClientRequest, + ClientResponse, + ClientResponseError, + ClientSession, + ClientSSLError, + ClientTimeout, + ClientWebSocketResponse, + ContentTypeError, + Fingerprint, + InvalidURL, + NamedPipeConnector, + RequestInfo, + ServerConnectionError, + ServerDisconnectedError, + ServerFingerprintMismatch, + ServerTimeoutError, + TCPConnector, + TooManyRedirects, + UnixConnector, + WSServerHandshakeError, + request, +) +from .cookiejar import CookieJar, DummyCookieJar +from .formdata import FormData +from .helpers import BasicAuth, ChainMapProxy, ETag +from .http import ( + HttpVersion, + HttpVersion10, + HttpVersion11, + WebSocketError, + WSCloseCode, + WSMessage, + WSMsgType, +) +from .multipart import ( + BadContentDispositionHeader, + BadContentDispositionParam, + BodyPartReader, + MultipartReader, + MultipartWriter, + content_disposition_filename, + parse_content_disposition, +) +from .payload import ( + PAYLOAD_REGISTRY, + AsyncIterablePayload, + BufferedReaderPayload, + BytesIOPayload, + BytesPayload, + IOBasePayload, + JsonPayload, + Payload, + StringIOPayload, + StringPayload, + TextIOPayload, + get_payload, + payload_type, +) +from .resolver import AsyncResolver, DefaultResolver, ThreadedResolver +from .streams import ( + EMPTY_PAYLOAD, + DataQueue, + EofStream, + FlowControlDataQueue, + StreamReader, +) +from .tracing import ( + TraceConfig, + TraceConnectionCreateEndParams, + TraceConnectionCreateStartParams, + TraceConnectionQueuedEndParams, + TraceConnectionQueuedStartParams, + TraceConnectionReuseconnParams, + TraceDnsCacheHitParams, + TraceDnsCacheMissParams, + TraceDnsResolveHostEndParams, + TraceDnsResolveHostStartParams, + TraceRequestChunkSentParams, + TraceRequestEndParams, + TraceRequestExceptionParams, + TraceRequestRedirectParams, + TraceRequestStartParams, + TraceResponseChunkReceivedParams, +) + +if TYPE_CHECKING: # pragma: no cover + # At runtime these are lazy-loaded at the bottom of the file. + from .worker import GunicornUVLoopWebWorker, GunicornWebWorker + +__all__: Tuple[str, ...] = ( + "hdrs", + # client + "BaseConnector", + "ClientConnectionError", + "ClientConnectorCertificateError", + "ClientConnectorError", + "ClientConnectorSSLError", + "ClientError", + "ClientHttpProxyError", + "ClientOSError", + "ClientPayloadError", + "ClientProxyConnectionError", + "ClientResponse", + "ClientRequest", + "ClientResponseError", + "ClientSSLError", + "ClientSession", + "ClientTimeout", + "ClientWebSocketResponse", + "ContentTypeError", + "Fingerprint", + "InvalidURL", + "RequestInfo", + "ServerConnectionError", + "ServerDisconnectedError", + "ServerFingerprintMismatch", + "ServerTimeoutError", + "TCPConnector", + "TooManyRedirects", + "UnixConnector", + "NamedPipeConnector", + "WSServerHandshakeError", + "request", + # cookiejar + "CookieJar", + "DummyCookieJar", + # formdata + "FormData", + # helpers + "BasicAuth", + "ChainMapProxy", + "ETag", + # http + "HttpVersion", + "HttpVersion10", + "HttpVersion11", + "WSMsgType", + "WSCloseCode", + "WSMessage", + "WebSocketError", + # multipart + "BadContentDispositionHeader", + "BadContentDispositionParam", + "BodyPartReader", + "MultipartReader", + "MultipartWriter", + "content_disposition_filename", + "parse_content_disposition", + # payload + "AsyncIterablePayload", + "BufferedReaderPayload", + "BytesIOPayload", + "BytesPayload", + "IOBasePayload", + "JsonPayload", + "PAYLOAD_REGISTRY", + "Payload", + "StringIOPayload", + "StringPayload", + "TextIOPayload", + "get_payload", + "payload_type", + # resolver + "AsyncResolver", + "DefaultResolver", + "ThreadedResolver", + # streams + "DataQueue", + "EMPTY_PAYLOAD", + "EofStream", + "FlowControlDataQueue", + "StreamReader", + # tracing + "TraceConfig", + "TraceConnectionCreateEndParams", + "TraceConnectionCreateStartParams", + "TraceConnectionQueuedEndParams", + "TraceConnectionQueuedStartParams", + "TraceConnectionReuseconnParams", + "TraceDnsCacheHitParams", + "TraceDnsCacheMissParams", + "TraceDnsResolveHostEndParams", + "TraceDnsResolveHostStartParams", + "TraceRequestChunkSentParams", + "TraceRequestEndParams", + "TraceRequestExceptionParams", + "TraceRequestRedirectParams", + "TraceRequestStartParams", + "TraceResponseChunkReceivedParams", + # workers (imported lazily with __getattr__) + "GunicornUVLoopWebWorker", + "GunicornWebWorker", +) + + +def __dir__() -> Tuple[str, ...]: + return __all__ + ("__author__", "__doc__") + + +def __getattr__(name: str) -> object: + global GunicornUVLoopWebWorker, GunicornWebWorker + + # Importing gunicorn takes a long time (>100ms), so only import if actually needed. + if name in ("GunicornUVLoopWebWorker", "GunicornWebWorker"): + try: + from .worker import GunicornUVLoopWebWorker as guv, GunicornWebWorker as gw + except ImportError: + return None + + GunicornUVLoopWebWorker = guv # type: ignore[misc] + GunicornWebWorker = gw # type: ignore[misc] + return guv if name == "GunicornUVLoopWebWorker" else gw + + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/aiohttp/_cparser.pxd b/aiohttp/_cparser.pxd new file mode 100644 index 0000000..c2cd5a9 --- /dev/null +++ b/aiohttp/_cparser.pxd @@ -0,0 +1,158 @@ +from libc.stdint cimport int32_t, uint8_t, uint16_t, uint64_t + + +cdef extern from "../vendor/llhttp/build/llhttp.h": + + struct llhttp__internal_s: + int32_t _index + void* _span_pos0 + void* _span_cb0 + int32_t error + const char* reason + const char* error_pos + void* data + void* _current + uint64_t content_length + uint8_t type + uint8_t method + uint8_t http_major + uint8_t http_minor + uint8_t header_state + uint8_t lenient_flags + uint8_t upgrade + uint8_t finish + uint16_t flags + uint16_t status_code + void* settings + + ctypedef llhttp__internal_s llhttp__internal_t + ctypedef llhttp__internal_t llhttp_t + + ctypedef int (*llhttp_data_cb)(llhttp_t*, const char *at, size_t length) except -1 + ctypedef int (*llhttp_cb)(llhttp_t*) except -1 + + struct llhttp_settings_s: + llhttp_cb on_message_begin + llhttp_data_cb on_url + llhttp_data_cb on_status + llhttp_data_cb on_header_field + llhttp_data_cb on_header_value + llhttp_cb on_headers_complete + llhttp_data_cb on_body + llhttp_cb on_message_complete + llhttp_cb on_chunk_header + llhttp_cb on_chunk_complete + + llhttp_cb on_url_complete + llhttp_cb on_status_complete + llhttp_cb on_header_field_complete + llhttp_cb on_header_value_complete + + ctypedef llhttp_settings_s llhttp_settings_t + + enum llhttp_errno: + HPE_OK, + HPE_INTERNAL, + HPE_STRICT, + HPE_LF_EXPECTED, + HPE_UNEXPECTED_CONTENT_LENGTH, + HPE_CLOSED_CONNECTION, + HPE_INVALID_METHOD, + HPE_INVALID_URL, + HPE_INVALID_CONSTANT, + HPE_INVALID_VERSION, + HPE_INVALID_HEADER_TOKEN, + HPE_INVALID_CONTENT_LENGTH, + HPE_INVALID_CHUNK_SIZE, + HPE_INVALID_STATUS, + HPE_INVALID_EOF_STATE, + HPE_INVALID_TRANSFER_ENCODING, + HPE_CB_MESSAGE_BEGIN, + HPE_CB_HEADERS_COMPLETE, + HPE_CB_MESSAGE_COMPLETE, + HPE_CB_CHUNK_HEADER, + HPE_CB_CHUNK_COMPLETE, + HPE_PAUSED, + HPE_PAUSED_UPGRADE, + HPE_USER + + ctypedef llhttp_errno llhttp_errno_t + + enum llhttp_flags: + F_CHUNKED, + F_CONTENT_LENGTH + + enum llhttp_type: + HTTP_REQUEST, + HTTP_RESPONSE, + HTTP_BOTH + + enum llhttp_method: + HTTP_DELETE, + HTTP_GET, + HTTP_HEAD, + HTTP_POST, + HTTP_PUT, + HTTP_CONNECT, + HTTP_OPTIONS, + HTTP_TRACE, + HTTP_COPY, + HTTP_LOCK, + HTTP_MKCOL, + HTTP_MOVE, + HTTP_PROPFIND, + HTTP_PROPPATCH, + HTTP_SEARCH, + HTTP_UNLOCK, + HTTP_BIND, + HTTP_REBIND, + HTTP_UNBIND, + HTTP_ACL, + HTTP_REPORT, + HTTP_MKACTIVITY, + HTTP_CHECKOUT, + HTTP_MERGE, + HTTP_MSEARCH, + HTTP_NOTIFY, + HTTP_SUBSCRIBE, + HTTP_UNSUBSCRIBE, + HTTP_PATCH, + HTTP_PURGE, + HTTP_MKCALENDAR, + HTTP_LINK, + HTTP_UNLINK, + HTTP_SOURCE, + HTTP_PRI, + HTTP_DESCRIBE, + HTTP_ANNOUNCE, + HTTP_SETUP, + HTTP_PLAY, + HTTP_PAUSE, + HTTP_TEARDOWN, + HTTP_GET_PARAMETER, + HTTP_SET_PARAMETER, + HTTP_REDIRECT, + HTTP_RECORD, + HTTP_FLUSH + + ctypedef llhttp_method llhttp_method_t; + + void llhttp_settings_init(llhttp_settings_t* settings) + void llhttp_init(llhttp_t* parser, llhttp_type type, + const llhttp_settings_t* settings) + + llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len) + + int llhttp_should_keep_alive(const llhttp_t* parser) + + void llhttp_resume_after_upgrade(llhttp_t* parser) + + llhttp_errno_t llhttp_get_errno(const llhttp_t* parser) + const char* llhttp_get_error_reason(const llhttp_t* parser) + const char* llhttp_get_error_pos(const llhttp_t* parser) + + const char* llhttp_method_name(llhttp_method_t method) + + void llhttp_set_lenient_headers(llhttp_t* parser, int enabled) + void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled) + void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled) diff --git a/aiohttp/_find_header.h b/aiohttp/_find_header.h new file mode 100644 index 0000000..99b7b4f --- /dev/null +++ b/aiohttp/_find_header.h @@ -0,0 +1,14 @@ +#ifndef _FIND_HEADERS_H +#define _FIND_HEADERS_H + +#ifdef __cplusplus +extern "C" { +#endif + +int find_header(const char *str, int size); + + +#ifdef __cplusplus +} +#endif +#endif diff --git a/aiohttp/_find_header.pxd b/aiohttp/_find_header.pxd new file mode 100644 index 0000000..37a6c37 --- /dev/null +++ b/aiohttp/_find_header.pxd @@ -0,0 +1,2 @@ +cdef extern from "_find_header.h": + int find_header(char *, int) diff --git a/aiohttp/_helpers.pyi b/aiohttp/_helpers.pyi new file mode 100644 index 0000000..1e35893 --- /dev/null +++ b/aiohttp/_helpers.pyi @@ -0,0 +1,6 @@ +from typing import Any + +class reify: + def __init__(self, wrapped: Any) -> None: ... + def __get__(self, inst: Any, owner: Any) -> Any: ... + def __set__(self, inst: Any, value: Any) -> None: ... diff --git a/aiohttp/_helpers.pyx b/aiohttp/_helpers.pyx new file mode 100644 index 0000000..665f367 --- /dev/null +++ b/aiohttp/_helpers.pyx @@ -0,0 +1,35 @@ +cdef class reify: + """Use as a class method decorator. It operates almost exactly like + the Python `@property` decorator, but it puts the result of the + method it decorates into the instance dict after the first call, + effectively replacing the function it decorates with an instance + variable. It is, in Python parlance, a data descriptor. + + """ + + cdef object wrapped + cdef object name + + def __init__(self, wrapped): + self.wrapped = wrapped + self.name = wrapped.__name__ + + @property + def __doc__(self): + return self.wrapped.__doc__ + + def __get__(self, inst, owner): + try: + try: + return inst._cache[self.name] + except KeyError: + val = self.wrapped(inst) + inst._cache[self.name] = val + return val + except AttributeError: + if inst is None: + return self + raise + + def __set__(self, inst, value): + raise AttributeError("reified property is read-only") diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx new file mode 100644 index 0000000..2b4b844 --- /dev/null +++ b/aiohttp/_http_parser.pyx @@ -0,0 +1,844 @@ +#cython: language_level=3 +# +# Based on https://github.com/MagicStack/httptools +# +from __future__ import absolute_import, print_function + +from cpython cimport ( + Py_buffer, + PyBUF_SIMPLE, + PyBuffer_Release, + PyBytes_AsString, + PyBytes_AsStringAndSize, + PyObject_GetBuffer, +) +from cpython.mem cimport PyMem_Free, PyMem_Malloc +from libc.limits cimport ULLONG_MAX +from libc.string cimport memcpy + +from multidict import CIMultiDict as _CIMultiDict, CIMultiDictProxy as _CIMultiDictProxy +from yarl import URL as _URL + +from aiohttp import hdrs +from aiohttp.helpers import DEBUG + +from .http_exceptions import ( + BadHttpMessage, + BadStatusLine, + ContentLengthError, + InvalidHeader, + InvalidURLError, + LineTooLong, + PayloadEncodingError, + TransferEncodingError, +) +from .http_parser import DeflateBuffer as _DeflateBuffer +from .http_writer import ( + HttpVersion as _HttpVersion, + HttpVersion10 as _HttpVersion10, + HttpVersion11 as _HttpVersion11, +) +from .streams import EMPTY_PAYLOAD as _EMPTY_PAYLOAD, StreamReader as _StreamReader + +cimport cython + +from aiohttp cimport _cparser as cparser + +include "_headers.pxi" + +from aiohttp cimport _find_header + +DEF DEFAULT_FREELIST_SIZE = 250 + +cdef extern from "Python.h": + int PyByteArray_Resize(object, Py_ssize_t) except -1 + Py_ssize_t PyByteArray_Size(object) except -1 + char* PyByteArray_AsString(object) + +__all__ = ('HttpRequestParser', 'HttpResponseParser', + 'RawRequestMessage', 'RawResponseMessage') + +cdef object URL = _URL +cdef object URL_build = URL.build +cdef object CIMultiDict = _CIMultiDict +cdef object CIMultiDictProxy = _CIMultiDictProxy +cdef object HttpVersion = _HttpVersion +cdef object HttpVersion10 = _HttpVersion10 +cdef object HttpVersion11 = _HttpVersion11 +cdef object SEC_WEBSOCKET_KEY1 = hdrs.SEC_WEBSOCKET_KEY1 +cdef object CONTENT_ENCODING = hdrs.CONTENT_ENCODING +cdef object EMPTY_PAYLOAD = _EMPTY_PAYLOAD +cdef object StreamReader = _StreamReader +cdef object DeflateBuffer = _DeflateBuffer + + +cdef inline object extend(object buf, const char* at, size_t length): + cdef Py_ssize_t s + cdef char* ptr + s = PyByteArray_Size(buf) + PyByteArray_Resize(buf, s + length) + ptr = PyByteArray_AsString(buf) + memcpy(ptr + s, at, length) + + +DEF METHODS_COUNT = 46; + +cdef list _http_method = [] + +for i in range(METHODS_COUNT): + _http_method.append( + cparser.llhttp_method_name( i).decode('ascii')) + + +cdef inline str http_method_str(int i): + if i < METHODS_COUNT: + return _http_method[i] + else: + return "" + +cdef inline object find_header(bytes raw_header): + cdef Py_ssize_t size + cdef char *buf + cdef int idx + PyBytes_AsStringAndSize(raw_header, &buf, &size) + idx = _find_header.find_header(buf, size) + if idx == -1: + return raw_header.decode('utf-8', 'surrogateescape') + return headers[idx] + + +@cython.freelist(DEFAULT_FREELIST_SIZE) +cdef class RawRequestMessage: + cdef readonly str method + cdef readonly str path + cdef readonly object version # HttpVersion + cdef readonly object headers # CIMultiDict + cdef readonly object raw_headers # tuple + cdef readonly object should_close + cdef readonly object compression + cdef readonly object upgrade + cdef readonly object chunked + cdef readonly object url # yarl.URL + + def __init__(self, method, path, version, headers, raw_headers, + should_close, compression, upgrade, chunked, url): + self.method = method + self.path = path + self.version = version + self.headers = headers + self.raw_headers = raw_headers + self.should_close = should_close + self.compression = compression + self.upgrade = upgrade + self.chunked = chunked + self.url = url + + def __repr__(self): + info = [] + info.append(("method", self.method)) + info.append(("path", self.path)) + info.append(("version", self.version)) + info.append(("headers", self.headers)) + info.append(("raw_headers", self.raw_headers)) + info.append(("should_close", self.should_close)) + info.append(("compression", self.compression)) + info.append(("upgrade", self.upgrade)) + info.append(("chunked", self.chunked)) + info.append(("url", self.url)) + sinfo = ', '.join(name + '=' + repr(val) for name, val in info) + return '' + + def _replace(self, **dct): + cdef RawRequestMessage ret + ret = _new_request_message(self.method, + self.path, + self.version, + self.headers, + self.raw_headers, + self.should_close, + self.compression, + self.upgrade, + self.chunked, + self.url) + if "method" in dct: + ret.method = dct["method"] + if "path" in dct: + ret.path = dct["path"] + if "version" in dct: + ret.version = dct["version"] + if "headers" in dct: + ret.headers = dct["headers"] + if "raw_headers" in dct: + ret.raw_headers = dct["raw_headers"] + if "should_close" in dct: + ret.should_close = dct["should_close"] + if "compression" in dct: + ret.compression = dct["compression"] + if "upgrade" in dct: + ret.upgrade = dct["upgrade"] + if "chunked" in dct: + ret.chunked = dct["chunked"] + if "url" in dct: + ret.url = dct["url"] + return ret + +cdef _new_request_message(str method, + str path, + object version, + object headers, + object raw_headers, + bint should_close, + object compression, + bint upgrade, + bint chunked, + object url): + cdef RawRequestMessage ret + ret = RawRequestMessage.__new__(RawRequestMessage) + ret.method = method + ret.path = path + ret.version = version + ret.headers = headers + ret.raw_headers = raw_headers + ret.should_close = should_close + ret.compression = compression + ret.upgrade = upgrade + ret.chunked = chunked + ret.url = url + return ret + + +@cython.freelist(DEFAULT_FREELIST_SIZE) +cdef class RawResponseMessage: + cdef readonly object version # HttpVersion + cdef readonly int code + cdef readonly str reason + cdef readonly object headers # CIMultiDict + cdef readonly object raw_headers # tuple + cdef readonly object should_close + cdef readonly object compression + cdef readonly object upgrade + cdef readonly object chunked + + def __init__(self, version, code, reason, headers, raw_headers, + should_close, compression, upgrade, chunked): + self.version = version + self.code = code + self.reason = reason + self.headers = headers + self.raw_headers = raw_headers + self.should_close = should_close + self.compression = compression + self.upgrade = upgrade + self.chunked = chunked + + def __repr__(self): + info = [] + info.append(("version", self.version)) + info.append(("code", self.code)) + info.append(("reason", self.reason)) + info.append(("headers", self.headers)) + info.append(("raw_headers", self.raw_headers)) + info.append(("should_close", self.should_close)) + info.append(("compression", self.compression)) + info.append(("upgrade", self.upgrade)) + info.append(("chunked", self.chunked)) + sinfo = ', '.join(name + '=' + repr(val) for name, val in info) + return '' + + +cdef _new_response_message(object version, + int code, + str reason, + object headers, + object raw_headers, + bint should_close, + object compression, + bint upgrade, + bint chunked): + cdef RawResponseMessage ret + ret = RawResponseMessage.__new__(RawResponseMessage) + ret.version = version + ret.code = code + ret.reason = reason + ret.headers = headers + ret.raw_headers = raw_headers + ret.should_close = should_close + ret.compression = compression + ret.upgrade = upgrade + ret.chunked = chunked + return ret + + +@cython.internal +cdef class HttpParser: + + cdef: + cparser.llhttp_t* _cparser + cparser.llhttp_settings_t* _csettings + + bytearray _raw_name + bytearray _raw_value + bint _has_value + + object _protocol + object _loop + object _timer + + size_t _max_line_size + size_t _max_field_size + size_t _max_headers + bint _response_with_body + bint _read_until_eof + + bint _started + object _url + bytearray _buf + str _path + str _reason + object _headers + list _raw_headers + bint _upgraded + list _messages + object _payload + bint _payload_error + object _payload_exception + object _last_error + bint _auto_decompress + int _limit + + str _content_encoding + + Py_buffer py_buf + + def __cinit__(self): + self._cparser = \ + PyMem_Malloc(sizeof(cparser.llhttp_t)) + if self._cparser is NULL: + raise MemoryError() + + self._csettings = \ + PyMem_Malloc(sizeof(cparser.llhttp_settings_t)) + if self._csettings is NULL: + raise MemoryError() + + def __dealloc__(self): + PyMem_Free(self._cparser) + PyMem_Free(self._csettings) + + cdef _init( + self, cparser.llhttp_type mode, + object protocol, object loop, int limit, + object timer=None, + size_t max_line_size=8190, size_t max_headers=32768, + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, + bint auto_decompress=True, + ): + cparser.llhttp_settings_init(self._csettings) + cparser.llhttp_init(self._cparser, mode, self._csettings) + self._cparser.data = self + self._cparser.content_length = 0 + + self._protocol = protocol + self._loop = loop + self._timer = timer + + self._buf = bytearray() + self._payload = None + self._payload_error = 0 + self._payload_exception = payload_exception + self._messages = [] + + self._raw_name = bytearray() + self._raw_value = bytearray() + self._has_value = False + + self._max_line_size = max_line_size + self._max_headers = max_headers + self._max_field_size = max_field_size + self._response_with_body = response_with_body + self._read_until_eof = read_until_eof + self._upgraded = False + self._auto_decompress = auto_decompress + self._content_encoding = None + + self._csettings.on_url = cb_on_url + self._csettings.on_status = cb_on_status + self._csettings.on_header_field = cb_on_header_field + self._csettings.on_header_value = cb_on_header_value + self._csettings.on_headers_complete = cb_on_headers_complete + self._csettings.on_body = cb_on_body + self._csettings.on_message_begin = cb_on_message_begin + self._csettings.on_message_complete = cb_on_message_complete + self._csettings.on_chunk_header = cb_on_chunk_header + self._csettings.on_chunk_complete = cb_on_chunk_complete + + self._last_error = None + self._limit = limit + + cdef _process_header(self): + if self._raw_name: + raw_name = bytes(self._raw_name) + raw_value = bytes(self._raw_value) + + name = find_header(raw_name) + value = raw_value.decode('utf-8', 'surrogateescape') + + self._headers.add(name, value) + + if name is CONTENT_ENCODING: + self._content_encoding = value + + PyByteArray_Resize(self._raw_name, 0) + PyByteArray_Resize(self._raw_value, 0) + self._has_value = False + self._raw_headers.append((raw_name, raw_value)) + + cdef _on_header_field(self, char* at, size_t length): + cdef Py_ssize_t size + cdef char *buf + if self._has_value: + self._process_header() + + size = PyByteArray_Size(self._raw_name) + PyByteArray_Resize(self._raw_name, size + length) + buf = PyByteArray_AsString(self._raw_name) + memcpy(buf + size, at, length) + + cdef _on_header_value(self, char* at, size_t length): + cdef Py_ssize_t size + cdef char *buf + + size = PyByteArray_Size(self._raw_value) + PyByteArray_Resize(self._raw_value, size + length) + buf = PyByteArray_AsString(self._raw_value) + memcpy(buf + size, at, length) + self._has_value = True + + cdef _on_headers_complete(self): + self._process_header() + + method = http_method_str(self._cparser.method) + should_close = not cparser.llhttp_should_keep_alive(self._cparser) + upgrade = self._cparser.upgrade + chunked = self._cparser.flags & cparser.F_CHUNKED + + raw_headers = tuple(self._raw_headers) + headers = CIMultiDictProxy(self._headers) + + if upgrade or self._cparser.method == cparser.HTTP_CONNECT: + self._upgraded = True + + # do not support old websocket spec + if SEC_WEBSOCKET_KEY1 in headers: + raise InvalidHeader(SEC_WEBSOCKET_KEY1) + + encoding = None + enc = self._content_encoding + if enc is not None: + self._content_encoding = None + enc = enc.lower() + if enc in ('gzip', 'deflate', 'br'): + encoding = enc + + if self._cparser.type == cparser.HTTP_REQUEST: + msg = _new_request_message( + method, self._path, + self.http_version(), headers, raw_headers, + should_close, encoding, upgrade, chunked, self._url) + else: + msg = _new_response_message( + self.http_version(), self._cparser.status_code, self._reason, + headers, raw_headers, should_close, encoding, + upgrade, chunked) + + if ( + ULLONG_MAX > self._cparser.content_length > 0 or chunked or + self._cparser.method == cparser.HTTP_CONNECT or + (self._cparser.status_code >= 199 and + self._cparser.content_length == 0 and + self._read_until_eof) + ): + payload = StreamReader( + self._protocol, timer=self._timer, loop=self._loop, + limit=self._limit) + else: + payload = EMPTY_PAYLOAD + + self._payload = payload + if encoding is not None and self._auto_decompress: + self._payload = DeflateBuffer(payload, encoding) + + if not self._response_with_body: + payload = EMPTY_PAYLOAD + + self._messages.append((msg, payload)) + + cdef _on_message_complete(self): + self._payload.feed_eof() + self._payload = None + + cdef _on_chunk_header(self): + self._payload.begin_http_chunk_receiving() + + cdef _on_chunk_complete(self): + self._payload.end_http_chunk_receiving() + + cdef object _on_status_complete(self): + pass + + cdef inline http_version(self): + cdef cparser.llhttp_t* parser = self._cparser + + if parser.http_major == 1: + if parser.http_minor == 0: + return HttpVersion10 + elif parser.http_minor == 1: + return HttpVersion11 + + return HttpVersion(parser.http_major, parser.http_minor) + + ### Public API ### + + def feed_eof(self): + cdef bytes desc + + if self._payload is not None: + if self._cparser.flags & cparser.F_CHUNKED: + raise TransferEncodingError( + "Not enough data for satisfy transfer length header.") + elif self._cparser.flags & cparser.F_CONTENT_LENGTH: + raise ContentLengthError( + "Not enough data for satisfy content length header.") + elif cparser.llhttp_get_errno(self._cparser) != cparser.HPE_OK: + desc = cparser.llhttp_get_error_reason(self._cparser) + raise PayloadEncodingError(desc.decode('latin-1')) + else: + self._payload.feed_eof() + elif self._started: + self._on_headers_complete() + if self._messages: + return self._messages[-1][0] + + def feed_data(self, data): + cdef: + size_t data_len + size_t nb + cdef cparser.llhttp_errno_t errno + + PyObject_GetBuffer(data, &self.py_buf, PyBUF_SIMPLE) + data_len = self.py_buf.len + + errno = cparser.llhttp_execute( + self._cparser, + self.py_buf.buf, + data_len) + + if errno is cparser.HPE_PAUSED_UPGRADE: + cparser.llhttp_resume_after_upgrade(self._cparser) + + nb = cparser.llhttp_get_error_pos(self._cparser) - self.py_buf.buf + + PyBuffer_Release(&self.py_buf) + + if errno not in (cparser.HPE_OK, cparser.HPE_PAUSED_UPGRADE): + if self._payload_error == 0: + if self._last_error is not None: + ex = self._last_error + self._last_error = None + else: + after = cparser.llhttp_get_error_pos(self._cparser) + before = data[:after - self.py_buf.buf] + after_b = after.split(b"\r\n", 1)[0] + before = before.rsplit(b"\r\n", 1)[-1] + data = before + after_b + pointer = " " * (len(repr(before))-1) + "^" + ex = parser_error_from_errno(self._cparser, data, pointer) + self._payload = None + raise ex + + if self._messages: + messages = self._messages + self._messages = [] + else: + messages = () + + if self._upgraded: + return messages, True, data[nb:] + else: + return messages, False, b'' + + def set_upgraded(self, val): + self._upgraded = val + + +cdef class HttpRequestParser(HttpParser): + + def __init__( + self, protocol, loop, int limit, timer=None, + size_t max_line_size=8190, size_t max_headers=32768, + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, + bint auto_decompress=True, + ): + self._init(cparser.HTTP_REQUEST, protocol, loop, limit, timer, + max_line_size, max_headers, max_field_size, + payload_exception, response_with_body, read_until_eof, + auto_decompress) + + cdef object _on_status_complete(self): + cdef int idx1, idx2 + if not self._buf: + return + self._path = self._buf.decode('utf-8', 'surrogateescape') + try: + idx3 = len(self._path) + if self._cparser.method == cparser.HTTP_CONNECT: + # authority-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3 + self._url = URL.build(authority=self._path, encoded=True) + elif idx3 > 1 and self._path[0] == '/': + # origin-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1 + idx1 = self._path.find("?") + if idx1 == -1: + query = "" + idx2 = self._path.find("#") + if idx2 == -1: + path = self._path + fragment = "" + else: + path = self._path[0: idx2] + fragment = self._path[idx2+1:] + + else: + path = self._path[0:idx1] + idx1 += 1 + idx2 = self._path.find("#", idx1+1) + if idx2 == -1: + query = self._path[idx1:] + fragment = "" + else: + query = self._path[idx1: idx2] + fragment = self._path[idx2+1:] + + self._url = URL.build( + path=path, + query_string=query, + fragment=fragment, + encoded=True, + ) + else: + # absolute-form for proxy maybe, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2 + self._url = URL(self._path, encoded=True) + finally: + PyByteArray_Resize(self._buf, 0) + + +cdef class HttpResponseParser(HttpParser): + + def __init__( + self, protocol, loop, int limit, timer=None, + size_t max_line_size=8190, size_t max_headers=32768, + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, + bint auto_decompress=True + ): + self._init(cparser.HTTP_RESPONSE, protocol, loop, limit, timer, + max_line_size, max_headers, max_field_size, + payload_exception, response_with_body, read_until_eof, + auto_decompress) + # Use strict parsing on dev mode, so users are warned about broken servers. + if not DEBUG: + cparser.llhttp_set_lenient_headers(self._cparser, 1) + cparser.llhttp_set_lenient_optional_cr_before_lf(self._cparser, 1) + cparser.llhttp_set_lenient_spaces_after_chunk_size(self._cparser, 1) + + cdef object _on_status_complete(self): + if self._buf: + self._reason = self._buf.decode('utf-8', 'surrogateescape') + PyByteArray_Resize(self._buf, 0) + else: + self._reason = self._reason or '' + +cdef int cb_on_message_begin(cparser.llhttp_t* parser) except -1: + cdef HttpParser pyparser = parser.data + + pyparser._started = True + pyparser._headers = CIMultiDict() + pyparser._raw_headers = [] + PyByteArray_Resize(pyparser._buf, 0) + pyparser._path = None + pyparser._reason = None + return 0 + + +cdef int cb_on_url(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + try: + if length > pyparser._max_line_size: + raise LineTooLong( + 'Status line is too long', pyparser._max_line_size, length) + extend(pyparser._buf, at, length) + except BaseException as ex: + pyparser._last_error = ex + return -1 + else: + return 0 + + +cdef int cb_on_status(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + cdef str reason + try: + if length > pyparser._max_line_size: + raise LineTooLong( + 'Status line is too long', pyparser._max_line_size, length) + extend(pyparser._buf, at, length) + except BaseException as ex: + pyparser._last_error = ex + return -1 + else: + return 0 + + +cdef int cb_on_header_field(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + cdef Py_ssize_t size + try: + pyparser._on_status_complete() + size = len(pyparser._raw_name) + length + if size > pyparser._max_field_size: + raise LineTooLong( + 'Header name is too long', pyparser._max_field_size, size) + pyparser._on_header_field(at, length) + except BaseException as ex: + pyparser._last_error = ex + return -1 + else: + return 0 + + +cdef int cb_on_header_value(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + cdef Py_ssize_t size + try: + size = len(pyparser._raw_value) + length + if size > pyparser._max_field_size: + raise LineTooLong( + 'Header value is too long', pyparser._max_field_size, size) + pyparser._on_header_value(at, length) + except BaseException as ex: + pyparser._last_error = ex + return -1 + else: + return 0 + + +cdef int cb_on_headers_complete(cparser.llhttp_t* parser) except -1: + cdef HttpParser pyparser = parser.data + try: + pyparser._on_status_complete() + pyparser._on_headers_complete() + except BaseException as exc: + pyparser._last_error = exc + return -1 + else: + if ( + pyparser._cparser.upgrade or + pyparser._cparser.method == cparser.HTTP_CONNECT + ): + return 2 + else: + return 0 + + +cdef int cb_on_body(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + cdef bytes body = at[:length] + try: + pyparser._payload.feed_data(body, length) + except BaseException as exc: + if pyparser._payload_exception is not None: + pyparser._payload.set_exception(pyparser._payload_exception(str(exc))) + else: + pyparser._payload.set_exception(exc) + pyparser._payload_error = 1 + return -1 + else: + return 0 + + +cdef int cb_on_message_complete(cparser.llhttp_t* parser) except -1: + cdef HttpParser pyparser = parser.data + try: + pyparser._started = False + pyparser._on_message_complete() + except BaseException as exc: + pyparser._last_error = exc + return -1 + else: + return 0 + + +cdef int cb_on_chunk_header(cparser.llhttp_t* parser) except -1: + cdef HttpParser pyparser = parser.data + try: + pyparser._on_chunk_header() + except BaseException as exc: + pyparser._last_error = exc + return -1 + else: + return 0 + + +cdef int cb_on_chunk_complete(cparser.llhttp_t* parser) except -1: + cdef HttpParser pyparser = parser.data + try: + pyparser._on_chunk_complete() + except BaseException as exc: + pyparser._last_error = exc + return -1 + else: + return 0 + + +cdef parser_error_from_errno(cparser.llhttp_t* parser, data, pointer): + cdef cparser.llhttp_errno_t errno = cparser.llhttp_get_errno(parser) + cdef bytes desc = cparser.llhttp_get_error_reason(parser) + + if errno in (cparser.HPE_CB_MESSAGE_BEGIN, + cparser.HPE_CB_HEADERS_COMPLETE, + cparser.HPE_CB_MESSAGE_COMPLETE, + cparser.HPE_CB_CHUNK_HEADER, + cparser.HPE_CB_CHUNK_COMPLETE, + cparser.HPE_INVALID_CONSTANT, + cparser.HPE_INVALID_HEADER_TOKEN, + cparser.HPE_INVALID_CONTENT_LENGTH, + cparser.HPE_INVALID_CHUNK_SIZE, + cparser.HPE_INVALID_EOF_STATE, + cparser.HPE_INVALID_TRANSFER_ENCODING): + cls = BadHttpMessage + + elif errno == cparser.HPE_INVALID_STATUS: + cls = BadStatusLine + + elif errno == cparser.HPE_INVALID_METHOD: + cls = BadStatusLine + + elif errno == cparser.HPE_INVALID_VERSION: + cls = BadStatusLine + + elif errno == cparser.HPE_INVALID_URL: + cls = InvalidURLError + + else: + cls = BadHttpMessage + + return cls("{}:\n\n {!r}\n {}".format(desc.decode("latin-1"), data, pointer)) diff --git a/aiohttp/_http_writer.pyx b/aiohttp/_http_writer.pyx new file mode 100644 index 0000000..eff8521 --- /dev/null +++ b/aiohttp/_http_writer.pyx @@ -0,0 +1,163 @@ +from cpython.bytes cimport PyBytes_FromStringAndSize +from cpython.exc cimport PyErr_NoMemory +from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc +from cpython.object cimport PyObject_Str +from libc.stdint cimport uint8_t, uint64_t +from libc.string cimport memcpy + +from multidict import istr + +DEF BUF_SIZE = 16 * 1024 # 16KiB +cdef char BUFFER[BUF_SIZE] + +cdef object _istr = istr + + +# ----------------- writer --------------------------- + +cdef struct Writer: + char *buf + Py_ssize_t size + Py_ssize_t pos + + +cdef inline void _init_writer(Writer* writer): + writer.buf = &BUFFER[0] + writer.size = BUF_SIZE + writer.pos = 0 + + +cdef inline void _release_writer(Writer* writer): + if writer.buf != BUFFER: + PyMem_Free(writer.buf) + + +cdef inline int _write_byte(Writer* writer, uint8_t ch): + cdef char * buf + cdef Py_ssize_t size + + if writer.pos == writer.size: + # reallocate + size = writer.size + BUF_SIZE + if writer.buf == BUFFER: + buf = PyMem_Malloc(size) + if buf == NULL: + PyErr_NoMemory() + return -1 + memcpy(buf, writer.buf, writer.size) + else: + buf = PyMem_Realloc(writer.buf, size) + if buf == NULL: + PyErr_NoMemory() + return -1 + writer.buf = buf + writer.size = size + writer.buf[writer.pos] = ch + writer.pos += 1 + return 0 + + +cdef inline int _write_utf8(Writer* writer, Py_UCS4 symbol): + cdef uint64_t utf = symbol + + if utf < 0x80: + return _write_byte(writer, utf) + elif utf < 0x800: + if _write_byte(writer, (0xc0 | (utf >> 6))) < 0: + return -1 + return _write_byte(writer, (0x80 | (utf & 0x3f))) + elif 0xD800 <= utf <= 0xDFFF: + # surogate pair, ignored + return 0 + elif utf < 0x10000: + if _write_byte(writer, (0xe0 | (utf >> 12))) < 0: + return -1 + if _write_byte(writer, (0x80 | ((utf >> 6) & 0x3f))) < 0: + return -1 + return _write_byte(writer, (0x80 | (utf & 0x3f))) + elif utf > 0x10FFFF: + # symbol is too large + return 0 + else: + if _write_byte(writer, (0xf0 | (utf >> 18))) < 0: + return -1 + if _write_byte(writer, + (0x80 | ((utf >> 12) & 0x3f))) < 0: + return -1 + if _write_byte(writer, + (0x80 | ((utf >> 6) & 0x3f))) < 0: + return -1 + return _write_byte(writer, (0x80 | (utf & 0x3f))) + + +cdef inline int _write_str(Writer* writer, str s): + cdef Py_UCS4 ch + for ch in s: + if _write_utf8(writer, ch) < 0: + return -1 + + +# --------------- _serialize_headers ---------------------- + +cdef str to_str(object s): + typ = type(s) + if typ is str: + return s + elif typ is _istr: + return PyObject_Str(s) + elif not isinstance(s, str): + raise TypeError("Cannot serialize non-str key {!r}".format(s)) + else: + return str(s) + + +cdef void _safe_header(str string) except *: + if "\r" in string or "\n" in string: + raise ValueError( + "Newline or carriage return character detected in HTTP status message or " + "header. This is a potential security issue." + ) + + +def _serialize_headers(str status_line, headers): + cdef Writer writer + cdef object key + cdef object val + cdef bytes ret + + _init_writer(&writer) + + for key, val in headers.items(): + _safe_header(to_str(key)) + _safe_header(to_str(val)) + + try: + if _write_str(&writer, status_line) < 0: + raise + if _write_byte(&writer, b'\r') < 0: + raise + if _write_byte(&writer, b'\n') < 0: + raise + + for key, val in headers.items(): + if _write_str(&writer, to_str(key)) < 0: + raise + if _write_byte(&writer, b':') < 0: + raise + if _write_byte(&writer, b' ') < 0: + raise + if _write_str(&writer, to_str(val)) < 0: + raise + if _write_byte(&writer, b'\r') < 0: + raise + if _write_byte(&writer, b'\n') < 0: + raise + + if _write_byte(&writer, b'\r') < 0: + raise + if _write_byte(&writer, b'\n') < 0: + raise + + return PyBytes_FromStringAndSize(writer.buf, writer.pos) + finally: + _release_writer(&writer) diff --git a/aiohttp/_websocket.pyx b/aiohttp/_websocket.pyx new file mode 100644 index 0000000..94318d2 --- /dev/null +++ b/aiohttp/_websocket.pyx @@ -0,0 +1,56 @@ +from cpython cimport PyBytes_AsString + + +#from cpython cimport PyByteArray_AsString # cython still not exports that +cdef extern from "Python.h": + char* PyByteArray_AsString(bytearray ba) except NULL + +from libc.stdint cimport uint32_t, uint64_t, uintmax_t + + +def _websocket_mask_cython(object mask, object data): + """Note, this function mutates its `data` argument + """ + cdef: + Py_ssize_t data_len, i + # bit operations on signed integers are implementation-specific + unsigned char * in_buf + const unsigned char * mask_buf + uint32_t uint32_msk + uint64_t uint64_msk + + assert len(mask) == 4 + + if not isinstance(mask, bytes): + mask = bytes(mask) + + if isinstance(data, bytearray): + data = data + else: + data = bytearray(data) + + data_len = len(data) + in_buf = PyByteArray_AsString(data) + mask_buf = PyBytes_AsString(mask) + uint32_msk = (mask_buf)[0] + + # TODO: align in_data ptr to achieve even faster speeds + # does it need in python ?! malloc() always aligns to sizeof(long) bytes + + if sizeof(size_t) >= 8: + uint64_msk = uint32_msk + uint64_msk = (uint64_msk << 32) | uint32_msk + + while data_len >= 8: + (in_buf)[0] ^= uint64_msk + in_buf += 8 + data_len -= 8 + + + while data_len >= 4: + (in_buf)[0] ^= uint32_msk + in_buf += 4 + data_len -= 4 + + for i in range(0, data_len): + in_buf[i] ^= mask_buf[i] diff --git a/aiohttp/abc.py b/aiohttp/abc.py new file mode 100644 index 0000000..41200b9 --- /dev/null +++ b/aiohttp/abc.py @@ -0,0 +1,214 @@ +import logging +from abc import ABC, abstractmethod +from collections.abc import Sized +from http.cookies import BaseCookie, Morsel +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Dict, + Generator, + Iterable, + List, + Optional, + Tuple, +) + +from multidict import CIMultiDict +from yarl import URL + +from .typedefs import LooseCookies + +if TYPE_CHECKING: # pragma: no cover + from .web_app import Application + from .web_exceptions import HTTPException + from .web_request import BaseRequest, Request + from .web_response import StreamResponse +else: + BaseRequest = Request = Application = StreamResponse = None + HTTPException = None + + +class AbstractRouter(ABC): + def __init__(self) -> None: + self._frozen = False + + def post_init(self, app: Application) -> None: + """Post init stage. + + Not an abstract method for sake of backward compatibility, + but if the router wants to be aware of the application + it can override this. + """ + + @property + def frozen(self) -> bool: + return self._frozen + + def freeze(self) -> None: + """Freeze router.""" + self._frozen = True + + @abstractmethod + async def resolve(self, request: Request) -> "AbstractMatchInfo": + """Return MATCH_INFO for given request""" + + +class AbstractMatchInfo(ABC): + @property # pragma: no branch + @abstractmethod + def handler(self) -> Callable[[Request], Awaitable[StreamResponse]]: + """Execute matched request handler""" + + @property + @abstractmethod + def expect_handler( + self, + ) -> Callable[[Request], Awaitable[Optional[StreamResponse]]]: + """Expect handler for 100-continue processing""" + + @property # pragma: no branch + @abstractmethod + def http_exception(self) -> Optional[HTTPException]: + """HTTPException instance raised on router's resolving, or None""" + + @abstractmethod # pragma: no branch + def get_info(self) -> Dict[str, Any]: + """Return a dict with additional info useful for introspection""" + + @property # pragma: no branch + @abstractmethod + def apps(self) -> Tuple[Application, ...]: + """Stack of nested applications. + + Top level application is left-most element. + + """ + + @abstractmethod + def add_app(self, app: Application) -> None: + """Add application to the nested apps stack.""" + + @abstractmethod + def freeze(self) -> None: + """Freeze the match info. + + The method is called after route resolution. + + After the call .add_app() is forbidden. + + """ + + +class AbstractView(ABC): + """Abstract class based view.""" + + def __init__(self, request: Request) -> None: + self._request = request + + @property + def request(self) -> Request: + """Request instance.""" + return self._request + + @abstractmethod + def __await__(self) -> Generator[Any, None, StreamResponse]: + """Execute the view handler.""" + + +class AbstractResolver(ABC): + """Abstract DNS resolver.""" + + @abstractmethod + async def resolve(self, host: str, port: int, family: int) -> List[Dict[str, Any]]: + """Return IP address for given hostname""" + + @abstractmethod + async def close(self) -> None: + """Release resolver""" + + +if TYPE_CHECKING: # pragma: no cover + IterableBase = Iterable[Morsel[str]] +else: + IterableBase = Iterable + + +ClearCookiePredicate = Callable[["Morsel[str]"], bool] + + +class AbstractCookieJar(Sized, IterableBase): + """Abstract Cookie Jar.""" + + @abstractmethod + def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None: + """Clear all cookies if no predicate is passed.""" + + @abstractmethod + def clear_domain(self, domain: str) -> None: + """Clear all cookies for domain and all subdomains.""" + + @abstractmethod + def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None: + """Update cookies.""" + + @abstractmethod + def filter_cookies(self, request_url: URL) -> "BaseCookie[str]": + """Return the jar's cookies filtered by their attributes.""" + + +class AbstractStreamWriter(ABC): + """Abstract stream writer.""" + + buffer_size = 0 + output_size = 0 + length: Optional[int] = 0 + + @abstractmethod + async def write(self, chunk: bytes) -> None: + """Write chunk into stream.""" + + @abstractmethod + async def write_eof(self, chunk: bytes = b"") -> None: + """Write last chunk.""" + + @abstractmethod + async def drain(self) -> None: + """Flush the write buffer.""" + + @abstractmethod + def enable_compression(self, encoding: str = "deflate") -> None: + """Enable HTTP body compression""" + + @abstractmethod + def enable_chunking(self) -> None: + """Enable HTTP chunked mode""" + + @abstractmethod + async def write_headers( + self, status_line: str, headers: "CIMultiDict[str]" + ) -> None: + """Write HTTP headers""" + + +class AbstractAccessLogger(ABC): + """Abstract writer to access log.""" + + def __init__(self, logger: logging.Logger, log_format: str) -> None: + self.logger = logger + self.log_format = log_format + + @abstractmethod + def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None: + """Emit log to logger.""" + + +class AbstractAsyncAccessLogger(ABC): + """Abstract asynchronous writer to access log.""" + + @abstractmethod + async def log( + self, request: BaseRequest, response: StreamResponse, request_start: float + ) -> None: + """Emit log to logger.""" diff --git a/aiohttp/base_protocol.py b/aiohttp/base_protocol.py new file mode 100644 index 0000000..4c9f0a7 --- /dev/null +++ b/aiohttp/base_protocol.py @@ -0,0 +1,90 @@ +import asyncio +from typing import Optional, cast + +from .tcp_helpers import tcp_nodelay + + +class BaseProtocol(asyncio.Protocol): + __slots__ = ( + "_loop", + "_paused", + "_drain_waiter", + "_connection_lost", + "_reading_paused", + "transport", + ) + + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + self._loop: asyncio.AbstractEventLoop = loop + self._paused = False + self._drain_waiter: Optional[asyncio.Future[None]] = None + self._reading_paused = False + + self.transport: Optional[asyncio.Transport] = None + + @property + def connected(self) -> bool: + """Return True if the connection is open.""" + return self.transport is not None + + def pause_writing(self) -> None: + assert not self._paused + self._paused = True + + def resume_writing(self) -> None: + assert self._paused + self._paused = False + + waiter = self._drain_waiter + if waiter is not None: + self._drain_waiter = None + if not waiter.done(): + waiter.set_result(None) + + def pause_reading(self) -> None: + if not self._reading_paused and self.transport is not None: + try: + self.transport.pause_reading() + except (AttributeError, NotImplementedError, RuntimeError): + pass + self._reading_paused = True + + def resume_reading(self) -> None: + if self._reading_paused and self.transport is not None: + try: + self.transport.resume_reading() + except (AttributeError, NotImplementedError, RuntimeError): + pass + self._reading_paused = False + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + tr = cast(asyncio.Transport, transport) + tcp_nodelay(tr, True) + self.transport = tr + + def connection_lost(self, exc: Optional[BaseException]) -> None: + # Wake up the writer if currently paused. + self.transport = None + if not self._paused: + return + waiter = self._drain_waiter + if waiter is None: + return + self._drain_waiter = None + if waiter.done(): + return + if exc is None: + waiter.set_result(None) + else: + waiter.set_exception(exc) + + async def _drain_helper(self) -> None: + if not self.connected: + raise ConnectionResetError("Connection lost") + if not self._paused: + return + waiter = self._drain_waiter + if waiter is None: + waiter = self._loop.create_future() + self._drain_waiter = waiter + await asyncio.shield(waiter) diff --git a/aiohttp/client.py b/aiohttp/client.py new file mode 100644 index 0000000..1d9d9fe --- /dev/null +++ b/aiohttp/client.py @@ -0,0 +1,1289 @@ +"""HTTP Client for asyncio.""" + +import asyncio +import base64 +import dataclasses +import hashlib +import json +import os +import sys +import traceback +import warnings +from contextlib import suppress +from types import SimpleNamespace, TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Collection, + Coroutine, + Final, + FrozenSet, + Generator, + Generic, + Iterable, + List, + Literal, + Mapping, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + final, +) + +from multidict import CIMultiDict, MultiDict, MultiDictProxy, istr +from yarl import URL + +from . import hdrs, http, payload +from .abc import AbstractCookieJar +from .client_exceptions import ( + ClientConnectionError, + ClientConnectorCertificateError, + ClientConnectorError, + ClientConnectorSSLError, + ClientError, + ClientHttpProxyError, + ClientOSError, + ClientPayloadError, + ClientProxyConnectionError, + ClientResponseError, + ClientSSLError, + ContentTypeError, + InvalidURL, + ServerConnectionError, + ServerDisconnectedError, + ServerFingerprintMismatch, + ServerTimeoutError, + TooManyRedirects, + WSServerHandshakeError, +) +from .client_reqrep import ( + SSL_ALLOWED_TYPES, + ClientRequest, + ClientResponse, + Fingerprint, + RequestInfo, +) +from .client_ws import ( + DEFAULT_WS_CLIENT_TIMEOUT, + ClientWebSocketResponse, + ClientWSTimeout, +) +from .connector import BaseConnector, NamedPipeConnector, TCPConnector, UnixConnector +from .cookiejar import CookieJar +from .helpers import ( + _SENTINEL, + BasicAuth, + TimeoutHandle, + ceil_timeout, + get_env_proxy_for_url, + sentinel, + strip_auth_from_url, +) +from .http import WS_KEY, HttpVersion, WebSocketReader, WebSocketWriter +from .http_websocket import WSHandshakeError, WSMessage, ws_ext_gen, ws_ext_parse +from .streams import FlowControlDataQueue +from .tracing import Trace, TraceConfig +from .typedefs import JSONEncoder, LooseCookies, LooseHeaders, StrOrURL + +__all__ = ( + # client_exceptions + "ClientConnectionError", + "ClientConnectorCertificateError", + "ClientConnectorError", + "ClientConnectorSSLError", + "ClientError", + "ClientHttpProxyError", + "ClientOSError", + "ClientPayloadError", + "ClientProxyConnectionError", + "ClientResponseError", + "ClientSSLError", + "ContentTypeError", + "InvalidURL", + "ServerConnectionError", + "ServerDisconnectedError", + "ServerFingerprintMismatch", + "ServerTimeoutError", + "TooManyRedirects", + "WSServerHandshakeError", + # client_reqrep + "ClientRequest", + "ClientResponse", + "Fingerprint", + "RequestInfo", + # connector + "BaseConnector", + "TCPConnector", + "UnixConnector", + "NamedPipeConnector", + # client_ws + "ClientWebSocketResponse", + # client + "ClientSession", + "ClientTimeout", + "request", +) + + +if TYPE_CHECKING: + from ssl import SSLContext +else: + SSLContext = None + + +@dataclasses.dataclass(frozen=True) +class ClientTimeout: + total: Optional[float] = None + connect: Optional[float] = None + sock_read: Optional[float] = None + sock_connect: Optional[float] = None + ceil_threshold: float = 5 + + # pool_queue_timeout: Optional[float] = None + # dns_resolution_timeout: Optional[float] = None + # socket_connect_timeout: Optional[float] = None + # connection_acquiring_timeout: Optional[float] = None + # new_connection_timeout: Optional[float] = None + # http_header_timeout: Optional[float] = None + # response_body_timeout: Optional[float] = None + + # to create a timeout specific for a single request, either + # - create a completely new one to overwrite the default + # - or use https://docs.python.org/3/library/dataclasses.html#dataclasses.replace + # to overwrite the defaults + + +# 5 Minute default read timeout +DEFAULT_TIMEOUT: Final[ClientTimeout] = ClientTimeout(total=5 * 60) + +_RetType = TypeVar("_RetType") +_CharsetResolver = Callable[[ClientResponse, bytes], str] + + +@final +class ClientSession: + """First-class interface for making HTTP requests.""" + + __slots__ = ( + "_base_url", + "_source_traceback", + "_connector", + "_loop", + "_cookie_jar", + "_connector_owner", + "_default_auth", + "_version", + "_json_serialize", + "_requote_redirect_url", + "_timeout", + "_raise_for_status", + "_auto_decompress", + "_trust_env", + "_default_headers", + "_skip_auto_headers", + "_request_class", + "_response_class", + "_ws_response_class", + "_trace_configs", + "_read_bufsize", + "_max_line_size", + "_max_field_size", + "_resolve_charset", + ) + + def __init__( + self, + base_url: Optional[StrOrURL] = None, + *, + connector: Optional[BaseConnector] = None, + cookies: Optional[LooseCookies] = None, + headers: Optional[LooseHeaders] = None, + skip_auto_headers: Optional[Iterable[str]] = None, + auth: Optional[BasicAuth] = None, + json_serialize: JSONEncoder = json.dumps, + request_class: Type[ClientRequest] = ClientRequest, + response_class: Type[ClientResponse] = ClientResponse, + ws_response_class: Type[ClientWebSocketResponse] = ClientWebSocketResponse, + version: HttpVersion = http.HttpVersion11, + cookie_jar: Optional[AbstractCookieJar] = None, + connector_owner: bool = True, + raise_for_status: Union[ + bool, Callable[[ClientResponse], Awaitable[None]] + ] = False, + timeout: Union[_SENTINEL, ClientTimeout, None] = sentinel, + auto_decompress: bool = True, + trust_env: bool = False, + requote_redirect_url: bool = True, + trace_configs: Optional[List[TraceConfig]] = None, + read_bufsize: int = 2**16, + max_line_size: int = 8190, + max_field_size: int = 8190, + fallback_charset_resolver: _CharsetResolver = lambda r, b: "utf-8", + ) -> None: + if base_url is None or isinstance(base_url, URL): + self._base_url: Optional[URL] = base_url + else: + self._base_url = URL(base_url) + assert ( + self._base_url.origin() == self._base_url + ), "Only absolute URLs without path part are supported" + + loop = asyncio.get_running_loop() + + if connector is None: + connector = TCPConnector() + + # Initialize these three attrs before raising any exception, + # they are used in __del__ + self._connector: Optional[BaseConnector] = connector + self._loop = loop + if loop.get_debug(): + self._source_traceback: Optional[ + traceback.StackSummary + ] = traceback.extract_stack(sys._getframe(1)) + else: + self._source_traceback = None + + if connector._loop is not loop: + raise RuntimeError("Session and connector have to use same event loop") + + if cookie_jar is None: + cookie_jar = CookieJar() + self._cookie_jar = cookie_jar + + if cookies is not None: + self._cookie_jar.update_cookies(cookies) + + self._connector_owner = connector_owner + self._default_auth = auth + self._version = version + self._json_serialize = json_serialize + if timeout is sentinel or timeout is None: + self._timeout = DEFAULT_TIMEOUT + else: + self._timeout = timeout + self._raise_for_status = raise_for_status + self._auto_decompress = auto_decompress + self._trust_env = trust_env + self._requote_redirect_url = requote_redirect_url + self._read_bufsize = read_bufsize + self._max_line_size = max_line_size + self._max_field_size = max_field_size + + # Convert to list of tuples + if headers: + real_headers: CIMultiDict[str] = CIMultiDict(headers) + else: + real_headers = CIMultiDict() + self._default_headers: CIMultiDict[str] = real_headers + if skip_auto_headers is not None: + self._skip_auto_headers = frozenset(istr(i) for i in skip_auto_headers) + else: + self._skip_auto_headers = frozenset() + + self._request_class = request_class + self._response_class = response_class + self._ws_response_class = ws_response_class + + self._trace_configs = trace_configs or [] + for trace_config in self._trace_configs: + trace_config.freeze() + + self._resolve_charset = fallback_charset_resolver + + def __init_subclass__(cls: Type["ClientSession"]) -> None: + raise TypeError( + "Inheritance class {} from ClientSession " + "is forbidden".format(cls.__name__) + ) + + def __del__(self, _warnings: Any = warnings) -> None: + if not self.closed: + _warnings.warn( + f"Unclosed client session {self!r}", + ResourceWarning, + source=self, + ) + context = {"client_session": self, "message": "Unclosed client session"} + if self._source_traceback is not None: + context["source_traceback"] = self._source_traceback + self._loop.call_exception_handler(context) + + def request( + self, method: str, url: StrOrURL, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP request.""" + return _RequestContextManager(self._request(method, url, **kwargs)) + + def _build_url(self, str_or_url: StrOrURL) -> URL: + url = URL(str_or_url) + if self._base_url is None: + return url + else: + assert not url.is_absolute() and url.path.startswith("/") + return self._base_url.join(url) + + async def _request( + self, + method: str, + str_or_url: StrOrURL, + *, + params: Optional[Mapping[str, str]] = None, + data: Any = None, + json: Any = None, + cookies: Optional[LooseCookies] = None, + headers: Optional[LooseHeaders] = None, + skip_auto_headers: Optional[Iterable[str]] = None, + auth: Optional[BasicAuth] = None, + allow_redirects: bool = True, + max_redirects: int = 10, + compress: Optional[str] = None, + chunked: Optional[bool] = None, + expect100: bool = False, + raise_for_status: Union[ + None, bool, Callable[[ClientResponse], Awaitable[None]] + ] = None, + read_until_eof: bool = True, + proxy: Optional[StrOrURL] = None, + proxy_auth: Optional[BasicAuth] = None, + timeout: Union[ClientTimeout, _SENTINEL, None] = sentinel, + ssl: Optional[Union[SSLContext, Literal[False], Fingerprint]] = None, + server_hostname: Optional[str] = None, + proxy_headers: Optional[LooseHeaders] = None, + trace_request_ctx: Optional[SimpleNamespace] = None, + read_bufsize: Optional[int] = None, + auto_decompress: Optional[bool] = None, + max_line_size: Optional[int] = None, + max_field_size: Optional[int] = None, + ) -> ClientResponse: + # NOTE: timeout clamps existing connect and read timeouts. We cannot + # set the default to None because we need to detect if the user wants + # to use the existing timeouts by setting timeout to None. + + if self.closed: + raise RuntimeError("Session is closed") + + if not isinstance(ssl, SSL_ALLOWED_TYPES): + raise TypeError( + "ssl should be SSLContext, bool, Fingerprint, " + "or None, got {!r} instead.".format(ssl) + ) + + if data is not None and json is not None: + raise ValueError( + "data and json parameters can not be used at the same time" + ) + elif json is not None: + data = payload.JsonPayload(json, dumps=self._json_serialize) + + redirects = 0 + history = [] + version = self._version + params = params or {} + + # Merge with default headers and transform to CIMultiDict + headers = self._prepare_headers(headers) + proxy_headers = self._prepare_headers(proxy_headers) + + try: + url = self._build_url(str_or_url) + except ValueError as e: + raise InvalidURL(str_or_url) from e + + skip_headers = set(self._skip_auto_headers) + if skip_auto_headers is not None: + for i in skip_auto_headers: + skip_headers.add(istr(i)) + + if proxy is not None: + try: + proxy = URL(proxy) + except ValueError as e: + raise InvalidURL(proxy) from e + + if timeout is sentinel or timeout is None: + real_timeout: ClientTimeout = self._timeout + else: + real_timeout = timeout + # timeout is cumulative for all request operations + # (request, redirects, responses, data consuming) + tm = TimeoutHandle( + self._loop, real_timeout.total, ceil_threshold=real_timeout.ceil_threshold + ) + handle = tm.start() + + if read_bufsize is None: + read_bufsize = self._read_bufsize + + if auto_decompress is None: + auto_decompress = self._auto_decompress + + if max_line_size is None: + max_line_size = self._max_line_size + + if max_field_size is None: + max_field_size = self._max_field_size + + traces = [ + Trace( + self, + trace_config, + trace_config.trace_config_ctx(trace_request_ctx=trace_request_ctx), + ) + for trace_config in self._trace_configs + ] + + for trace in traces: + await trace.send_request_start(method, url.update_query(params), headers) + + timer = tm.timer() + try: + with timer: + while True: + url, auth_from_url = strip_auth_from_url(url) + if auth and auth_from_url: + raise ValueError( + "Cannot combine AUTH argument with " + "credentials encoded in URL" + ) + + if auth is None: + auth = auth_from_url + if auth is None: + auth = self._default_auth + # It would be confusing if we support explicit + # Authorization header with auth argument + if auth is not None and hdrs.AUTHORIZATION in headers: + raise ValueError( + "Cannot combine AUTHORIZATION header " + "with AUTH argument or credentials " + "encoded in URL" + ) + + all_cookies = self._cookie_jar.filter_cookies(url) + + if cookies is not None: + tmp_cookie_jar = CookieJar() + tmp_cookie_jar.update_cookies(cookies) + req_cookies = tmp_cookie_jar.filter_cookies(url) + if req_cookies: + all_cookies.load(req_cookies) + + if proxy is not None: + proxy = URL(proxy) + elif self._trust_env: + with suppress(LookupError): + proxy, proxy_auth = get_env_proxy_for_url(url) + + req = self._request_class( + method, + url, + params=params, + headers=headers, + skip_auto_headers=skip_headers, + data=data, + cookies=all_cookies, + auth=auth, + version=version, + compress=compress, + chunked=chunked, + expect100=expect100, + loop=self._loop, + response_class=self._response_class, + proxy=proxy, + proxy_auth=proxy_auth, + timer=timer, + session=self, + ssl=ssl, + server_hostname=server_hostname, + proxy_headers=proxy_headers, + traces=traces, + trust_env=self.trust_env, + ) + + # connection timeout + try: + async with ceil_timeout( + real_timeout.connect, + ceil_threshold=real_timeout.ceil_threshold, + ): + assert self._connector is not None + conn = await self._connector.connect( + req, traces=traces, timeout=real_timeout + ) + except asyncio.TimeoutError as exc: + raise ServerTimeoutError( + f"Connection timeout to host {url}" + ) from exc + + assert conn.transport is not None + + assert conn.protocol is not None + conn.protocol.set_response_params( + timer=timer, + skip_payload=method.upper() == "HEAD", + read_until_eof=read_until_eof, + auto_decompress=auto_decompress, + read_timeout=real_timeout.sock_read, + read_bufsize=read_bufsize, + timeout_ceil_threshold=self._connector._timeout_ceil_threshold, + max_line_size=max_line_size, + max_field_size=max_field_size, + ) + + try: + try: + resp = await req.send(conn) + try: + await resp.start(conn) + except BaseException: + resp.close() + raise + except BaseException: + conn.close() + raise + except ClientError: + raise + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + raise ClientOSError(*exc.args) from exc + + self._cookie_jar.update_cookies(resp.cookies, resp.url) + + # redirects + if resp.status in (301, 302, 303, 307, 308) and allow_redirects: + for trace in traces: + await trace.send_request_redirect( + method, url.update_query(params), headers, resp + ) + + redirects += 1 + history.append(resp) + if max_redirects and redirects >= max_redirects: + resp.close() + raise TooManyRedirects( + history[0].request_info, tuple(history) + ) + + # For 301 and 302, mimic IE, now changed in RFC + # https://github.com/kennethreitz/requests/pull/269 + if (resp.status == 303 and resp.method != hdrs.METH_HEAD) or ( + resp.status in (301, 302) and resp.method == hdrs.METH_POST + ): + method = hdrs.METH_GET + data = None + if headers.get(hdrs.CONTENT_LENGTH): + headers.pop(hdrs.CONTENT_LENGTH) + + r_url = resp.headers.get(hdrs.LOCATION) or resp.headers.get( + hdrs.URI + ) + if r_url is None: + # see github.com/aio-libs/aiohttp/issues/2022 + break + else: + # reading from correct redirection + # response is forbidden + resp.release() + + try: + parsed_url = URL( + r_url, encoded=not self._requote_redirect_url + ) + + except ValueError as e: + raise InvalidURL(r_url) from e + + scheme = parsed_url.scheme + if scheme not in ("http", "https", ""): + resp.close() + raise ValueError("Can redirect only to http or https") + elif not scheme: + parsed_url = url.join(parsed_url) + + is_same_host_https_redirect = ( + url.host == parsed_url.host + and parsed_url.scheme == "https" + and url.scheme == "http" + ) + + if ( + url.origin() != parsed_url.origin() + and not is_same_host_https_redirect + ): + auth = None + headers.pop(hdrs.AUTHORIZATION, None) + + url = parsed_url + params = {} + resp.release() + continue + + break + + # check response status + if raise_for_status is None: + raise_for_status = self._raise_for_status + + if raise_for_status is None: + pass + elif callable(raise_for_status): + await raise_for_status(resp) + elif raise_for_status: + resp.raise_for_status() + + # register connection + if handle is not None: + if resp.connection is not None: + resp.connection.add_callback(handle.cancel) + else: + handle.cancel() + + resp._history = tuple(history) + + for trace in traces: + await trace.send_request_end( + method, url.update_query(params), headers, resp + ) + return resp + + except BaseException as e: + # cleanup timer + tm.close() + if handle: + handle.cancel() + handle = None + + for trace in traces: + await trace.send_request_exception( + method, url.update_query(params), headers, e + ) + raise + + def ws_connect( + self, + url: StrOrURL, + *, + method: str = hdrs.METH_GET, + protocols: Collection[str] = (), + timeout: Union[ClientWSTimeout, float, _SENTINEL, None] = sentinel, + receive_timeout: Optional[float] = None, + autoclose: bool = True, + autoping: bool = True, + heartbeat: Optional[float] = None, + auth: Optional[BasicAuth] = None, + origin: Optional[str] = None, + params: Optional[Mapping[str, str]] = None, + headers: Optional[LooseHeaders] = None, + proxy: Optional[StrOrURL] = None, + proxy_auth: Optional[BasicAuth] = None, + ssl: Union[SSLContext, Literal[False], None, Fingerprint] = None, + proxy_headers: Optional[LooseHeaders] = None, + compress: int = 0, + max_msg_size: int = 4 * 1024 * 1024, + ) -> "_WSRequestContextManager": + """Initiate websocket connection.""" + return _WSRequestContextManager( + self._ws_connect( + url, + method=method, + protocols=protocols, + timeout=timeout, + receive_timeout=receive_timeout, + autoclose=autoclose, + autoping=autoping, + heartbeat=heartbeat, + auth=auth, + origin=origin, + params=params, + headers=headers, + proxy=proxy, + proxy_auth=proxy_auth, + ssl=ssl, + proxy_headers=proxy_headers, + compress=compress, + max_msg_size=max_msg_size, + ) + ) + + async def _ws_connect( + self, + url: StrOrURL, + *, + method: str = hdrs.METH_GET, + protocols: Collection[str] = (), + timeout: Union[ClientWSTimeout, float, _SENTINEL, None] = sentinel, + receive_timeout: Optional[float] = None, + autoclose: bool = True, + autoping: bool = True, + heartbeat: Optional[float] = None, + auth: Optional[BasicAuth] = None, + origin: Optional[str] = None, + params: Optional[Mapping[str, str]] = None, + headers: Optional[LooseHeaders] = None, + proxy: Optional[StrOrURL] = None, + proxy_auth: Optional[BasicAuth] = None, + ssl: Union[SSLContext, Literal[False], None, Fingerprint] = None, + proxy_headers: Optional[LooseHeaders] = None, + compress: int = 0, + max_msg_size: int = 4 * 1024 * 1024, + ) -> ClientWebSocketResponse: + if timeout is sentinel or timeout is None: + ws_timeout = DEFAULT_WS_CLIENT_TIMEOUT + else: + if isinstance(timeout, ClientWSTimeout): + ws_timeout = timeout + else: + warnings.warn( + "parameter 'timeout' of type 'float' " + "is deprecated, please use " + "'timeout=ClientWSTimeout(ws_close=...)'", + DeprecationWarning, + stacklevel=2, + ) + ws_timeout = ClientWSTimeout(ws_close=timeout) + + if receive_timeout is not None: + warnings.warn( + "float parameter 'receive_timeout' " + "is deprecated, please use parameter " + "'timeout=ClientWSTimeout(ws_receive=...)'", + DeprecationWarning, + stacklevel=2, + ) + ws_timeout = dataclasses.replace(ws_timeout, ws_receive=receive_timeout) + + if headers is None: + real_headers: CIMultiDict[str] = CIMultiDict() + else: + real_headers = CIMultiDict(headers) + + default_headers = { + hdrs.UPGRADE: "websocket", + hdrs.CONNECTION: "Upgrade", + hdrs.SEC_WEBSOCKET_VERSION: "13", + } + + for key, value in default_headers.items(): + real_headers.setdefault(key, value) + + sec_key = base64.b64encode(os.urandom(16)) + real_headers[hdrs.SEC_WEBSOCKET_KEY] = sec_key.decode() + + if protocols: + real_headers[hdrs.SEC_WEBSOCKET_PROTOCOL] = ",".join(protocols) + if origin is not None: + real_headers[hdrs.ORIGIN] = origin + if compress: + extstr = ws_ext_gen(compress=compress) + real_headers[hdrs.SEC_WEBSOCKET_EXTENSIONS] = extstr + + if not isinstance(ssl, SSL_ALLOWED_TYPES): + raise TypeError( + "ssl should be SSLContext, bool, Fingerprint, " + "or None, got {!r} instead.".format(ssl) + ) + + # send request + resp = await self.request( + method, + url, + params=params, + headers=real_headers, + read_until_eof=False, + auth=auth, + proxy=proxy, + proxy_auth=proxy_auth, + ssl=ssl, + proxy_headers=proxy_headers, + ) + + try: + # check handshake + if resp.status != 101: + raise WSServerHandshakeError( + resp.request_info, + resp.history, + message="Invalid response status", + status=resp.status, + headers=resp.headers, + ) + + if resp.headers.get(hdrs.UPGRADE, "").lower() != "websocket": + raise WSServerHandshakeError( + resp.request_info, + resp.history, + message="Invalid upgrade header", + status=resp.status, + headers=resp.headers, + ) + + if resp.headers.get(hdrs.CONNECTION, "").lower() != "upgrade": + raise WSServerHandshakeError( + resp.request_info, + resp.history, + message="Invalid connection header", + status=resp.status, + headers=resp.headers, + ) + + # key calculation + r_key = resp.headers.get(hdrs.SEC_WEBSOCKET_ACCEPT, "") + match = base64.b64encode(hashlib.sha1(sec_key + WS_KEY).digest()).decode() + if r_key != match: + raise WSServerHandshakeError( + resp.request_info, + resp.history, + message="Invalid challenge response", + status=resp.status, + headers=resp.headers, + ) + + # websocket protocol + protocol = None + if protocols and hdrs.SEC_WEBSOCKET_PROTOCOL in resp.headers: + resp_protocols = [ + proto.strip() + for proto in resp.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",") + ] + + for proto in resp_protocols: + if proto in protocols: + protocol = proto + break + + # websocket compress + notakeover = False + if compress: + compress_hdrs = resp.headers.get(hdrs.SEC_WEBSOCKET_EXTENSIONS) + if compress_hdrs: + try: + compress, notakeover = ws_ext_parse(compress_hdrs) + except WSHandshakeError as exc: + raise WSServerHandshakeError( + resp.request_info, + resp.history, + message=exc.args[0], + status=resp.status, + headers=resp.headers, + ) from exc + else: + compress = 0 + notakeover = False + + conn = resp.connection + assert conn is not None + conn_proto = conn.protocol + assert conn_proto is not None + transport = conn.transport + assert transport is not None + reader: FlowControlDataQueue[WSMessage] = FlowControlDataQueue( + conn_proto, 2**16, loop=self._loop + ) + conn_proto.set_parser(WebSocketReader(reader, max_msg_size), reader) + writer = WebSocketWriter( + conn_proto, + transport, + use_mask=True, + compress=compress, + notakeover=notakeover, + ) + except BaseException: + resp.close() + raise + else: + return self._ws_response_class( + reader, + writer, + protocol, + resp, + ws_timeout, + autoclose, + autoping, + self._loop, + heartbeat=heartbeat, + compress=compress, + client_notakeover=notakeover, + ) + + def _prepare_headers(self, headers: Optional[LooseHeaders]) -> "CIMultiDict[str]": + """Add default headers and transform it to CIMultiDict""" + # Convert headers to MultiDict + result = CIMultiDict(self._default_headers) + if headers: + if not isinstance(headers, (MultiDictProxy, MultiDict)): + headers = CIMultiDict(headers) + added_names: Set[str] = set() + for key, value in headers.items(): + if key in added_names: + result.add(key, value) + else: + result[key] = value + added_names.add(key) + return result + + def get( + self, url: StrOrURL, *, allow_redirects: bool = True, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP GET request.""" + return _RequestContextManager( + self._request(hdrs.METH_GET, url, allow_redirects=allow_redirects, **kwargs) + ) + + def options( + self, url: StrOrURL, *, allow_redirects: bool = True, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP OPTIONS request.""" + return _RequestContextManager( + self._request( + hdrs.METH_OPTIONS, url, allow_redirects=allow_redirects, **kwargs + ) + ) + + def head( + self, url: StrOrURL, *, allow_redirects: bool = False, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP HEAD request.""" + return _RequestContextManager( + self._request( + hdrs.METH_HEAD, url, allow_redirects=allow_redirects, **kwargs + ) + ) + + def post( + self, url: StrOrURL, *, data: Any = None, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP POST request.""" + return _RequestContextManager( + self._request(hdrs.METH_POST, url, data=data, **kwargs) + ) + + def put( + self, url: StrOrURL, *, data: Any = None, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP PUT request.""" + return _RequestContextManager( + self._request(hdrs.METH_PUT, url, data=data, **kwargs) + ) + + def patch( + self, url: StrOrURL, *, data: Any = None, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP PATCH request.""" + return _RequestContextManager( + self._request(hdrs.METH_PATCH, url, data=data, **kwargs) + ) + + def delete(self, url: StrOrURL, **kwargs: Any) -> "_RequestContextManager": + """Perform HTTP DELETE request.""" + return _RequestContextManager(self._request(hdrs.METH_DELETE, url, **kwargs)) + + async def close(self) -> None: + """Close underlying connector. + + Release all acquired resources. + """ + if not self.closed: + if self._connector is not None and self._connector_owner: + await self._connector.close() + self._connector = None + + @property + def closed(self) -> bool: + """Is client session closed. + + A readonly property. + """ + return self._connector is None or self._connector.closed + + @property + def connector(self) -> Optional[BaseConnector]: + """Connector instance used for the session.""" + return self._connector + + @property + def cookie_jar(self) -> AbstractCookieJar: + """The session cookies.""" + return self._cookie_jar + + @property + def version(self) -> Tuple[int, int]: + """The session HTTP protocol version.""" + return self._version + + @property + def requote_redirect_url(self) -> bool: + """Do URL requoting on redirection handling.""" + return self._requote_redirect_url + + @property + def timeout(self) -> ClientTimeout: + """Timeout for the session.""" + return self._timeout + + @property + def headers(self) -> "CIMultiDict[str]": + """The default headers of the client session.""" + return self._default_headers + + @property + def skip_auto_headers(self) -> FrozenSet[istr]: + """Headers for which autogeneration should be skipped""" + return self._skip_auto_headers + + @property + def auth(self) -> Optional[BasicAuth]: + """An object that represents HTTP Basic Authorization""" + return self._default_auth + + @property + def json_serialize(self) -> JSONEncoder: + """Json serializer callable""" + return self._json_serialize + + @property + def connector_owner(self) -> bool: + """Should connector be closed on session closing""" + return self._connector_owner + + @property + def raise_for_status( + self, + ) -> Union[bool, Callable[[ClientResponse], Awaitable[None]]]: + """Should `ClientResponse.raise_for_status()` be called for each response.""" + return self._raise_for_status + + @property + def auto_decompress(self) -> bool: + """Should the body response be automatically decompressed.""" + return self._auto_decompress + + @property + def trust_env(self) -> bool: + """ + Should proxies information from environment or netrc be trusted. + + Information is from HTTP_PROXY / HTTPS_PROXY environment variables + or ~/.netrc file if present. + """ + return self._trust_env + + @property + def trace_configs(self) -> List[TraceConfig]: + """A list of TraceConfig instances used for client tracing""" + return self._trace_configs + + def detach(self) -> None: + """Detach connector from session without closing the former. + + Session is switched to closed state anyway. + """ + self._connector = None + + async def __aenter__(self) -> "ClientSession": + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + await self.close() + + +class _BaseRequestContextManager(Coroutine[Any, Any, _RetType], Generic[_RetType]): + __slots__ = ("_coro", "_resp") + + def __init__(self, coro: Coroutine["asyncio.Future[Any]", None, _RetType]) -> None: + self._coro = coro + + def send(self, arg: None) -> "asyncio.Future[Any]": + return self._coro.send(arg) + + def throw(self, arg: BaseException) -> None: # type: ignore[override] + self._coro.throw(arg) # type: ignore[unused-awaitable] + + def close(self) -> None: + return self._coro.close() + + def __await__(self) -> Generator[Any, None, _RetType]: + ret = self._coro.__await__() + return ret + + def __iter__(self) -> Generator[Any, None, _RetType]: + return self.__await__() + + async def __aenter__(self) -> _RetType: + self._resp = await self._coro + return self._resp + + +class _RequestContextManager(_BaseRequestContextManager[ClientResponse]): + __slots__ = () + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + tb: Optional[TracebackType], + ) -> None: + # We're basing behavior on the exception as it can be caused by + # user code unrelated to the status of the connection. If you + # would like to close a connection you must do that + # explicitly. Otherwise connection error handling should kick in + # and close/recycle the connection as required. + self._resp.release() + + +class _WSRequestContextManager(_BaseRequestContextManager[ClientWebSocketResponse]): + __slots__ = () + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + tb: Optional[TracebackType], + ) -> None: + await self._resp.close() + + +class _SessionRequestContextManager: + __slots__ = ("_coro", "_resp", "_session") + + def __init__( + self, + coro: Coroutine["asyncio.Future[Any]", None, ClientResponse], + session: ClientSession, + ) -> None: + self._coro = coro + self._resp: Optional[ClientResponse] = None + self._session = session + + async def __aenter__(self) -> ClientResponse: + try: + self._resp = await self._coro + except BaseException: + await self._session.close() + raise + else: + return self._resp + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + tb: Optional[TracebackType], + ) -> None: + assert self._resp is not None + self._resp.close() + await self._session.close() + + +def request( + method: str, + url: StrOrURL, + *, + params: Optional[Mapping[str, str]] = None, + data: Any = None, + json: Any = None, + headers: Optional[LooseHeaders] = None, + skip_auto_headers: Optional[Iterable[str]] = None, + auth: Optional[BasicAuth] = None, + allow_redirects: bool = True, + max_redirects: int = 10, + compress: Optional[str] = None, + chunked: Optional[bool] = None, + expect100: bool = False, + raise_for_status: Optional[bool] = None, + read_until_eof: bool = True, + proxy: Optional[StrOrURL] = None, + proxy_auth: Optional[BasicAuth] = None, + timeout: Union[ClientTimeout, _SENTINEL] = sentinel, + cookies: Optional[LooseCookies] = None, + version: HttpVersion = http.HttpVersion11, + connector: Optional[BaseConnector] = None, + read_bufsize: Optional[int] = None, + max_line_size: int = 8190, + max_field_size: int = 8190, +) -> _SessionRequestContextManager: + """Constructs and sends a request. + + Returns response object. + method - HTTP method + url - request url + params - (optional) Dictionary or bytes to be sent in the query + string of the new request + data - (optional) Dictionary, bytes, or file-like object to + send in the body of the request + json - (optional) Any json compatible python object + headers - (optional) Dictionary of HTTP Headers to send with + the request + cookies - (optional) Dict object to send with the request + auth - (optional) BasicAuth named tuple represent HTTP Basic Auth + auth - aiohttp.helpers.BasicAuth + allow_redirects - (optional) If set to False, do not follow + redirects + version - Request HTTP version. + compress - Set to True if request has to be compressed + with deflate encoding. + chunked - Set to chunk size for chunked transfer encoding. + expect100 - Expect 100-continue response from server. + connector - BaseConnector sub-class instance to support + connection pooling. + read_until_eof - Read response until eof if response + does not have Content-Length header. + loop - Optional event loop. + timeout - Optional ClientTimeout settings structure, 5min + total timeout by default. + Usage:: + >>> import aiohttp + >>> async with aiohttp.request('GET', 'http://python.org/') as resp: + ... print(resp) + ... data = await resp.read() + + """ + connector_owner = False + if connector is None: + connector_owner = True + connector = TCPConnector(force_close=True) + + session = ClientSession( + cookies=cookies, + version=version, + timeout=timeout, + connector=connector, + connector_owner=connector_owner, + ) + + return _SessionRequestContextManager( + session._request( + method, + url, + params=params, + data=data, + json=json, + headers=headers, + skip_auto_headers=skip_auto_headers, + auth=auth, + allow_redirects=allow_redirects, + max_redirects=max_redirects, + compress=compress, + chunked=chunked, + expect100=expect100, + raise_for_status=raise_for_status, + read_until_eof=read_until_eof, + proxy=proxy, + proxy_auth=proxy_auth, + read_bufsize=read_bufsize, + max_line_size=max_line_size, + max_field_size=max_field_size, + ), + session, + ) diff --git a/aiohttp/client_exceptions.py b/aiohttp/client_exceptions.py new file mode 100644 index 0000000..f7023ef --- /dev/null +++ b/aiohttp/client_exceptions.py @@ -0,0 +1,313 @@ +"""HTTP related errors.""" + +import asyncio +from typing import TYPE_CHECKING, Any, Optional, Tuple, Union + +from .http_parser import RawResponseMessage +from .typedefs import LooseHeaders + +try: + import ssl + + SSLContext = ssl.SSLContext +except ImportError: # pragma: no cover + ssl = SSLContext = None # type: ignore[assignment] + + +if TYPE_CHECKING: # pragma: no cover + from .client_reqrep import ClientResponse, ConnectionKey, Fingerprint, RequestInfo +else: + RequestInfo = ClientResponse = ConnectionKey = None + +__all__ = ( + "ClientError", + "ClientConnectionError", + "ClientOSError", + "ClientConnectorError", + "ClientProxyConnectionError", + "ClientSSLError", + "ClientConnectorSSLError", + "ClientConnectorCertificateError", + "ServerConnectionError", + "ServerTimeoutError", + "ServerDisconnectedError", + "ServerFingerprintMismatch", + "ClientResponseError", + "ClientHttpProxyError", + "WSServerHandshakeError", + "ContentTypeError", + "ClientPayloadError", + "InvalidURL", +) + + +class ClientError(Exception): + """Base class for client connection errors.""" + + +class ClientResponseError(ClientError): + """Base class for exceptions that occur after getting a response. + + request_info: An instance of RequestInfo. + history: A sequence of responses, if redirects occurred. + status: HTTP status code. + message: Error message. + headers: Response headers. + """ + + def __init__( + self, + request_info: RequestInfo, + history: Tuple[ClientResponse, ...], + *, + status: Optional[int] = None, + message: str = "", + headers: Optional[LooseHeaders] = None, + ) -> None: + self.request_info = request_info + if status is not None: + self.status = status + else: + self.status = 0 + self.message = message + self.headers = headers + self.history = history + self.args = (request_info, history) + + def __str__(self) -> str: + return "{}, message={!r}, url={!r}".format( + self.status, + self.message, + self.request_info.real_url, + ) + + def __repr__(self) -> str: + args = f"{self.request_info!r}, {self.history!r}" + if self.status != 0: + args += f", status={self.status!r}" + if self.message != "": + args += f", message={self.message!r}" + if self.headers is not None: + args += f", headers={self.headers!r}" + return f"{type(self).__name__}({args})" + + +class ContentTypeError(ClientResponseError): + """ContentType found is not valid.""" + + +class WSServerHandshakeError(ClientResponseError): + """websocket server handshake error.""" + + +class ClientHttpProxyError(ClientResponseError): + """HTTP proxy error. + + Raised in :class:`aiohttp.connector.TCPConnector` if + proxy responds with status other than ``200 OK`` + on ``CONNECT`` request. + """ + + +class TooManyRedirects(ClientResponseError): + """Client was redirected too many times.""" + + +class ClientConnectionError(ClientError): + """Base class for client socket errors.""" + + +class ClientOSError(ClientConnectionError, OSError): + """OSError error.""" + + +class ClientConnectorError(ClientOSError): + """Client connector error. + + Raised in :class:`aiohttp.connector.TCPConnector` if + a connection can not be established. + """ + + def __init__(self, connection_key: ConnectionKey, os_error: OSError) -> None: + self._conn_key = connection_key + self._os_error = os_error + super().__init__(os_error.errno, os_error.strerror) + self.args = (connection_key, os_error) + + @property + def os_error(self) -> OSError: + return self._os_error + + @property + def host(self) -> str: + return self._conn_key.host + + @property + def port(self) -> Optional[int]: + return self._conn_key.port + + @property + def ssl(self) -> Union[SSLContext, None, bool, "Fingerprint"]: + return self._conn_key.ssl + + def __str__(self) -> str: + return "Cannot connect to host {0.host}:{0.port} ssl:{1} [{2}]".format( + self, self.ssl if self.ssl is not None else "default", self.strerror + ) + + # OSError.__reduce__ does too much black magick + __reduce__ = BaseException.__reduce__ + + +class ClientProxyConnectionError(ClientConnectorError): + """Proxy connection error. + + Raised in :class:`aiohttp.connector.TCPConnector` if + connection to proxy can not be established. + """ + + +class UnixClientConnectorError(ClientConnectorError): + """Unix connector error. + + Raised in :py:class:`aiohttp.connector.UnixConnector` + if connection to unix socket can not be established. + """ + + def __init__( + self, path: str, connection_key: ConnectionKey, os_error: OSError + ) -> None: + self._path = path + super().__init__(connection_key, os_error) + + @property + def path(self) -> str: + return self._path + + def __str__(self) -> str: + return "Cannot connect to unix socket {0.path} ssl:{1} [{2}]".format( + self, self.ssl if self.ssl is not None else "default", self.strerror + ) + + +class ServerConnectionError(ClientConnectionError): + """Server connection errors.""" + + +class ServerDisconnectedError(ServerConnectionError): + """Server disconnected.""" + + def __init__(self, message: Union[RawResponseMessage, str, None] = None) -> None: + if message is None: + message = "Server disconnected" + + self.args = (message,) + self.message = message + + +class ServerTimeoutError(ServerConnectionError, asyncio.TimeoutError): + """Server timeout error.""" + + +class ServerFingerprintMismatch(ServerConnectionError): + """SSL certificate does not match expected fingerprint.""" + + def __init__(self, expected: bytes, got: bytes, host: str, port: int) -> None: + self.expected = expected + self.got = got + self.host = host + self.port = port + self.args = (expected, got, host, port) + + def __repr__(self) -> str: + return "<{} expected={!r} got={!r} host={!r} port={!r}>".format( + self.__class__.__name__, self.expected, self.got, self.host, self.port + ) + + +class ClientPayloadError(ClientError): + """Response payload error.""" + + +class InvalidURL(ClientError, ValueError): + """Invalid URL. + + URL used for fetching is malformed, e.g. it doesn't contains host + part. + """ + + # Derive from ValueError for backward compatibility + + def __init__(self, url: Any) -> None: + # The type of url is not yarl.URL because the exception can be raised + # on URL(url) call + super().__init__(url) + + @property + def url(self) -> Any: + return self.args[0] + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.url}>" + + +class ClientSSLError(ClientConnectorError): + """Base error for ssl.*Errors.""" + + +if ssl is not None: + cert_errors = (ssl.CertificateError,) + cert_errors_bases = ( + ClientSSLError, + ssl.CertificateError, + ) + + ssl_errors = (ssl.SSLError,) + ssl_error_bases = (ClientSSLError, ssl.SSLError) +else: # pragma: no cover + cert_errors = tuple() + cert_errors_bases = ( + ClientSSLError, + ValueError, + ) + + ssl_errors = tuple() + ssl_error_bases = (ClientSSLError,) + + +class ClientConnectorSSLError(*ssl_error_bases): # type: ignore[misc] + """Response ssl error.""" + + +class ClientConnectorCertificateError(*cert_errors_bases): # type: ignore[misc] + """Response certificate error.""" + + def __init__( + self, connection_key: ConnectionKey, certificate_error: Exception + ) -> None: + self._conn_key = connection_key + self._certificate_error = certificate_error + self.args = (connection_key, certificate_error) + + @property + def certificate_error(self) -> Exception: + return self._certificate_error + + @property + def host(self) -> str: + return self._conn_key.host + + @property + def port(self) -> Optional[int]: + return self._conn_key.port + + @property + def ssl(self) -> bool: + return self._conn_key.is_ssl + + def __str__(self) -> str: + return ( + "Cannot connect to host {0.host}:{0.port} ssl:{0.ssl} " + "[{0.certificate_error.__class__.__name__}: " + "{0.certificate_error.args}]".format(self) + ) diff --git a/aiohttp/client_proto.py b/aiohttp/client_proto.py new file mode 100644 index 0000000..3d42cf5 --- /dev/null +++ b/aiohttp/client_proto.py @@ -0,0 +1,269 @@ +import asyncio +from contextlib import suppress +from typing import Any, Optional, Tuple + +from .base_protocol import BaseProtocol +from .client_exceptions import ( + ClientOSError, + ClientPayloadError, + ServerDisconnectedError, + ServerTimeoutError, +) +from .helpers import BaseTimerContext, set_exception, set_result +from .http import HttpResponseParser, RawResponseMessage, WebSocketReader +from .streams import EMPTY_PAYLOAD, DataQueue, StreamReader + + +class ResponseHandler(BaseProtocol, DataQueue[Tuple[RawResponseMessage, StreamReader]]): + """Helper class to adapt between Protocol and StreamReader.""" + + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + BaseProtocol.__init__(self, loop=loop) + DataQueue.__init__(self, loop) + + self._should_close = False + + self._payload: Optional[StreamReader] = None + self._skip_payload = False + self._payload_parser: Optional[WebSocketReader] = None + + self._timer = None + + self._tail = b"" + self._upgraded = False + self._parser: Optional[HttpResponseParser] = None + + self._read_timeout: Optional[float] = None + self._read_timeout_handle: Optional[asyncio.TimerHandle] = None + + self._timeout_ceil_threshold: Optional[float] = 5 + + self.closed: asyncio.Future[None] = self._loop.create_future() + + @property + def upgraded(self) -> bool: + return self._upgraded + + @property + def should_close(self) -> bool: + if self._payload is not None and not self._payload.is_eof(): + return True + + return ( + self._should_close + or self._upgraded + or self.exception() is not None + or self._payload_parser is not None + or len(self) > 0 + or bool(self._tail) + ) + + def force_close(self) -> None: + self._should_close = True + + def close(self) -> None: + transport = self.transport + if transport is not None: + transport.close() + self.transport = None + self._payload = None + self._drop_timeout() + + def is_connected(self) -> bool: + return self.transport is not None and not self.transport.is_closing() + + def connection_lost(self, exc: Optional[BaseException]) -> None: + self._drop_timeout() + + if exc is not None: + set_exception(self.closed, exc) + else: + set_result(self.closed, None) + + if self._payload_parser is not None: + with suppress(Exception): + self._payload_parser.feed_eof() + + uncompleted = None + if self._parser is not None: + try: + uncompleted = self._parser.feed_eof() + except Exception: + if self._payload is not None: + self._payload.set_exception( + ClientPayloadError("Response payload is not completed") + ) + + if not self.is_eof(): + if isinstance(exc, OSError): + exc = ClientOSError(*exc.args) + if exc is None: + exc = ServerDisconnectedError(uncompleted) + # assigns self._should_close to True as side effect, + # we do it anyway below + self.set_exception(exc) + + self._should_close = True + self._parser = None + self._payload = None + self._payload_parser = None + self._reading_paused = False + + super().connection_lost(exc) + + def eof_received(self) -> None: + # should call parser.feed_eof() most likely + self._drop_timeout() + + def pause_reading(self) -> None: + super().pause_reading() + self._drop_timeout() + + def resume_reading(self) -> None: + super().resume_reading() + self._reschedule_timeout() + + def set_exception(self, exc: BaseException) -> None: + self._should_close = True + self._drop_timeout() + super().set_exception(exc) + + def set_parser(self, parser: Any, payload: Any) -> None: + # TODO: actual types are: + # parser: WebSocketReader + # payload: FlowControlDataQueue + # but they are not generi enough + # Need an ABC for both types + self._payload = payload + self._payload_parser = parser + + self._drop_timeout() + + if self._tail: + data, self._tail = self._tail, b"" + self.data_received(data) + + def set_response_params( + self, + *, + timer: Optional[BaseTimerContext] = None, + skip_payload: bool = False, + read_until_eof: bool = False, + auto_decompress: bool = True, + read_timeout: Optional[float] = None, + read_bufsize: int = 2**16, + timeout_ceil_threshold: float = 5, + max_line_size: int = 8190, + max_field_size: int = 8190, + ) -> None: + self._skip_payload = skip_payload + + self._read_timeout = read_timeout + + self._timeout_ceil_threshold = timeout_ceil_threshold + + self._parser = HttpResponseParser( + self, + self._loop, + read_bufsize, + timer=timer, + payload_exception=ClientPayloadError, + response_with_body=not skip_payload, + read_until_eof=read_until_eof, + auto_decompress=auto_decompress, + max_line_size=max_line_size, + max_field_size=max_field_size, + ) + + if self._tail: + data, self._tail = self._tail, b"" + self.data_received(data) + + def _drop_timeout(self) -> None: + if self._read_timeout_handle is not None: + self._read_timeout_handle.cancel() + self._read_timeout_handle = None + + def _reschedule_timeout(self) -> None: + timeout = self._read_timeout + if self._read_timeout_handle is not None: + self._read_timeout_handle.cancel() + + if timeout: + self._read_timeout_handle = self._loop.call_later( + timeout, self._on_read_timeout + ) + else: + self._read_timeout_handle = None + + def start_timeout(self) -> None: + self._reschedule_timeout() + + def _on_read_timeout(self) -> None: + exc = ServerTimeoutError("Timeout on reading data from socket") + self.set_exception(exc) + if self._payload is not None: + self._payload.set_exception(exc) + + def data_received(self, data: bytes) -> None: + self._reschedule_timeout() + + if not data: + return + + # custom payload parser + if self._payload_parser is not None: + eof, tail = self._payload_parser.feed_data(data) + if eof: + self._payload = None + self._payload_parser = None + + if tail: + self.data_received(tail) + return + else: + if self._upgraded or self._parser is None: + # i.e. websocket connection, websocket parser is not set yet + self._tail += data + else: + # parse http messages + try: + messages, upgraded, tail = self._parser.feed_data(data) + except BaseException as exc: + if self.transport is not None: + # connection.release() could be called BEFORE + # data_received(), the transport is already + # closed in this case + self.transport.close() + # should_close is True after the call + self.set_exception(exc) + return + + self._upgraded = upgraded + + payload: Optional[StreamReader] = None + for message, payload in messages: + if message.should_close: + self._should_close = True + + self._payload = payload + + if self._skip_payload or message.code in (204, 304): + self.feed_data((message, EMPTY_PAYLOAD), 0) + else: + self.feed_data((message, payload), 0) + if payload is not None: + # new message(s) was processed + # register timeout handler unsubscribing + # either on end-of-stream or immediately for + # EMPTY_PAYLOAD + if payload is not EMPTY_PAYLOAD: + payload.on_eof(self._drop_timeout) + else: + self._drop_timeout() + + if tail: + if upgraded: + self.data_received(tail) + else: + self._tail = tail diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py new file mode 100644 index 0000000..6766473 --- /dev/null +++ b/aiohttp/client_reqrep.py @@ -0,0 +1,1089 @@ +import asyncio +import codecs +import contextlib +import dataclasses +import functools +import io +import re +import sys +import traceback +import warnings +from hashlib import md5, sha1, sha256 +from http.cookies import CookieError, Morsel, SimpleCookie +from types import MappingProxyType, TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + List, + Literal, + Mapping, + Optional, + Tuple, + Type, + Union, + cast, +) + +from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy +from yarl import URL + +from . import hdrs, helpers, http, multipart, payload +from .abc import AbstractStreamWriter +from .client_exceptions import ( + ClientConnectionError, + ClientOSError, + ClientResponseError, + ContentTypeError, + InvalidURL, + ServerFingerprintMismatch, +) +from .compression_utils import HAS_BROTLI +from .formdata import FormData +from .hdrs import CONTENT_TYPE +from .helpers import ( + BaseTimerContext, + BasicAuth, + HeadersMixin, + TimerNoop, + basicauth_from_netrc, + is_expected_content_type, + netrc_from_env, + noop, + parse_mimetype, + reify, + set_result, +) +from .http import SERVER_SOFTWARE, HttpVersion10, HttpVersion11, StreamWriter +from .log import client_logger +from .streams import StreamReader +from .typedefs import ( + DEFAULT_JSON_DECODER, + JSONDecoder, + LooseCookies, + LooseHeaders, + RawHeaders, +) + +try: + import ssl + from ssl import SSLContext +except ImportError: # pragma: no cover + ssl = None # type: ignore[assignment] + SSLContext = object # type: ignore[misc,assignment] + + +__all__ = ("ClientRequest", "ClientResponse", "RequestInfo", "Fingerprint") + + +if TYPE_CHECKING: # pragma: no cover + from .client import ClientSession + from .connector import Connection + from .tracing import Trace + + +_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") + + +def _gen_default_accept_encoding() -> str: + return "gzip, deflate, br" if HAS_BROTLI else "gzip, deflate" + + +@dataclasses.dataclass(frozen=True) +class ContentDisposition: + type: Optional[str] + parameters: "MappingProxyType[str, str]" + filename: Optional[str] + + +@dataclasses.dataclass(frozen=True) +class RequestInfo: + url: URL + method: str + headers: "CIMultiDictProxy[str]" + real_url: URL + + +class Fingerprint: + HASHFUNC_BY_DIGESTLEN = { + 16: md5, + 20: sha1, + 32: sha256, + } + + def __init__(self, fingerprint: bytes) -> None: + digestlen = len(fingerprint) + hashfunc = self.HASHFUNC_BY_DIGESTLEN.get(digestlen) + if not hashfunc: + raise ValueError("fingerprint has invalid length") + elif hashfunc is md5 or hashfunc is sha1: + raise ValueError( + "md5 and sha1 are insecure and " "not supported. Use sha256." + ) + self._hashfunc = hashfunc + self._fingerprint = fingerprint + + @property + def fingerprint(self) -> bytes: + return self._fingerprint + + def check(self, transport: asyncio.Transport) -> None: + if not transport.get_extra_info("sslcontext"): + return + sslobj = transport.get_extra_info("ssl_object") + cert = sslobj.getpeercert(binary_form=True) + got = self._hashfunc(cert).digest() + if got != self._fingerprint: + host, port, *_ = transport.get_extra_info("peername") + raise ServerFingerprintMismatch(self._fingerprint, got, host, port) + + +if ssl is not None: + SSL_ALLOWED_TYPES = (ssl.SSLContext, bool, Fingerprint, type(None)) +else: # pragma: no cover + SSL_ALLOWED_TYPES = type(None) + + +@dataclasses.dataclass(frozen=True) +class ConnectionKey: + # the key should contain an information about used proxy / TLS + # to prevent reusing wrong connections from a pool + host: str + port: Optional[int] + is_ssl: bool + ssl: Union[SSLContext, None, Literal[False], Fingerprint] + proxy: Optional[URL] + proxy_auth: Optional[BasicAuth] + proxy_headers_hash: Optional[int] # hash(CIMultiDict) + + +class ClientRequest: + GET_METHODS = { + hdrs.METH_GET, + hdrs.METH_HEAD, + hdrs.METH_OPTIONS, + hdrs.METH_TRACE, + } + POST_METHODS = {hdrs.METH_PATCH, hdrs.METH_POST, hdrs.METH_PUT} + ALL_METHODS = GET_METHODS.union(POST_METHODS).union({hdrs.METH_DELETE}) + + DEFAULT_HEADERS = { + hdrs.ACCEPT: "*/*", + hdrs.ACCEPT_ENCODING: _gen_default_accept_encoding(), + } + + body = b"" + auth = None + response = None + + _writer = None # async task for streaming data + _continue = None # waiter future for '100 Continue' response + + # N.B. + # Adding __del__ method with self._writer closing doesn't make sense + # because _writer is instance method, thus it keeps a reference to self. + # Until writer has finished finalizer will not be called. + + def __init__( + self, + method: str, + url: URL, + *, + params: Optional[Mapping[str, str]] = None, + headers: Optional[LooseHeaders] = None, + skip_auto_headers: Iterable[str] = frozenset(), + data: Any = None, + cookies: Optional[LooseCookies] = None, + auth: Optional[BasicAuth] = None, + version: http.HttpVersion = http.HttpVersion11, + compress: Optional[str] = None, + chunked: Optional[bool] = None, + expect100: bool = False, + loop: asyncio.AbstractEventLoop, + response_class: Optional[Type["ClientResponse"]] = None, + proxy: Optional[URL] = None, + proxy_auth: Optional[BasicAuth] = None, + timer: Optional[BaseTimerContext] = None, + session: Optional["ClientSession"] = None, + ssl: Union[SSLContext, Literal[False], Fingerprint, None] = None, + proxy_headers: Optional[LooseHeaders] = None, + traces: Optional[List["Trace"]] = None, + trust_env: bool = False, + server_hostname: Optional[str] = None, + ): + match = _CONTAINS_CONTROL_CHAR_RE.search(method) + if match: + raise ValueError( + f"Method cannot contain non-token characters {method!r} " + f"(found at least {match.group()!r})" + ) + assert isinstance(url, URL), url + assert isinstance(proxy, (URL, type(None))), proxy + # FIXME: session is None in tests only, need to fix tests + # assert session is not None + self._session = cast("ClientSession", session) + if params: + q = MultiDict(url.query) + url2 = url.with_query(params) + q.extend(url2.query) + url = url.with_query(q) + self.original_url = url + self.url = url.with_fragment(None) + self.method = method.upper() + self.chunked = chunked + self.compress = compress + self.loop = loop + self.length = None + if response_class is None: + real_response_class = ClientResponse + else: + real_response_class = response_class + self.response_class: Type[ClientResponse] = real_response_class + self._timer = timer if timer is not None else TimerNoop() + self._ssl = ssl + self.server_hostname = server_hostname + + if loop.get_debug(): + self._source_traceback = traceback.extract_stack(sys._getframe(1)) + + self.update_version(version) + self.update_host(url) + self.update_headers(headers) + self.update_auto_headers(skip_auto_headers) + self.update_cookies(cookies) + self.update_content_encoding(data) + self.update_auth(auth, trust_env) + self.update_proxy(proxy, proxy_auth, proxy_headers) + + self.update_body_from_data(data) + if data is not None or self.method not in self.GET_METHODS: + self.update_transfer_encoding() + self.update_expect_continue(expect100) + if traces is None: + traces = [] + self._traces = traces + + def is_ssl(self) -> bool: + return self.url.scheme in ("https", "wss") + + @property + def ssl(self) -> Union["SSLContext", None, Literal[False], Fingerprint]: + return self._ssl + + @property + def connection_key(self) -> ConnectionKey: + proxy_headers = self.proxy_headers + if proxy_headers: + h: Optional[int] = hash(tuple((k, v) for k, v in proxy_headers.items())) + else: + h = None + return ConnectionKey( + self.host, + self.port, + self.is_ssl(), + self.ssl, + self.proxy, + self.proxy_auth, + h, + ) + + @property + def host(self) -> str: + ret = self.url.raw_host + assert ret is not None + return ret + + @property + def port(self) -> Optional[int]: + return self.url.port + + @property + def request_info(self) -> RequestInfo: + headers: CIMultiDictProxy[str] = CIMultiDictProxy(self.headers) + return RequestInfo(self.url, self.method, headers, self.original_url) + + def update_host(self, url: URL) -> None: + """Update destination host, port and connection type (ssl).""" + # get host/port + if not url.raw_host: + raise InvalidURL(url) + + # basic auth info + username, password = url.user, url.password + if username: + self.auth = helpers.BasicAuth(username, password or "") + + def update_version(self, version: Union[http.HttpVersion, str]) -> None: + """Convert request version to two elements tuple. + + parser HTTP version '1.1' => (1, 1) + """ + if isinstance(version, str): + v = [part.strip() for part in version.split(".", 1)] + try: + version = http.HttpVersion(int(v[0]), int(v[1])) + except ValueError: + raise ValueError( + f"Can not parse http version number: {version}" + ) from None + self.version = version + + def update_headers(self, headers: Optional[LooseHeaders]) -> None: + """Update request headers.""" + self.headers: CIMultiDict[str] = CIMultiDict() + + # add host + netloc = cast(str, self.url.raw_host) + if helpers.is_ipv6_address(netloc): + netloc = f"[{netloc}]" + # See https://github.com/aio-libs/aiohttp/issues/3636. + netloc = netloc.rstrip(".") + if self.url.port is not None and not self.url.is_default_port(): + netloc += ":" + str(self.url.port) + self.headers[hdrs.HOST] = netloc + + if headers: + if isinstance(headers, (dict, MultiDictProxy, MultiDict)): + headers = headers.items() # type: ignore[assignment] + + for key, value in headers: # type: ignore[misc] + # A special case for Host header + if key.lower() == "host": + self.headers[key] = value + else: + self.headers.add(key, value) + + def update_auto_headers(self, skip_auto_headers: Iterable[str]) -> None: + self.skip_auto_headers = CIMultiDict( + (hdr, None) for hdr in sorted(skip_auto_headers) + ) + used_headers = self.headers.copy() + used_headers.extend(self.skip_auto_headers) # type: ignore[arg-type] + + for hdr, val in self.DEFAULT_HEADERS.items(): + if hdr not in used_headers: + self.headers.add(hdr, val) + + if hdrs.USER_AGENT not in used_headers: + self.headers[hdrs.USER_AGENT] = SERVER_SOFTWARE + + def update_cookies(self, cookies: Optional[LooseCookies]) -> None: + """Update request cookies header.""" + if not cookies: + return + + c: SimpleCookie[str] = SimpleCookie() + if hdrs.COOKIE in self.headers: + c.load(self.headers.get(hdrs.COOKIE, "")) + del self.headers[hdrs.COOKIE] + + if isinstance(cookies, Mapping): + iter_cookies = cookies.items() + else: + iter_cookies = cookies # type: ignore[assignment] + for name, value in iter_cookies: + if isinstance(value, Morsel): + # Preserve coded_value + mrsl_val = value.get(value.key, Morsel()) + mrsl_val.set(value.key, value.value, value.coded_value) + c[name] = mrsl_val + else: + c[name] = value # type: ignore[assignment] + + self.headers[hdrs.COOKIE] = c.output(header="", sep=";").strip() + + def update_content_encoding(self, data: Any) -> None: + """Set request content encoding.""" + if data is None: + return + + enc = self.headers.get(hdrs.CONTENT_ENCODING, "").lower() + if enc: + if self.compress: + raise ValueError( + "compress can not be set " "if Content-Encoding header is set" + ) + elif self.compress: + if not isinstance(self.compress, str): + self.compress = "deflate" + self.headers[hdrs.CONTENT_ENCODING] = self.compress + self.chunked = True # enable chunked, no need to deal with length + + def update_transfer_encoding(self) -> None: + """Analyze transfer-encoding header.""" + te = self.headers.get(hdrs.TRANSFER_ENCODING, "").lower() + + if "chunked" in te: + if self.chunked: + raise ValueError( + "chunked can not be set " + 'if "Transfer-Encoding: chunked" header is set' + ) + + elif self.chunked: + if hdrs.CONTENT_LENGTH in self.headers: + raise ValueError( + "chunked can not be set " "if Content-Length header is set" + ) + + self.headers[hdrs.TRANSFER_ENCODING] = "chunked" + else: + if hdrs.CONTENT_LENGTH not in self.headers: + self.headers[hdrs.CONTENT_LENGTH] = str(len(self.body)) + + def update_auth(self, auth: Optional[BasicAuth], trust_env: bool = False) -> None: + """Set basic auth.""" + if auth is None: + auth = self.auth + if auth is None and trust_env and self.url.host is not None: + netrc_obj = netrc_from_env() + with contextlib.suppress(LookupError): + auth = basicauth_from_netrc(netrc_obj, self.url.host) + if auth is None: + return + + if not isinstance(auth, helpers.BasicAuth): + raise TypeError("BasicAuth() tuple is required instead") + + self.headers[hdrs.AUTHORIZATION] = auth.encode() + + def update_body_from_data(self, body: Any) -> None: + if body is None: + return + + # FormData + if isinstance(body, FormData): + body = body() + + try: + body = payload.PAYLOAD_REGISTRY.get(body, disposition=None) + except payload.LookupError: + boundary = None + if CONTENT_TYPE in self.headers: + boundary = parse_mimetype(self.headers[CONTENT_TYPE]).parameters.get( + "boundary" + ) + body = FormData(body, boundary=boundary)() + + self.body = body + + # enable chunked encoding if needed + if not self.chunked: + if hdrs.CONTENT_LENGTH not in self.headers: + size = body.size + if size is None: + self.chunked = True + else: + if hdrs.CONTENT_LENGTH not in self.headers: + self.headers[hdrs.CONTENT_LENGTH] = str(size) + + # copy payload headers + assert body.headers + for key, value in body.headers.items(): + if key in self.headers: + continue + if key in self.skip_auto_headers: + continue + self.headers[key] = value + + def update_expect_continue(self, expect: bool = False) -> None: + if expect: + self.headers[hdrs.EXPECT] = "100-continue" + elif self.headers.get(hdrs.EXPECT, "").lower() == "100-continue": + expect = True + + if expect: + self._continue = self.loop.create_future() + + def update_proxy( + self, + proxy: Optional[URL], + proxy_auth: Optional[BasicAuth], + proxy_headers: Optional[LooseHeaders], + ) -> None: + if proxy_auth and not isinstance(proxy_auth, helpers.BasicAuth): + raise ValueError("proxy_auth must be None or BasicAuth() tuple") + self.proxy = proxy + self.proxy_auth = proxy_auth + self.proxy_headers = proxy_headers + + def keep_alive(self) -> bool: + if self.version < HttpVersion10: + # keep alive not supported at all + return False + if self.version == HttpVersion10: + if self.headers.get(hdrs.CONNECTION) == "keep-alive": + return True + else: # no headers means we close for Http 1.0 + return False + elif self.headers.get(hdrs.CONNECTION) == "close": + return False + + return True + + async def write_bytes( + self, writer: AbstractStreamWriter, conn: "Connection" + ) -> None: + """Support coroutines that yields bytes objects.""" + # 100 response + if self._continue is not None: + await writer.drain() + await self._continue + + protocol = conn.protocol + assert protocol is not None + try: + if isinstance(self.body, payload.Payload): + await self.body.write(writer) + else: + if isinstance(self.body, (bytes, bytearray)): + self.body = (self.body,) # type: ignore[assignment] + + for chunk in self.body: + await writer.write(chunk) # type: ignore[arg-type] + + await writer.write_eof() + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + protocol.set_exception(exc) + else: + new_exc = ClientOSError( + exc.errno, "Can not write request body for %s" % self.url + ) + new_exc.__context__ = exc + new_exc.__cause__ = exc + protocol.set_exception(new_exc) + except asyncio.CancelledError as exc: + if not conn.closed: + protocol.set_exception(exc) + except Exception as exc: + protocol.set_exception(exc) + else: + protocol.start_timeout() + finally: + self._writer = None + + async def send(self, conn: "Connection") -> "ClientResponse": + # Specify request target: + # - CONNECT request must send authority form URI + # - not CONNECT proxy must send absolute form URI + # - most common is origin form URI + if self.method == hdrs.METH_CONNECT: + connect_host = self.url.raw_host + assert connect_host is not None + if helpers.is_ipv6_address(connect_host): + connect_host = f"[{connect_host}]" + path = f"{connect_host}:{self.url.port}" + elif self.proxy and not self.is_ssl(): + path = str(self.url) + else: + path = self.url.raw_path + if self.url.raw_query_string: + path += "?" + self.url.raw_query_string + + protocol = conn.protocol + assert protocol is not None + writer = StreamWriter( + protocol, + self.loop, + on_chunk_sent=functools.partial( + self._on_chunk_request_sent, self.method, self.url + ), + on_headers_sent=functools.partial( + self._on_headers_request_sent, self.method, self.url + ), + ) + + if self.compress: + writer.enable_compression(self.compress) + + if self.chunked is not None: + writer.enable_chunking() + + # set default content-type + if ( + self.method in self.POST_METHODS + and hdrs.CONTENT_TYPE not in self.skip_auto_headers + and hdrs.CONTENT_TYPE not in self.headers + ): + self.headers[hdrs.CONTENT_TYPE] = "application/octet-stream" + + # set the connection header + connection = self.headers.get(hdrs.CONNECTION) + if not connection: + if self.keep_alive(): + if self.version == HttpVersion10: + connection = "keep-alive" + else: + if self.version == HttpVersion11: + connection = "close" + + if connection is not None: + self.headers[hdrs.CONNECTION] = connection + + # status + headers + status_line = "{0} {1} HTTP/{2[0]}.{2[1]}".format( + self.method, path, self.version + ) + await writer.write_headers(status_line, self.headers) + + self._writer = self.loop.create_task(self.write_bytes(writer, conn)) + + response_class = self.response_class + assert response_class is not None + self.response = response_class( + self.method, + self.original_url, + writer=self._writer, + continue100=self._continue, + timer=self._timer, + request_info=self.request_info, + traces=self._traces, + loop=self.loop, + session=self._session, + ) + return self.response + + async def close(self) -> None: + if self._writer is not None: + try: + await self._writer + finally: + self._writer = None + + def terminate(self) -> None: + if self._writer is not None: + if not self.loop.is_closed(): + self._writer.cancel() + self._writer = None + + async def _on_chunk_request_sent(self, method: str, url: URL, chunk: bytes) -> None: + for trace in self._traces: + await trace.send_request_chunk_sent(method, url, chunk) + + async def _on_headers_request_sent( + self, method: str, url: URL, headers: "CIMultiDict[str]" + ) -> None: + for trace in self._traces: + await trace.send_request_headers(method, url, headers) + + +class ClientResponse(HeadersMixin): + # Some of these attributes are None when created, + # but will be set by the start() method. + # As the end user will likely never see the None values, we cheat the types below. + # from the Status-Line of the response + version = None # HTTP-Version + status: int = None # type: ignore[assignment] # Status-Code + reason = None # Reason-Phrase + + content: StreamReader = None # type: ignore[assignment] # Payload stream + _headers: CIMultiDictProxy[str] = None # type: ignore[assignment] + _raw_headers: RawHeaders = None # type: ignore[assignment] + + _connection = None # current connection + _source_traceback: Optional[traceback.StackSummary] = None + # set up by ClientRequest after ClientResponse object creation + # post-init stage allows to not change ctor signature + _closed = True # to allow __del__ for non-initialized properly response + _released = False + + def __init__( + self, + method: str, + url: URL, + *, + writer: "asyncio.Task[None]", + continue100: Optional["asyncio.Future[bool]"], + timer: Optional[BaseTimerContext], + request_info: RequestInfo, + traces: List["Trace"], + loop: asyncio.AbstractEventLoop, + session: "ClientSession", + ) -> None: + assert isinstance(url, URL) + super().__init__() + + self.method = method + self.cookies: SimpleCookie[str] = SimpleCookie() + + self._real_url = url + self._url = url.with_fragment(None) + self._body: Optional[bytes] = None + self._writer: Optional[asyncio.Task[None]] = writer + self._continue = continue100 # None by default + self._closed = True + self._history: Tuple[ClientResponse, ...] = () + self._request_info = request_info + self._timer = timer if timer is not None else TimerNoop() + self._cache: Dict[str, Any] = {} + self._traces = traces + self._loop = loop + # store a reference to session #1985 + self._session: Optional[ClientSession] = session + # Save reference to _resolve_charset, so that get_encoding() will still + # work after the response has finished reading the body. + if session is None: + # TODO: Fix session=None in tests (see ClientRequest.__init__). + self._resolve_charset: Callable[ + ["ClientResponse", bytes], str + ] = lambda *_: "utf-8" + else: + self._resolve_charset = session._resolve_charset + if loop.get_debug(): + self._source_traceback = traceback.extract_stack(sys._getframe(1)) + + @reify + def url(self) -> URL: + return self._url + + @reify + def real_url(self) -> URL: + return self._real_url + + @reify + def host(self) -> str: + assert self._url.host is not None + return self._url.host + + @reify + def headers(self) -> "CIMultiDictProxy[str]": + return self._headers + + @reify + def raw_headers(self) -> RawHeaders: + return self._raw_headers + + @reify + def request_info(self) -> RequestInfo: + return self._request_info + + @reify + def content_disposition(self) -> Optional[ContentDisposition]: + raw = self._headers.get(hdrs.CONTENT_DISPOSITION) + if raw is None: + return None + disposition_type, params_dct = multipart.parse_content_disposition(raw) + params = MappingProxyType(params_dct) + filename = multipart.content_disposition_filename(params) + return ContentDisposition(disposition_type, params, filename) + + def __del__(self, _warnings: Any = warnings) -> None: + if self._closed: + return + + if self._connection is not None: + self._connection.release() + self._cleanup_writer() + + if self._loop.get_debug(): + _warnings.warn( + f"Unclosed response {self!r}", ResourceWarning, source=self + ) + context = {"client_response": self, "message": "Unclosed response"} + if self._source_traceback: + context["source_traceback"] = self._source_traceback + self._loop.call_exception_handler(context) + + def __repr__(self) -> str: + out = io.StringIO() + ascii_encodable_url = str(self.url) + if self.reason: + ascii_encodable_reason = self.reason.encode( + "ascii", "backslashreplace" + ).decode("ascii") + else: + ascii_encodable_reason = self.reason + print( + "".format( + ascii_encodable_url, self.status, ascii_encodable_reason + ), + file=out, + ) + print(self.headers, file=out) + return out.getvalue() + + @property + def connection(self) -> Optional["Connection"]: + return self._connection + + @reify + def history(self) -> Tuple["ClientResponse", ...]: + """A sequence of responses, if redirects occurred.""" + return self._history + + @reify + def links(self) -> "MultiDictProxy[MultiDictProxy[Union[str, URL]]]": + links_str = ", ".join(self.headers.getall("link", [])) + + if not links_str: + return MultiDictProxy(MultiDict()) + + links: MultiDict[MultiDictProxy[Union[str, URL]]] = MultiDict() + + for val in re.split(r",(?=\s*<)", links_str): + match = re.match(r"\s*<(.*)>(.*)", val) + if match is None: # pragma: no cover + # the check exists to suppress mypy error + continue + url, params_str = match.groups() + params = params_str.split(";")[1:] + + link: MultiDict[Union[str, URL]] = MultiDict() + + for param in params: + match = re.match(r"^\s*(\S*)\s*=\s*(['\"]?)(.*?)(\2)\s*$", param, re.M) + if match is None: # pragma: no cover + # the check exists to suppress mypy error + continue + key, _, value, _ = match.groups() + + link.add(key, value) + + key = link.get("rel", url) + + link.add("url", self.url.join(URL(url))) + + links.add(str(key), MultiDictProxy(link)) + + return MultiDictProxy(links) + + async def start(self, connection: "Connection") -> "ClientResponse": + """Start response processing.""" + self._closed = False + self._protocol = connection.protocol + self._connection = connection + + with self._timer: + while True: + # read response + try: + protocol = self._protocol + message, payload = await protocol.read() # type: ignore[union-attr] + except http.HttpProcessingError as exc: + raise ClientResponseError( + self.request_info, + self.history, + status=exc.code, + message=exc.message, + headers=exc.headers, + ) from exc + + if message.code < 100 or message.code > 199 or message.code == 101: + break + + if self._continue is not None: + set_result(self._continue, True) + self._continue = None + + # payload eof handler + payload.on_eof(self._response_eof) + + # response status + self.version = message.version + self.status = message.code + self.reason = message.reason + + # headers + self._headers = message.headers # type is CIMultiDictProxy + self._raw_headers = message.raw_headers # type is Tuple[bytes, bytes] + + # payload + self.content = payload + + # cookies + for hdr in self.headers.getall(hdrs.SET_COOKIE, ()): + try: + self.cookies.load(hdr) + except CookieError as exc: + client_logger.warning("Can not load response cookies: %s", exc) + return self + + def _response_eof(self) -> None: + if self._closed: + return + + if self._connection is not None: + # websocket, protocol could be None because + # connection could be detached + if ( + self._connection.protocol is not None + and self._connection.protocol.upgraded + ): + return + + self._connection.release() + self._connection = None + + self._closed = True + self._cleanup_writer() + + @property + def closed(self) -> bool: + return self._closed + + def close(self) -> None: + if not self._released: + self._notify_content() + if self._closed: + return + + self._closed = True + if self._loop.is_closed(): + return + + if self._connection is not None: + self._connection.close() + self._connection = None + self._cleanup_writer() + + def release(self) -> Any: + if not self._released: + self._notify_content() + if self._closed: + return noop() + + self._closed = True + if self._connection is not None: + self._connection.release() + self._connection = None + + self._cleanup_writer() + return noop() + + @property + def ok(self) -> bool: + """Returns ``True`` if ``status`` is less than ``400``, ``False`` if not. + + This is **not** a check for ``200 OK`` but a check that the response + status is under 400. + """ + return 400 > self.status + + def raise_for_status(self) -> None: + if not self.ok: + # reason should always be not None for a started response + assert self.reason is not None + self.release() + raise ClientResponseError( + self.request_info, + self.history, + status=self.status, + message=self.reason, + headers=self.headers, + ) + + def _cleanup_writer(self) -> None: + if self._writer is not None: + self._writer.cancel() + self._writer = None + self._session = None + + def _notify_content(self) -> None: + content = self.content + # content can be None here, but the types are cheated elsewhere. + if content and content.exception() is None: # type: ignore[truthy-bool] + content.set_exception(ClientConnectionError("Connection closed")) + self._released = True + + async def wait_for_close(self) -> None: + if self._writer is not None: + try: + await self._writer + finally: + self._writer = None + self.release() + + async def read(self) -> bytes: + """Read response payload.""" + if self._body is None: + try: + self._body = await self.content.read() + for trace in self._traces: + await trace.send_response_chunk_received( + self.method, self.url, self._body + ) + except BaseException: + self.close() + raise + elif self._released: + raise ClientConnectionError("Connection closed") + + return self._body + + def get_encoding(self) -> str: + ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower() + mimetype = helpers.parse_mimetype(ctype) + + encoding = mimetype.parameters.get("charset") + if encoding: + with contextlib.suppress(LookupError): + return codecs.lookup(encoding).name + + if mimetype.type == "application" and ( + mimetype.subtype == "json" or mimetype.subtype == "rdap" + ): + # RFC 7159 states that the default encoding is UTF-8. + # RFC 7483 defines application/rdap+json + return "utf-8" + + if self._body is None: + raise RuntimeError( + "Cannot compute fallback encoding of a not yet read body" + ) + + return self._resolve_charset(self, self._body) + + async def text(self, encoding: Optional[str] = None, errors: str = "strict") -> str: + """Read response payload and decode.""" + if self._body is None: + await self.read() + + if encoding is None: + encoding = self.get_encoding() + + return self._body.decode(encoding, errors=errors) # type: ignore[union-attr] + + async def json( + self, + *, + encoding: Optional[str] = None, + loads: JSONDecoder = DEFAULT_JSON_DECODER, + content_type: Optional[str] = "application/json", + ) -> Any: + """Read and decodes JSON response.""" + if self._body is None: + await self.read() + + if content_type: + if not is_expected_content_type(self.content_type, content_type): + raise ContentTypeError( + self.request_info, + self.history, + message=( + "Attempt to decode JSON with " + "unexpected mimetype: %s" % self.content_type + ), + headers=self.headers, + ) + + if encoding is None: + encoding = self.get_encoding() + + return loads(self._body.decode(encoding)) # type: ignore[union-attr] + + async def __aenter__(self) -> "ClientResponse": + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + # similar to _RequestContextManager, we do not need to check + # for exceptions, response object can close connection + # if state is broken + self.release() diff --git a/aiohttp/client_ws.py b/aiohttp/client_ws.py new file mode 100644 index 0000000..0a010fa --- /dev/null +++ b/aiohttp/client_ws.py @@ -0,0 +1,327 @@ +"""WebSocket client for asyncio.""" + +import asyncio +import dataclasses +import sys +from typing import Any, Final, Optional, cast + +from .client_exceptions import ClientError +from .client_reqrep import ClientResponse +from .helpers import call_later, set_result +from .http import ( + WS_CLOSED_MESSAGE, + WS_CLOSING_MESSAGE, + WebSocketError, + WSCloseCode, + WSMessage, + WSMsgType, +) +from .http_websocket import WebSocketWriter # WSMessage +from .streams import EofStream, FlowControlDataQueue +from .typedefs import ( + DEFAULT_JSON_DECODER, + DEFAULT_JSON_ENCODER, + JSONDecoder, + JSONEncoder, +) + +if sys.version_info >= (3, 11): + import asyncio as async_timeout +else: + import async_timeout + + +@dataclasses.dataclass(frozen=True) +class ClientWSTimeout: + ws_receive: Optional[float] = None + ws_close: Optional[float] = None + + +DEFAULT_WS_CLIENT_TIMEOUT: Final[ClientWSTimeout] = ClientWSTimeout( + ws_receive=None, ws_close=10.0 +) + + +class ClientWebSocketResponse: + def __init__( + self, + reader: "FlowControlDataQueue[WSMessage]", + writer: WebSocketWriter, + protocol: Optional[str], + response: ClientResponse, + timeout: ClientWSTimeout, + autoclose: bool, + autoping: bool, + loop: asyncio.AbstractEventLoop, + *, + heartbeat: Optional[float] = None, + compress: int = 0, + client_notakeover: bool = False, + ) -> None: + self._response = response + self._conn = response.connection + + self._writer = writer + self._reader = reader + self._protocol = protocol + self._closed = False + self._closing = False + self._close_code: Optional[int] = None + self._timeout: ClientWSTimeout = timeout + self._autoclose = autoclose + self._autoping = autoping + self._heartbeat = heartbeat + self._heartbeat_cb: Optional[asyncio.TimerHandle] = None + if heartbeat is not None: + self._pong_heartbeat = heartbeat / 2.0 + self._pong_response_cb: Optional[asyncio.TimerHandle] = None + self._loop = loop + self._waiting: Optional[asyncio.Future[bool]] = None + self._exception: Optional[BaseException] = None + self._compress = compress + self._client_notakeover = client_notakeover + + self._reset_heartbeat() + + def _cancel_heartbeat(self) -> None: + if self._pong_response_cb is not None: + self._pong_response_cb.cancel() + self._pong_response_cb = None + + if self._heartbeat_cb is not None: + self._heartbeat_cb.cancel() + self._heartbeat_cb = None + + def _reset_heartbeat(self) -> None: + self._cancel_heartbeat() + + if self._heartbeat is not None: + self._heartbeat_cb = call_later( + self._send_heartbeat, + self._heartbeat, + self._loop, + timeout_ceil_threshold=self._conn._connector._timeout_ceil_threshold + if self._conn is not None + else 5, + ) + + def _send_heartbeat(self) -> None: + if self._heartbeat is not None and not self._closed: + # fire-and-forget a task is not perfect but maybe ok for + # sending ping. Otherwise we need a long-living heartbeat + # task in the class. + self._loop.create_task(self._writer.ping()) # type: ignore[unused-awaitable] + + if self._pong_response_cb is not None: + self._pong_response_cb.cancel() + self._pong_response_cb = call_later( + self._pong_not_received, + self._pong_heartbeat, + self._loop, + timeout_ceil_threshold=self._conn._connector._timeout_ceil_threshold + if self._conn is not None + else 5, + ) + + def _pong_not_received(self) -> None: + if not self._closed: + self._closed = True + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._exception = asyncio.TimeoutError() + self._response.close() + + @property + def closed(self) -> bool: + return self._closed + + @property + def close_code(self) -> Optional[int]: + return self._close_code + + @property + def protocol(self) -> Optional[str]: + return self._protocol + + @property + def compress(self) -> int: + return self._compress + + @property + def client_notakeover(self) -> bool: + return self._client_notakeover + + def get_extra_info(self, name: str, default: Any = None) -> Any: + """extra info from connection transport""" + conn = self._response.connection + if conn is None: + return default + transport = conn.transport + if transport is None: + return default + return transport.get_extra_info(name, default) + + def exception(self) -> Optional[BaseException]: + return self._exception + + async def ping(self, message: bytes = b"") -> None: + await self._writer.ping(message) + + async def pong(self, message: bytes = b"") -> None: + await self._writer.pong(message) + + async def send_str(self, data: str, compress: Optional[int] = None) -> None: + if not isinstance(data, str): + raise TypeError("data argument must be str (%r)" % type(data)) + await self._writer.send(data, binary=False, compress=compress) + + async def send_bytes(self, data: bytes, compress: Optional[int] = None) -> None: + if not isinstance(data, (bytes, bytearray, memoryview)): + raise TypeError("data argument must be byte-ish (%r)" % type(data)) + await self._writer.send(data, binary=True, compress=compress) + + async def send_json( + self, + data: Any, + compress: Optional[int] = None, + *, + dumps: JSONEncoder = DEFAULT_JSON_ENCODER, + ) -> None: + await self.send_str(dumps(data), compress=compress) + + async def close(self, *, code: int = WSCloseCode.OK, message: bytes = b"") -> bool: + # we need to break `receive()` cycle first, + # `close()` may be called from different task + if self._waiting is not None and not self._closed: + self._reader.feed_data(WS_CLOSING_MESSAGE, 0) + await self._waiting + + if not self._closed: + self._cancel_heartbeat() + self._closed = True + try: + await self._writer.close(code, message) + except asyncio.CancelledError: + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._response.close() + raise + except Exception as exc: + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._exception = exc + self._response.close() + return True + + if self._closing: + self._response.close() + return True + + while True: + try: + async with async_timeout.timeout(self._timeout.ws_close): + msg = await self._reader.read() + except asyncio.CancelledError: + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._response.close() + raise + except Exception as exc: + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._exception = exc + self._response.close() + return True + + if msg.type == WSMsgType.CLOSE: + self._close_code = msg.data + self._response.close() + return True + else: + return False + + async def receive(self, timeout: Optional[float] = None) -> WSMessage: + while True: + if self._waiting is not None: + raise RuntimeError("Concurrent call to receive() is not allowed") + + if self._closed: + return WS_CLOSED_MESSAGE + elif self._closing: + await self.close() + return WS_CLOSED_MESSAGE + + try: + self._waiting = self._loop.create_future() + try: + async with async_timeout.timeout( + timeout or self._timeout.ws_receive + ): + msg = await self._reader.read() + self._reset_heartbeat() + finally: + waiter = self._waiting + self._waiting = None + set_result(waiter, True) + except (asyncio.CancelledError, asyncio.TimeoutError): + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + raise + except EofStream: + self._close_code = WSCloseCode.OK + await self.close() + return WSMessage(WSMsgType.CLOSED, None, None) + except ClientError: + self._closed = True + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + return WS_CLOSED_MESSAGE + except WebSocketError as exc: + self._close_code = exc.code + await self.close(code=exc.code) + return WSMessage(WSMsgType.ERROR, exc, None) + except Exception as exc: + self._exception = exc + self._closing = True + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + await self.close() + return WSMessage(WSMsgType.ERROR, exc, None) + + if msg.type == WSMsgType.CLOSE: + self._closing = True + self._close_code = msg.data + # Could be closed elsewhere while awaiting reader + if not self._closed and self._autoclose: # type: ignore[redundant-expr] + await self.close() + elif msg.type == WSMsgType.CLOSING: + self._closing = True + elif msg.type == WSMsgType.PING and self._autoping: + await self.pong(msg.data) + continue + elif msg.type == WSMsgType.PONG and self._autoping: + continue + + return msg + + async def receive_str(self, *, timeout: Optional[float] = None) -> str: + msg = await self.receive(timeout) + if msg.type != WSMsgType.TEXT: + raise TypeError(f"Received message {msg.type}:{msg.data!r} is not str") + return cast(str, msg.data) + + async def receive_bytes(self, *, timeout: Optional[float] = None) -> bytes: + msg = await self.receive(timeout) + if msg.type != WSMsgType.BINARY: + raise TypeError(f"Received message {msg.type}:{msg.data!r} is not bytes") + return cast(bytes, msg.data) + + async def receive_json( + self, + *, + loads: JSONDecoder = DEFAULT_JSON_DECODER, + timeout: Optional[float] = None, + ) -> Any: + data = await self.receive_str(timeout=timeout) + return loads(data) + + def __aiter__(self) -> "ClientWebSocketResponse": + return self + + async def __anext__(self) -> WSMessage: + msg = await self.receive() + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED): + raise StopAsyncIteration + return msg diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py new file mode 100644 index 0000000..52791fe --- /dev/null +++ b/aiohttp/compression_utils.py @@ -0,0 +1,151 @@ +import asyncio +import zlib +from concurrent.futures import Executor +from typing import Optional, cast + +try: + try: + import brotlicffi as brotli + except ImportError: + import brotli + + HAS_BROTLI = True +except ImportError: # pragma: no cover + HAS_BROTLI = False + +MAX_SYNC_CHUNK_SIZE = 1024 + + +def encoding_to_mode( + encoding: Optional[str] = None, + suppress_deflate_header: bool = False, +) -> int: + if encoding == "gzip": + return 16 + zlib.MAX_WBITS + + return -zlib.MAX_WBITS if suppress_deflate_header else zlib.MAX_WBITS + + +class ZlibBaseHandler: + def __init__( + self, + mode: int, + executor: Optional[Executor] = None, + max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE, + ): + self._mode = mode + self._executor = executor + self._max_sync_chunk_size = max_sync_chunk_size + + +class ZLibCompressor(ZlibBaseHandler): + def __init__( + self, + encoding: Optional[str] = None, + suppress_deflate_header: bool = False, + level: Optional[int] = None, + wbits: Optional[int] = None, + strategy: int = zlib.Z_DEFAULT_STRATEGY, + executor: Optional[Executor] = None, + max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE, + ): + super().__init__( + mode=encoding_to_mode(encoding, suppress_deflate_header) + if wbits is None + else wbits, + executor=executor, + max_sync_chunk_size=max_sync_chunk_size, + ) + if level is None: + self._compressor = zlib.compressobj(wbits=self._mode, strategy=strategy) + else: + self._compressor = zlib.compressobj( + wbits=self._mode, strategy=strategy, level=level + ) + + def compress_sync(self, data: bytes) -> bytes: + return self._compressor.compress(data) + + async def compress(self, data: bytes) -> bytes: + if ( + self._max_sync_chunk_size is not None + and len(data) > self._max_sync_chunk_size + ): + return await asyncio.get_event_loop().run_in_executor( + self._executor, self.compress_sync, data + ) + return self.compress_sync(data) + + def flush(self, mode: int = zlib.Z_FINISH) -> bytes: + return self._compressor.flush(mode) + + +class ZLibDecompressor(ZlibBaseHandler): + def __init__( + self, + encoding: Optional[str] = None, + suppress_deflate_header: bool = False, + executor: Optional[Executor] = None, + max_sync_chunk_size: Optional[int] = MAX_SYNC_CHUNK_SIZE, + ): + super().__init__( + mode=encoding_to_mode(encoding, suppress_deflate_header), + executor=executor, + max_sync_chunk_size=max_sync_chunk_size, + ) + self._decompressor = zlib.decompressobj(wbits=self._mode) + + def decompress_sync(self, data: bytes, max_length: int = 0) -> bytes: + return self._decompressor.decompress(data, max_length) + + async def decompress(self, data: bytes, max_length: int = 0) -> bytes: + if ( + self._max_sync_chunk_size is not None + and len(data) > self._max_sync_chunk_size + ): + return await asyncio.get_event_loop().run_in_executor( + self._executor, self.decompress_sync, data, max_length + ) + return self.decompress_sync(data, max_length) + + def flush(self, length: int = 0) -> bytes: + return ( + self._decompressor.flush(length) + if length > 0 + else self._decompressor.flush() + ) + + @property + def eof(self) -> bool: + return self._decompressor.eof + + @property + def unconsumed_tail(self) -> bytes: + return self._decompressor.unconsumed_tail + + @property + def unused_data(self) -> bytes: + return self._decompressor.unused_data + + +class BrotliDecompressor: + # Supports both 'brotlipy' and 'Brotli' packages + # since they share an import name. The top branches + # are for 'brotlipy' and bottom branches for 'Brotli' + def __init__(self) -> None: + if not HAS_BROTLI: + raise RuntimeError( + "The brotli decompression is not available. " + "Please install `Brotli` module" + ) + self._obj = brotli.Decompressor() + + def decompress_sync(self, data: bytes) -> bytes: + if hasattr(self._obj, "decompress"): + return cast(bytes, self._obj.decompress(data)) + return cast(bytes, self._obj.process(data)) + + def flush(self) -> bytes: + if hasattr(self._obj, "flush"): + return cast(bytes, self._obj.flush()) + return b"" diff --git a/aiohttp/connector.py b/aiohttp/connector.py new file mode 100644 index 0000000..01a1ca8 --- /dev/null +++ b/aiohttp/connector.py @@ -0,0 +1,1386 @@ +import asyncio +import dataclasses +import functools +import logging +import random +import sys +import traceback +import warnings +from collections import defaultdict, deque +from contextlib import suppress +from http import HTTPStatus +from http.cookies import SimpleCookie +from itertools import cycle, islice +from time import monotonic +from types import TracebackType +from typing import ( # noqa + TYPE_CHECKING, + Any, + Awaitable, + Callable, + DefaultDict, + Dict, + Iterator, + List, + Literal, + Optional, + Set, + Tuple, + Type, + Union, + cast, +) + +from . import hdrs, helpers +from .abc import AbstractResolver +from .client_exceptions import ( + ClientConnectionError, + ClientConnectorCertificateError, + ClientConnectorError, + ClientConnectorSSLError, + ClientHttpProxyError, + ClientProxyConnectionError, + ServerFingerprintMismatch, + UnixClientConnectorError, + cert_errors, + ssl_errors, +) +from .client_proto import ResponseHandler +from .client_reqrep import SSL_ALLOWED_TYPES, ClientRequest, Fingerprint +from .helpers import _SENTINEL, ceil_timeout, is_ip_address, sentinel, set_result +from .locks import EventResultOrError +from .resolver import DefaultResolver + +try: + import ssl + + SSLContext = ssl.SSLContext +except ImportError: # pragma: no cover + ssl = None # type: ignore[assignment] + SSLContext = object # type: ignore[misc,assignment] + + +__all__ = ("BaseConnector", "TCPConnector", "UnixConnector", "NamedPipeConnector") + + +if TYPE_CHECKING: # pragma: no cover + from .client import ClientTimeout + from .client_reqrep import ConnectionKey + from .tracing import Trace + + +class Connection: + _source_traceback = None + _transport = None + + def __init__( + self, + connector: "BaseConnector", + key: "ConnectionKey", + protocol: ResponseHandler, + loop: asyncio.AbstractEventLoop, + ) -> None: + self._key = key + self._connector = connector + self._loop = loop + self._protocol: Optional[ResponseHandler] = protocol + self._callbacks: List[Callable[[], None]] = [] + + if loop.get_debug(): + self._source_traceback = traceback.extract_stack(sys._getframe(1)) + + def __repr__(self) -> str: + return f"Connection<{self._key}>" + + def __del__(self, _warnings: Any = warnings) -> None: + if self._protocol is not None: + _warnings.warn( + f"Unclosed connection {self!r}", ResourceWarning, source=self + ) + if self._loop.is_closed(): + return + + self._connector._release(self._key, self._protocol, should_close=True) + + context = {"client_connection": self, "message": "Unclosed connection"} + if self._source_traceback is not None: + context["source_traceback"] = self._source_traceback + self._loop.call_exception_handler(context) + + @property + def transport(self) -> Optional[asyncio.Transport]: + if self._protocol is None: + return None + return self._protocol.transport + + @property + def protocol(self) -> Optional[ResponseHandler]: + return self._protocol + + def add_callback(self, callback: Callable[[], None]) -> None: + if callback is not None: + self._callbacks.append(callback) + + def _notify_release(self) -> None: + callbacks, self._callbacks = self._callbacks[:], [] + + for cb in callbacks: + with suppress(Exception): + cb() + + def close(self) -> None: + self._notify_release() + + if self._protocol is not None: + self._connector._release(self._key, self._protocol, should_close=True) + self._protocol = None + + def release(self) -> None: + self._notify_release() + + if self._protocol is not None: + self._connector._release( + self._key, self._protocol, should_close=self._protocol.should_close + ) + self._protocol = None + + @property + def closed(self) -> bool: + return self._protocol is None or not self._protocol.is_connected() + + +class _TransportPlaceholder: + """placeholder for BaseConnector.connect function""" + + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + fut = loop.create_future() + fut.set_result(None) + self.closed: asyncio.Future[Optional[Exception]] = fut + + def close(self) -> None: + pass + + +class BaseConnector: + """Base connector class. + + keepalive_timeout - (optional) Keep-alive timeout. + force_close - Set to True to force close and do reconnect + after each request (and between redirects). + limit - The total number of simultaneous connections. + limit_per_host - Number of simultaneous connections to one host. + enable_cleanup_closed - Enables clean-up closed ssl transports. + Disabled by default. + timeout_ceil_threshold - Trigger ceiling of timeout values when + it's above timeout_ceil_threshold. + loop - Optional event loop. + """ + + _closed = True # prevent AttributeError in __del__ if ctor was failed + _source_traceback = None + + # abort transport after 2 seconds (cleanup broken connections) + _cleanup_closed_period = 2.0 + + def __init__( + self, + *, + keepalive_timeout: Union[_SENTINEL, None, float] = sentinel, + force_close: bool = False, + limit: int = 100, + limit_per_host: int = 0, + enable_cleanup_closed: bool = False, + timeout_ceil_threshold: float = 5, + ) -> None: + if force_close: + if keepalive_timeout is not None and keepalive_timeout is not sentinel: + raise ValueError( + "keepalive_timeout cannot " "be set if force_close is True" + ) + else: + if keepalive_timeout is sentinel: + keepalive_timeout = 15.0 + + self._timeout_ceil_threshold = timeout_ceil_threshold + + loop = asyncio.get_running_loop() + + self._closed = False + if loop.get_debug(): + self._source_traceback = traceback.extract_stack(sys._getframe(1)) + + self._conns: Dict[ConnectionKey, List[Tuple[ResponseHandler, float]]] = {} + self._limit = limit + self._limit_per_host = limit_per_host + self._acquired: Set[ResponseHandler] = set() + self._acquired_per_host: DefaultDict[ + ConnectionKey, Set[ResponseHandler] + ] = defaultdict(set) + self._keepalive_timeout = cast(float, keepalive_timeout) + self._force_close = force_close + + # {host_key: FIFO list of waiters} + self._waiters = defaultdict(deque) # type: ignore[var-annotated] + + self._loop = loop + self._factory = functools.partial(ResponseHandler, loop=loop) + + self.cookies: SimpleCookie[str] = SimpleCookie() + + # start keep-alive connection cleanup task + self._cleanup_handle: Optional[asyncio.TimerHandle] = None + + # start cleanup closed transports task + self._cleanup_closed_handle: Optional[asyncio.TimerHandle] = None + self._cleanup_closed_disabled = not enable_cleanup_closed + self._cleanup_closed_transports: List[Optional[asyncio.Transport]] = [] + self._cleanup_closed() + + def __del__(self, _warnings: Any = warnings) -> None: + if self._closed: + return + if not self._conns: + return + + conns = [repr(c) for c in self._conns.values()] + + self._close_immediately() + + _warnings.warn(f"Unclosed connector {self!r}", ResourceWarning, source=self) + context = { + "connector": self, + "connections": conns, + "message": "Unclosed connector", + } + if self._source_traceback is not None: + context["source_traceback"] = self._source_traceback + self._loop.call_exception_handler(context) + + async def __aenter__(self) -> "BaseConnector": + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_value: Optional[BaseException] = None, + exc_traceback: Optional[TracebackType] = None, + ) -> None: + await self.close() + + @property + def force_close(self) -> bool: + """Ultimately close connection on releasing if True.""" + return self._force_close + + @property + def limit(self) -> int: + """The total number for simultaneous connections. + + If limit is 0 the connector has no limit. + The default limit size is 100. + """ + return self._limit + + @property + def limit_per_host(self) -> int: + """The limit for simultaneous connections to the same endpoint. + + Endpoints are the same if they are have equal + (host, port, is_ssl) triple. + """ + return self._limit_per_host + + def _cleanup(self) -> None: + """Cleanup unused transports.""" + if self._cleanup_handle: + self._cleanup_handle.cancel() + # _cleanup_handle should be unset, otherwise _release() will not + # recreate it ever! + self._cleanup_handle = None + + now = self._loop.time() + timeout = self._keepalive_timeout + + if self._conns: + connections = {} + deadline = now - timeout + for key, conns in self._conns.items(): + alive = [] + for proto, use_time in conns: + if proto.is_connected(): + if use_time - deadline < 0: + transport = proto.transport + proto.close() + if key.is_ssl and not self._cleanup_closed_disabled: + self._cleanup_closed_transports.append(transport) + else: + alive.append((proto, use_time)) + else: + transport = proto.transport + proto.close() + if key.is_ssl and not self._cleanup_closed_disabled: + self._cleanup_closed_transports.append(transport) + + if alive: + connections[key] = alive + + self._conns = connections + + if self._conns: + self._cleanup_handle = helpers.weakref_handle( + self, + "_cleanup", + timeout, + self._loop, + timeout_ceil_threshold=self._timeout_ceil_threshold, + ) + + def _drop_acquired_per_host( + self, key: "ConnectionKey", val: ResponseHandler + ) -> None: + acquired_per_host = self._acquired_per_host + if key not in acquired_per_host: + return + conns = acquired_per_host[key] + conns.remove(val) + if not conns: + del self._acquired_per_host[key] + + def _cleanup_closed(self) -> None: + """Double confirmation for transport close. + + Some broken ssl servers may leave socket open without proper close. + """ + if self._cleanup_closed_handle: + self._cleanup_closed_handle.cancel() + + for transport in self._cleanup_closed_transports: + if transport is not None: + transport.abort() + + self._cleanup_closed_transports = [] + + if not self._cleanup_closed_disabled: + self._cleanup_closed_handle = helpers.weakref_handle( + self, + "_cleanup_closed", + self._cleanup_closed_period, + self._loop, + timeout_ceil_threshold=self._timeout_ceil_threshold, + ) + + async def close(self) -> None: + """Close all opened transports.""" + waiters = self._close_immediately() + if waiters: + results = await asyncio.gather(*waiters, return_exceptions=True) + for res in results: + if isinstance(res, Exception): + err_msg = "Error while closing connector: " + repr(res) + logging.error(err_msg) + + def _close_immediately(self) -> List["asyncio.Future[None]"]: + waiters: List["asyncio.Future[None]"] = [] + + if self._closed: + return waiters + + self._closed = True + + try: + if self._loop.is_closed(): + return waiters + + # cancel cleanup task + if self._cleanup_handle: + self._cleanup_handle.cancel() + + # cancel cleanup close task + if self._cleanup_closed_handle: + self._cleanup_closed_handle.cancel() + + for data in self._conns.values(): + for proto, t0 in data: + proto.close() + waiters.append(proto.closed) + + for proto in self._acquired: + proto.close() + waiters.append(proto.closed) + + # TODO (A.Yushovskiy, 24-May-2019) collect transp. closing futures + for transport in self._cleanup_closed_transports: + if transport is not None: + transport.abort() + + return waiters + + finally: + self._conns.clear() + self._acquired.clear() + self._waiters.clear() + self._cleanup_handle = None + self._cleanup_closed_transports.clear() + self._cleanup_closed_handle = None + + @property + def closed(self) -> bool: + """Is connector closed. + + A readonly property. + """ + return self._closed + + def _available_connections(self, key: "ConnectionKey") -> int: + """ + Return number of available connections. + + The limit, limit_per_host and the connection key are taken into account. + + If it returns less than 1 means that there are no connections + available. + """ + if self._limit: + # total calc available connections + available = self._limit - len(self._acquired) + + # check limit per host + if ( + self._limit_per_host + and available > 0 + and key in self._acquired_per_host + ): + acquired = self._acquired_per_host.get(key) + assert acquired is not None + available = self._limit_per_host - len(acquired) + + elif self._limit_per_host and key in self._acquired_per_host: + # check limit per host + acquired = self._acquired_per_host.get(key) + assert acquired is not None + available = self._limit_per_host - len(acquired) + else: + available = 1 + + return available + + async def connect( + self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" + ) -> Connection: + """Get from pool or create new connection.""" + key = req.connection_key + available = self._available_connections(key) + + # Wait if there are no available connections or if there are/were + # waiters (i.e. don't steal connection from a waiter about to wake up) + if available <= 0 or key in self._waiters: + fut = self._loop.create_future() + + # This connection will now count towards the limit. + self._waiters[key].append(fut) + + if traces: + for trace in traces: + await trace.send_connection_queued_start() + + try: + await fut + except BaseException as e: + if key in self._waiters: + # remove a waiter even if it was cancelled, normally it's + # removed when it's notified + try: + self._waiters[key].remove(fut) + except ValueError: # fut may no longer be in list + pass + + raise e + finally: + if key in self._waiters and not self._waiters[key]: + del self._waiters[key] + + if traces: + for trace in traces: + await trace.send_connection_queued_end() + + proto = self._get(key) + if proto is None: + placeholder = cast(ResponseHandler, _TransportPlaceholder(self._loop)) + self._acquired.add(placeholder) + self._acquired_per_host[key].add(placeholder) + + if traces: + for trace in traces: + await trace.send_connection_create_start() + + try: + proto = await self._create_connection(req, traces, timeout) + if self._closed: + proto.close() + raise ClientConnectionError("Connector is closed.") + except BaseException: + if not self._closed: + self._acquired.remove(placeholder) + self._drop_acquired_per_host(key, placeholder) + self._release_waiter() + raise + else: + if not self._closed: + self._acquired.remove(placeholder) + self._drop_acquired_per_host(key, placeholder) + + if traces: + for trace in traces: + await trace.send_connection_create_end() + else: + if traces: + # Acquire the connection to prevent race conditions with limits + placeholder = cast(ResponseHandler, _TransportPlaceholder(self._loop)) + self._acquired.add(placeholder) + self._acquired_per_host[key].add(placeholder) + for trace in traces: + await trace.send_connection_reuseconn() + self._acquired.remove(placeholder) + self._drop_acquired_per_host(key, placeholder) + + self._acquired.add(proto) + self._acquired_per_host[key].add(proto) + return Connection(self, key, proto, self._loop) + + def _get(self, key: "ConnectionKey") -> Optional[ResponseHandler]: + try: + conns = self._conns[key] + except KeyError: + return None + + t1 = self._loop.time() + while conns: + proto, t0 = conns.pop() + if proto.is_connected(): + if t1 - t0 > self._keepalive_timeout: + transport = proto.transport + proto.close() + # only for SSL transports + if key.is_ssl and not self._cleanup_closed_disabled: + self._cleanup_closed_transports.append(transport) + else: + if not conns: + # The very last connection was reclaimed: drop the key + del self._conns[key] + return proto + else: + transport = proto.transport + proto.close() + if key.is_ssl and not self._cleanup_closed_disabled: + self._cleanup_closed_transports.append(transport) + + # No more connections: drop the key + del self._conns[key] + return None + + def _release_waiter(self) -> None: + """ + Iterates over all waiters until one to be released is found. + + The one to be released is not finished and + belongs to a host that has available connections. + """ + if not self._waiters: + return + + # Having the dict keys ordered this avoids to iterate + # at the same order at each call. + queues = list(self._waiters.keys()) + random.shuffle(queues) + + for key in queues: + if self._available_connections(key) < 1: + continue + + waiters = self._waiters[key] + while waiters: + waiter = waiters.popleft() + if not waiter.done(): + waiter.set_result(None) + return + + def _release_acquired(self, key: "ConnectionKey", proto: ResponseHandler) -> None: + if self._closed: + # acquired connection is already released on connector closing + return + + try: + self._acquired.remove(proto) + self._drop_acquired_per_host(key, proto) + except KeyError: # pragma: no cover + # this may be result of undetermenistic order of objects + # finalization due garbage collection. + pass + else: + self._release_waiter() + + def _release( + self, + key: "ConnectionKey", + protocol: ResponseHandler, + *, + should_close: bool = False, + ) -> None: + if self._closed: + # acquired connection is already released on connector closing + return + + self._release_acquired(key, protocol) + + if self._force_close: + should_close = True + + if should_close or protocol.should_close: + transport = protocol.transport + protocol.close() + # TODO: Remove once fixed: https://bugs.python.org/issue39951 + # See PR #6321 + set_result(protocol.closed, None) + + if key.is_ssl and not self._cleanup_closed_disabled: + self._cleanup_closed_transports.append(transport) + else: + conns = self._conns.get(key) + if conns is None: + conns = self._conns[key] = [] + conns.append((protocol, self._loop.time())) + + if self._cleanup_handle is None: + self._cleanup_handle = helpers.weakref_handle( + self, + "_cleanup", + self._keepalive_timeout, + self._loop, + timeout_ceil_threshold=self._timeout_ceil_threshold, + ) + + async def _create_connection( + self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" + ) -> ResponseHandler: + raise NotImplementedError() + + +class _DNSCacheTable: + def __init__(self, ttl: Optional[float] = None) -> None: + self._addrs_rr: Dict[Tuple[str, int], Tuple[Iterator[Dict[str, Any]], int]] = {} + self._timestamps: Dict[Tuple[str, int], float] = {} + self._ttl = ttl + + def __contains__(self, host: object) -> bool: + return host in self._addrs_rr + + def add(self, key: Tuple[str, int], addrs: List[Dict[str, Any]]) -> None: + self._addrs_rr[key] = (cycle(addrs), len(addrs)) + + if self._ttl is not None: + self._timestamps[key] = monotonic() + + def remove(self, key: Tuple[str, int]) -> None: + self._addrs_rr.pop(key, None) + + if self._ttl is not None: + self._timestamps.pop(key, None) + + def clear(self) -> None: + self._addrs_rr.clear() + self._timestamps.clear() + + def next_addrs(self, key: Tuple[str, int]) -> List[Dict[str, Any]]: + loop, length = self._addrs_rr[key] + addrs = list(islice(loop, length)) + # Consume one more element to shift internal state of `cycle` + next(loop) + return addrs + + def expired(self, key: Tuple[str, int]) -> bool: + if self._ttl is None: + return False + + return self._timestamps[key] + self._ttl < monotonic() + + +class TCPConnector(BaseConnector): + """TCP connector. + + verify_ssl - Set to True to check ssl certifications. + fingerprint - Pass the binary sha256 + digest of the expected certificate in DER format to verify + that the certificate the server presents matches. See also + https://en.wikipedia.org/wiki/Transport_Layer_Security#Certificate_pinning + resolver - Enable DNS lookups and use this + resolver + use_dns_cache - Use memory cache for DNS lookups. + ttl_dns_cache - Max seconds having cached a DNS entry, None forever. + family - socket address family + local_addr - local tuple of (host, port) to bind socket to + + keepalive_timeout - (optional) Keep-alive timeout. + force_close - Set to True to force close and do reconnect + after each request (and between redirects). + limit - The total number of simultaneous connections. + limit_per_host - Number of simultaneous connections to one host. + enable_cleanup_closed - Enables clean-up closed ssl transports. + Disabled by default. + loop - Optional event loop. + """ + + def __init__( + self, + *, + use_dns_cache: bool = True, + ttl_dns_cache: Optional[int] = 10, + family: int = 0, + ssl: Union[None, Literal[False], Fingerprint, SSLContext] = None, + local_addr: Optional[Tuple[str, int]] = None, + resolver: Optional[AbstractResolver] = None, + keepalive_timeout: Union[None, float, _SENTINEL] = sentinel, + force_close: bool = False, + limit: int = 100, + limit_per_host: int = 0, + enable_cleanup_closed: bool = False, + timeout_ceil_threshold: float = 5, + ) -> None: + super().__init__( + keepalive_timeout=keepalive_timeout, + force_close=force_close, + limit=limit, + limit_per_host=limit_per_host, + enable_cleanup_closed=enable_cleanup_closed, + timeout_ceil_threshold=timeout_ceil_threshold, + ) + + if not isinstance(ssl, SSL_ALLOWED_TYPES): + raise TypeError( + "ssl should be SSLContext, bool, Fingerprint, " + "or None, got {!r} instead.".format(ssl) + ) + self._ssl = ssl + if resolver is None: + resolver = DefaultResolver() + self._resolver: AbstractResolver = resolver + + self._use_dns_cache = use_dns_cache + self._cached_hosts = _DNSCacheTable(ttl=ttl_dns_cache) + self._throttle_dns_events: Dict[Tuple[str, int], EventResultOrError] = {} + self._family = family + self._local_addr = local_addr + + def _close_immediately(self) -> List["asyncio.Future[None]"]: + for ev in self._throttle_dns_events.values(): + ev.cancel() + return super()._close_immediately() + + @property + def family(self) -> int: + """Socket family like AF_INET.""" + return self._family + + @property + def use_dns_cache(self) -> bool: + """True if local DNS caching is enabled.""" + return self._use_dns_cache + + def clear_dns_cache( + self, host: Optional[str] = None, port: Optional[int] = None + ) -> None: + """Remove specified host/port or clear all dns local cache.""" + if host is not None and port is not None: + self._cached_hosts.remove((host, port)) + elif host is not None or port is not None: + raise ValueError("either both host and port " "or none of them are allowed") + else: + self._cached_hosts.clear() + + async def _resolve_host( + self, host: str, port: int, traces: Optional[List["Trace"]] = None + ) -> List[Dict[str, Any]]: + if is_ip_address(host): + return [ + { + "hostname": host, + "host": host, + "port": port, + "family": self._family, + "proto": 0, + "flags": 0, + } + ] + + if not self._use_dns_cache: + if traces: + for trace in traces: + await trace.send_dns_resolvehost_start(host) + + res = await self._resolver.resolve(host, port, family=self._family) + + if traces: + for trace in traces: + await trace.send_dns_resolvehost_end(host) + + return res + + key = (host, port) + + if (key in self._cached_hosts) and (not self._cached_hosts.expired(key)): + # get result early, before any await (#4014) + result = self._cached_hosts.next_addrs(key) + + if traces: + for trace in traces: + await trace.send_dns_cache_hit(host) + return result + + if key in self._throttle_dns_events: + # get event early, before any await (#4014) + event = self._throttle_dns_events[key] + if traces: + for trace in traces: + await trace.send_dns_cache_hit(host) + await event.wait() + else: + # update dict early, before any await (#4014) + self._throttle_dns_events[key] = EventResultOrError(self._loop) + if traces: + for trace in traces: + await trace.send_dns_cache_miss(host) + try: + if traces: + for trace in traces: + await trace.send_dns_resolvehost_start(host) + + addrs = await self._resolver.resolve(host, port, family=self._family) + if traces: + for trace in traces: + await trace.send_dns_resolvehost_end(host) + + self._cached_hosts.add(key, addrs) + self._throttle_dns_events[key].set() + except BaseException as e: + # any DNS exception, independently of the implementation + # is set for the waiters to raise the same exception. + self._throttle_dns_events[key].set(exc=e) + raise + finally: + self._throttle_dns_events.pop(key) + + return self._cached_hosts.next_addrs(key) + + async def _create_connection( + self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" + ) -> ResponseHandler: + """Create connection. + + Has same keyword arguments as BaseEventLoop.create_connection. + """ + if req.proxy: + _, proto = await self._create_proxy_connection(req, traces, timeout) + else: + _, proto = await self._create_direct_connection(req, traces, timeout) + + return proto + + @staticmethod + @functools.lru_cache(None) + def _make_ssl_context(verified: bool) -> SSLContext: + if verified: + return ssl.create_default_context() + else: + sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + sslcontext.options |= ssl.OP_NO_SSLv2 + sslcontext.options |= ssl.OP_NO_SSLv3 + sslcontext.check_hostname = False + sslcontext.verify_mode = ssl.CERT_NONE + try: + sslcontext.options |= ssl.OP_NO_COMPRESSION + except AttributeError as attr_err: + warnings.warn( + "{!s}: The Python interpreter is compiled " + "against OpenSSL < 1.0.0. Ref: " + "https://docs.python.org/3/library/ssl.html" + "#ssl.OP_NO_COMPRESSION".format(attr_err), + ) + sslcontext.set_default_verify_paths() + return sslcontext + + def _get_ssl_context(self, req: ClientRequest) -> Optional[SSLContext]: + """Logic to get the correct SSL context + + 0. if req.ssl is false, return None + + 1. if ssl_context is specified in req, use it + 2. if _ssl_context is specified in self, use it + 3. otherwise: + 1. if verify_ssl is not specified in req, use self.ssl_context + (will generate a default context according to self.verify_ssl) + 2. if verify_ssl is True in req, generate a default SSL context + 3. if verify_ssl is False in req, generate a SSL context that + won't verify + """ + if req.is_ssl(): + if ssl is None: # pragma: no cover + raise RuntimeError("SSL is not supported.") + sslcontext = req.ssl + if isinstance(sslcontext, ssl.SSLContext): + return sslcontext + if sslcontext is not None: + # not verified or fingerprinted + return self._make_ssl_context(False) + sslcontext = self._ssl + if isinstance(sslcontext, ssl.SSLContext): + return sslcontext + if sslcontext is not None: + # not verified or fingerprinted + return self._make_ssl_context(False) + return self._make_ssl_context(True) + else: + return None + + def _get_fingerprint(self, req: ClientRequest) -> Optional["Fingerprint"]: + ret = req.ssl + if isinstance(ret, Fingerprint): + return ret + ret = self._ssl + if isinstance(ret, Fingerprint): + return ret + return None + + async def _wrap_create_connection( + self, + *args: Any, + req: ClientRequest, + timeout: "ClientTimeout", + client_error: Type[Exception] = ClientConnectorError, + **kwargs: Any, + ) -> Tuple[asyncio.Transport, ResponseHandler]: + try: + async with ceil_timeout( + timeout.sock_connect, ceil_threshold=timeout.ceil_threshold + ): + return await self._loop.create_connection(*args, **kwargs) + except cert_errors as exc: + raise ClientConnectorCertificateError(req.connection_key, exc) from exc + except ssl_errors as exc: + raise ClientConnectorSSLError(req.connection_key, exc) from exc + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + raise client_error(req.connection_key, exc) from exc + + def _warn_about_tls_in_tls( + self, + underlying_transport: asyncio.Transport, + req: ClientRequest, + ) -> None: + """Issue a warning if the requested URL has HTTPS scheme.""" + if req.request_info.url.scheme != "https": + return + + asyncio_supports_tls_in_tls = getattr( + underlying_transport, + "_start_tls_compatible", + False, + ) + + if asyncio_supports_tls_in_tls: + return + + warnings.warn( + "An HTTPS request is being sent through an HTTPS proxy. " + "This support for TLS in TLS is known to be disabled " + "in the stdlib asyncio. This is why you'll probably see " + "an error in the log below.\n\n" + "It is possible to enable it via monkeypatching. " + "For more details, see:\n" + "* https://bugs.python.org/issue37179\n" + "* https://github.com/python/cpython/pull/28073\n\n" + "You can temporarily patch this as follows:\n" + "* https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support\n" + "* https://github.com/aio-libs/aiohttp/discussions/6044\n", + RuntimeWarning, + source=self, + # Why `4`? At least 3 of the calls in the stack originate + # from the methods in this class. + stacklevel=3, + ) + + async def _start_tls_connection( + self, + underlying_transport: asyncio.Transport, + req: ClientRequest, + timeout: "ClientTimeout", + client_error: Type[Exception] = ClientConnectorError, + ) -> Tuple[asyncio.BaseTransport, ResponseHandler]: + """Wrap the raw TCP transport with TLS.""" + tls_proto = self._factory() # Create a brand new proto for TLS + + # Safety of the `cast()` call here is based on the fact that + # internally `_get_ssl_context()` only returns `None` when + # `req.is_ssl()` evaluates to `False` which is never gonna happen + # in this code path. Of course, it's rather fragile + # maintainability-wise but this is to be solved separately. + sslcontext = cast(ssl.SSLContext, self._get_ssl_context(req)) + + try: + async with ceil_timeout( + timeout.sock_connect, ceil_threshold=timeout.ceil_threshold + ): + try: + tls_transport = await self._loop.start_tls( + underlying_transport, + tls_proto, + sslcontext, + server_hostname=req.server_hostname or req.host, + ssl_handshake_timeout=timeout.total, + ) + except BaseException: + # We need to close the underlying transport since + # `start_tls()` probably failed before it had a + # chance to do this: + underlying_transport.close() + raise + except cert_errors as exc: + raise ClientConnectorCertificateError(req.connection_key, exc) from exc + except ssl_errors as exc: + raise ClientConnectorSSLError(req.connection_key, exc) from exc + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + raise client_error(req.connection_key, exc) from exc + except TypeError as type_err: + # Example cause looks like this: + # TypeError: transport is not supported by start_tls() + + raise ClientConnectionError( + "Cannot initialize a TLS-in-TLS connection to host " + f"{req.host!s}:{req.port:d} through an underlying connection " + f"to an HTTPS proxy {req.proxy!s} ssl:{req.ssl or 'default'} " + f"[{type_err!s}]" + ) from type_err + else: + if tls_transport is None: + msg = "Failed to start TLS (possibly caused by closing transport)" + raise client_error(req.connection_key, OSError(msg)) + tls_proto.connection_made( + tls_transport + ) # Kick the state machine of the new TLS protocol + + return tls_transport, tls_proto + + async def _create_direct_connection( + self, + req: ClientRequest, + traces: List["Trace"], + timeout: "ClientTimeout", + *, + client_error: Type[Exception] = ClientConnectorError, + ) -> Tuple[asyncio.Transport, ResponseHandler]: + sslcontext = self._get_ssl_context(req) + fingerprint = self._get_fingerprint(req) + + host = req.url.raw_host + assert host is not None + # Replace multiple trailing dots with a single one. + # A trailing dot is only present for fully-qualified domain names. + # See https://github.com/aio-libs/aiohttp/pull/7364. + if host.endswith(".."): + host = host.rstrip(".") + "." + port = req.port + assert port is not None + host_resolved = asyncio.ensure_future( + self._resolve_host(host, port, traces=traces), loop=self._loop + ) + try: + # Cancelling this lookup should not cancel the underlying lookup + # or else the cancel event will get broadcast to all the waiters + # across all connections. + hosts = await asyncio.shield(host_resolved) + except asyncio.CancelledError: + + def drop_exception(fut: "asyncio.Future[List[Dict[str, Any]]]") -> None: + with suppress(Exception, asyncio.CancelledError): + fut.result() + + host_resolved.add_done_callback(drop_exception) + raise + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + # in case of proxy it is not ClientProxyConnectionError + # it is problem of resolving proxy ip itself + raise ClientConnectorError(req.connection_key, exc) from exc + + last_exc: Optional[Exception] = None + + for hinfo in hosts: + host = hinfo["host"] + port = hinfo["port"] + + # Strip trailing dots, certificates contain FQDN without dots. + # See https://github.com/aio-libs/aiohttp/issues/3636 + server_hostname = ( + (req.server_hostname or hinfo["hostname"]).rstrip(".") + if sslcontext + else None + ) + + try: + transp, proto = await self._wrap_create_connection( + self._factory, + host, + port, + timeout=timeout, + ssl=sslcontext, + family=hinfo["family"], + proto=hinfo["proto"], + flags=hinfo["flags"], + server_hostname=server_hostname, + local_addr=self._local_addr, + req=req, + client_error=client_error, + ) + except ClientConnectorError as exc: + last_exc = exc + continue + + if req.is_ssl() and fingerprint: + try: + fingerprint.check(transp) + except ServerFingerprintMismatch as exc: + transp.close() + if not self._cleanup_closed_disabled: + self._cleanup_closed_transports.append(transp) + last_exc = exc + continue + + return transp, proto + assert last_exc is not None + raise last_exc + + async def _create_proxy_connection( + self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" + ) -> Tuple[asyncio.BaseTransport, ResponseHandler]: + headers: Dict[str, str] = {} + if req.proxy_headers is not None: + headers = req.proxy_headers # type: ignore[assignment] + headers[hdrs.HOST] = req.headers[hdrs.HOST] + + url = req.proxy + assert url is not None + proxy_req = ClientRequest( + hdrs.METH_GET, + url, + headers=headers, + auth=req.proxy_auth, + loop=self._loop, + ssl=req.ssl, + ) + + # create connection to proxy server + transport, proto = await self._create_direct_connection( + proxy_req, [], timeout, client_error=ClientProxyConnectionError + ) + + # Many HTTP proxies has buggy keepalive support. Let's not + # reuse connection but close it after processing every + # response. + proto.force_close() + + auth = proxy_req.headers.pop(hdrs.AUTHORIZATION, None) + if auth is not None: + if not req.is_ssl(): + req.headers[hdrs.PROXY_AUTHORIZATION] = auth + else: + proxy_req.headers[hdrs.PROXY_AUTHORIZATION] = auth + + if req.is_ssl(): + self._warn_about_tls_in_tls(transport, req) + + # For HTTPS requests over HTTP proxy + # we must notify proxy to tunnel connection + # so we send CONNECT command: + # CONNECT www.python.org:443 HTTP/1.1 + # Host: www.python.org + # + # next we must do TLS handshake and so on + # to do this we must wrap raw socket into secure one + # asyncio handles this perfectly + proxy_req.method = hdrs.METH_CONNECT + proxy_req.url = req.url + key = dataclasses.replace( + req.connection_key, proxy=None, proxy_auth=None, proxy_headers_hash=None + ) + conn = Connection(self, key, proto, self._loop) + proxy_resp = await proxy_req.send(conn) + try: + protocol = conn._protocol + assert protocol is not None + + # read_until_eof=True will ensure the connection isn't closed + # once the response is received and processed allowing + # START_TLS to work on the connection below. + protocol.set_response_params( + read_until_eof=True, + timeout_ceil_threshold=self._timeout_ceil_threshold, + ) + resp = await proxy_resp.start(conn) + except BaseException: + proxy_resp.close() + conn.close() + raise + else: + conn._protocol = None + conn._transport = None + try: + if resp.status != 200: + message = resp.reason + if message is None: + message = HTTPStatus(resp.status).phrase + raise ClientHttpProxyError( + proxy_resp.request_info, + resp.history, + status=resp.status, + message=message, + headers=resp.headers, + ) + except BaseException: + # It shouldn't be closed in `finally` because it's fed to + # `loop.start_tls()` and the docs say not to touch it after + # passing there. + transport.close() + raise + + return await self._start_tls_connection( + # Access the old transport for the last time before it's + # closed and forgotten forever: + transport, + req=req, + timeout=timeout, + ) + finally: + proxy_resp.close() + + return transport, proto + + +class UnixConnector(BaseConnector): + """Unix socket connector. + + path - Unix socket path. + keepalive_timeout - (optional) Keep-alive timeout. + force_close - Set to True to force close and do reconnect + after each request (and between redirects). + limit - The total number of simultaneous connections. + limit_per_host - Number of simultaneous connections to one host. + loop - Optional event loop. + """ + + def __init__( + self, + path: str, + force_close: bool = False, + keepalive_timeout: Union[_SENTINEL, float, None] = sentinel, + limit: int = 100, + limit_per_host: int = 0, + ) -> None: + super().__init__( + force_close=force_close, + keepalive_timeout=keepalive_timeout, + limit=limit, + limit_per_host=limit_per_host, + ) + self._path = path + + @property + def path(self) -> str: + """Path to unix socket.""" + return self._path + + async def _create_connection( + self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" + ) -> ResponseHandler: + try: + async with ceil_timeout( + timeout.sock_connect, ceil_threshold=timeout.ceil_threshold + ): + _, proto = await self._loop.create_unix_connection( + self._factory, self._path + ) + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + raise UnixClientConnectorError(self.path, req.connection_key, exc) from exc + + return proto + + +class NamedPipeConnector(BaseConnector): + """Named pipe connector. + + Only supported by the proactor event loop. + See also: https://docs.python.org/3/library/asyncio-eventloop.html + + path - Windows named pipe path. + keepalive_timeout - (optional) Keep-alive timeout. + force_close - Set to True to force close and do reconnect + after each request (and between redirects). + limit - The total number of simultaneous connections. + limit_per_host - Number of simultaneous connections to one host. + loop - Optional event loop. + """ + + def __init__( + self, + path: str, + force_close: bool = False, + keepalive_timeout: Union[_SENTINEL, float, None] = sentinel, + limit: int = 100, + limit_per_host: int = 0, + ) -> None: + super().__init__( + force_close=force_close, + keepalive_timeout=keepalive_timeout, + limit=limit, + limit_per_host=limit_per_host, + ) + if not isinstance( + self._loop, asyncio.ProactorEventLoop # type: ignore[attr-defined] + ): + raise RuntimeError( + "Named Pipes only available in proactor " "loop under windows" + ) + self._path = path + + @property + def path(self) -> str: + """Path to the named pipe.""" + return self._path + + async def _create_connection( + self, req: ClientRequest, traces: List["Trace"], timeout: "ClientTimeout" + ) -> ResponseHandler: + try: + async with ceil_timeout( + timeout.sock_connect, ceil_threshold=timeout.ceil_threshold + ): + _, proto = await self._loop.create_pipe_connection( # type: ignore[attr-defined] + self._factory, self._path + ) + # the drain is required so that the connection_made is called + # and transport is set otherwise it is not set before the + # `assert conn.transport is not None` + # in client.py's _request method + await asyncio.sleep(0) + # other option is to manually set transport like + # `proto.transport = trans` + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + raise ClientConnectorError(req.connection_key, exc) from exc + + return cast(ResponseHandler, proto) diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py new file mode 100644 index 0000000..a35c15f --- /dev/null +++ b/aiohttp/cookiejar.py @@ -0,0 +1,417 @@ +import contextlib +import datetime +import os # noqa +import pathlib +import pickle +import re +import warnings +from collections import defaultdict +from http.cookies import BaseCookie, Morsel, SimpleCookie +from typing import ( # noqa + DefaultDict, + Dict, + Iterable, + Iterator, + List, + Mapping, + Optional, + Set, + Tuple, + Union, + cast, +) + +from yarl import URL + +from .abc import AbstractCookieJar, ClearCookiePredicate +from .helpers import is_ip_address, next_whole_second +from .typedefs import LooseCookies, PathLike, StrOrURL + +__all__ = ("CookieJar", "DummyCookieJar") + + +CookieItem = Union[str, "Morsel[str]"] + + +class CookieJar(AbstractCookieJar): + """Implements cookie storage adhering to RFC 6265.""" + + DATE_TOKENS_RE = re.compile( + r"[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]*" + r"(?P[\x00-\x08\x0A-\x1F\d:a-zA-Z\x7F-\xFF]+)" + ) + + DATE_HMS_TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})") + + DATE_DAY_OF_MONTH_RE = re.compile(r"(\d{1,2})") + + DATE_MONTH_RE = re.compile( + "(jan)|(feb)|(mar)|(apr)|(may)|(jun)|(jul)|" "(aug)|(sep)|(oct)|(nov)|(dec)", + re.I, + ) + + DATE_YEAR_RE = re.compile(r"(\d{2,4})") + + MAX_TIME = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc) + + MAX_32BIT_TIME = datetime.datetime.fromtimestamp(2**31 - 1, datetime.timezone.utc) + + def __init__( + self, + *, + unsafe: bool = False, + quote_cookie: bool = True, + treat_as_secure_origin: Union[StrOrURL, List[StrOrURL], None] = None + ) -> None: + self._cookies: DefaultDict[Tuple[str, str], SimpleCookie[str]] = defaultdict( + SimpleCookie + ) + self._host_only_cookies: Set[Tuple[str, str]] = set() + self._unsafe = unsafe + self._quote_cookie = quote_cookie + if treat_as_secure_origin is None: + treat_as_secure_origin = [] + elif isinstance(treat_as_secure_origin, URL): + treat_as_secure_origin = [treat_as_secure_origin.origin()] + elif isinstance(treat_as_secure_origin, str): + treat_as_secure_origin = [URL(treat_as_secure_origin).origin()] + else: + treat_as_secure_origin = [ + URL(url).origin() if isinstance(url, str) else url.origin() + for url in treat_as_secure_origin + ] + self._treat_as_secure_origin = treat_as_secure_origin + self._next_expiration = next_whole_second() + self._expirations: Dict[Tuple[str, str, str], datetime.datetime] = {} + # #4515: datetime.max may not be representable on 32-bit platforms + self._max_time = self.MAX_TIME + try: + self._max_time.timestamp() + except OverflowError: + self._max_time = self.MAX_32BIT_TIME + + def save(self, file_path: PathLike) -> None: + file_path = pathlib.Path(file_path) + with file_path.open(mode="wb") as f: + pickle.dump(self._cookies, f, pickle.HIGHEST_PROTOCOL) + + def load(self, file_path: PathLike) -> None: + file_path = pathlib.Path(file_path) + with file_path.open(mode="rb") as f: + self._cookies = pickle.load(f) + + def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None: + if predicate is None: + self._next_expiration = next_whole_second() + self._cookies.clear() + self._host_only_cookies.clear() + self._expirations.clear() + return + + to_del = [] + now = datetime.datetime.now(datetime.timezone.utc) + for (domain, path), cookie in self._cookies.items(): + for name, morsel in cookie.items(): + key = (domain, path, name) + if ( + key in self._expirations and self._expirations[key] <= now + ) or predicate(morsel): + to_del.append(key) + + for domain, path, name in to_del: + self._host_only_cookies.discard((domain, name)) + key = (domain, path, name) + if key in self._expirations: + del self._expirations[(domain, path, name)] + self._cookies[(domain, path)].pop(name, None) + + next_expiration = min(self._expirations.values(), default=self._max_time) + try: + self._next_expiration = next_expiration.replace( + microsecond=0 + ) + datetime.timedelta(seconds=1) + except OverflowError: + self._next_expiration = self._max_time + + def clear_domain(self, domain: str) -> None: + self.clear(lambda x: self._is_domain_match(domain, x["domain"])) + + def __iter__(self) -> "Iterator[Morsel[str]]": + self._do_expiration() + for val in self._cookies.values(): + yield from val.values() + + def __len__(self) -> int: + return sum(1 for i in self) + + def _do_expiration(self) -> None: + self.clear(lambda x: False) + + def _expire_cookie( + self, when: datetime.datetime, domain: str, path: str, name: str + ) -> None: + self._next_expiration = min(self._next_expiration, when) + self._expirations[(domain, path, name)] = when + + def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None: + """Update cookies.""" + hostname = response_url.raw_host + + if not self._unsafe and is_ip_address(hostname): + # Don't accept cookies from IPs + return + + if isinstance(cookies, Mapping): + cookies = cookies.items() + + for name, cookie in cookies: + if not isinstance(cookie, Morsel): + tmp: SimpleCookie[str] = SimpleCookie() + tmp[name] = cookie # type: ignore[assignment] + cookie = tmp[name] + + domain = cookie["domain"] + + # ignore domains with trailing dots + if domain.endswith("."): + domain = "" + del cookie["domain"] + + if not domain and hostname is not None: + # Set the cookie's domain to the response hostname + # and set its host-only-flag + self._host_only_cookies.add((hostname, name)) + domain = cookie["domain"] = hostname + + if domain.startswith("."): + # Remove leading dot + domain = domain[1:] + cookie["domain"] = domain + + if hostname and not self._is_domain_match(domain, hostname): + # Setting cookies for different domains is not allowed + continue + + path = cookie["path"] + if not path or not path.startswith("/"): + # Set the cookie's path to the response path + path = response_url.path + if not path.startswith("/"): + path = "/" + else: + # Cut everything from the last slash to the end + path = "/" + path[1 : path.rfind("/")] + cookie["path"] = path + + max_age = cookie["max-age"] + if max_age: + try: + delta_seconds = int(max_age) + try: + max_age_expiration = datetime.datetime.now( + datetime.timezone.utc + ) + datetime.timedelta(seconds=delta_seconds) + except OverflowError: + max_age_expiration = self._max_time + self._expire_cookie(max_age_expiration, domain, path, name) + except ValueError: + cookie["max-age"] = "" + + else: + expires = cookie["expires"] + if expires: + expire_time = self._parse_date(expires) + if expire_time: + self._expire_cookie(expire_time, domain, path, name) + else: + cookie["expires"] = "" + + self._cookies[(domain, path)][name] = cookie + + self._do_expiration() + + def filter_cookies( + self, request_url: URL = URL() + ) -> Union["BaseCookie[str]", "SimpleCookie[str]"]: + """Returns this jar's cookies filtered by their attributes.""" + self._do_expiration() + if not isinstance(request_url, URL): + warnings.warn( + "The method accepts yarl.URL instances only, got {}".format( + type(request_url) + ), + DeprecationWarning, + ) + request_url = URL(request_url) + filtered: Union["SimpleCookie[str]", "BaseCookie[str]"] = ( + SimpleCookie() if self._quote_cookie else BaseCookie() + ) + hostname = request_url.raw_host or "" + request_origin = URL() + with contextlib.suppress(ValueError): + request_origin = request_url.origin() + + is_not_secure = ( + request_url.scheme not in ("https", "wss") + and request_origin not in self._treat_as_secure_origin + ) + + # Point 2: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.4 + for cookie in sorted(self, key=lambda c: len(c["path"])): + name = cookie.key + domain = cookie["domain"] + + # Send shared cookies + if not domain: + filtered[name] = cookie.value + continue + + if not self._unsafe and is_ip_address(hostname): + continue + + if (domain, name) in self._host_only_cookies: + if domain != hostname: + continue + elif not self._is_domain_match(domain, hostname): + continue + + if not self._is_path_match(request_url.path, cookie["path"]): + continue + + if is_not_secure and cookie["secure"]: + continue + + # It's critical we use the Morsel so the coded_value + # (based on cookie version) is preserved + mrsl_val = cast("Morsel[str]", cookie.get(cookie.key, Morsel())) + mrsl_val.set(cookie.key, cookie.value, cookie.coded_value) + filtered[name] = mrsl_val + + return filtered + + @staticmethod + def _is_domain_match(domain: str, hostname: str) -> bool: + """Implements domain matching adhering to RFC 6265.""" + if hostname == domain: + return True + + if not hostname.endswith(domain): + return False + + non_matching = hostname[: -len(domain)] + + if not non_matching.endswith("."): + return False + + return not is_ip_address(hostname) + + @staticmethod + def _is_path_match(req_path: str, cookie_path: str) -> bool: + """Implements path matching adhering to RFC 6265.""" + if not req_path.startswith("/"): + req_path = "/" + + if req_path == cookie_path: + return True + + if not req_path.startswith(cookie_path): + return False + + if cookie_path.endswith("/"): + return True + + non_matching = req_path[len(cookie_path) :] + + return non_matching.startswith("/") + + @classmethod + def _parse_date(cls, date_str: str) -> Optional[datetime.datetime]: + """Implements date string parsing adhering to RFC 6265.""" + if not date_str: + return None + + found_time = False + found_day = False + found_month = False + found_year = False + + hour = minute = second = 0 + day = 0 + month = 0 + year = 0 + + for token_match in cls.DATE_TOKENS_RE.finditer(date_str): + token = token_match.group("token") + + if not found_time: + time_match = cls.DATE_HMS_TIME_RE.match(token) + if time_match: + found_time = True + hour, minute, second = (int(s) for s in time_match.groups()) + continue + + if not found_day: + day_match = cls.DATE_DAY_OF_MONTH_RE.match(token) + if day_match: + found_day = True + day = int(day_match.group()) + continue + + if not found_month: + month_match = cls.DATE_MONTH_RE.match(token) + if month_match: + found_month = True + assert month_match.lastindex is not None + month = month_match.lastindex + continue + + if not found_year: + year_match = cls.DATE_YEAR_RE.match(token) + if year_match: + found_year = True + year = int(year_match.group()) + + if 70 <= year <= 99: + year += 1900 + elif 0 <= year <= 69: + year += 2000 + + if False in (found_day, found_month, found_year, found_time): + return None + + if not 1 <= day <= 31: + return None + + if year < 1601 or hour > 23 or minute > 59 or second > 59: + return None + + return datetime.datetime( + year, month, day, hour, minute, second, tzinfo=datetime.timezone.utc + ) + + +class DummyCookieJar(AbstractCookieJar): + """Implements a dummy cookie storage. + + It can be used with the ClientSession when no cookie processing is needed. + + """ + + def __iter__(self) -> "Iterator[Morsel[str]]": + while False: + yield None + + def __len__(self) -> int: + return 0 + + def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None: + pass + + def clear_domain(self, domain: str) -> None: + pass + + def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None: + pass + + def filter_cookies(self, request_url: URL) -> "BaseCookie[str]": + return SimpleCookie() diff --git a/aiohttp/formdata.py b/aiohttp/formdata.py new file mode 100644 index 0000000..e159fb6 --- /dev/null +++ b/aiohttp/formdata.py @@ -0,0 +1,173 @@ +import io +from typing import Any, Iterable, List, Optional +from urllib.parse import urlencode + +from multidict import MultiDict, MultiDictProxy + +from . import hdrs, multipart, payload +from .helpers import guess_filename +from .payload import Payload + +__all__ = ("FormData",) + + +class FormData: + """Helper class for form body generation. + + Supports multipart/form-data and application/x-www-form-urlencoded. + """ + + def __init__( + self, + fields: Iterable[Any] = (), + quote_fields: bool = True, + charset: Optional[str] = None, + boundary: Optional[str] = None, + ) -> None: + self._boundary = boundary + self._writer = multipart.MultipartWriter("form-data", boundary=self._boundary) + self._fields: List[Any] = [] + self._is_multipart = False + self._is_processed = False + self._quote_fields = quote_fields + self._charset = charset + + if isinstance(fields, dict): + fields = list(fields.items()) + elif not isinstance(fields, (list, tuple)): + fields = (fields,) + self.add_fields(*fields) + + @property + def is_multipart(self) -> bool: + return self._is_multipart + + def add_field( + self, + name: str, + value: Any, + *, + content_type: Optional[str] = None, + filename: Optional[str] = None, + content_transfer_encoding: Optional[str] = None, + ) -> None: + if isinstance(value, io.IOBase): + self._is_multipart = True + elif isinstance(value, (bytes, bytearray, memoryview)): + if filename is None and content_transfer_encoding is None: + filename = name + + type_options: MultiDict[str] = MultiDict({"name": name}) + if filename is not None and not isinstance(filename, str): + raise TypeError( + "filename must be an instance of str. " "Got: %s" % filename + ) + if filename is None and isinstance(value, io.IOBase): + filename = guess_filename(value, name) + if filename is not None: + type_options["filename"] = filename + self._is_multipart = True + + headers = {} + if content_type is not None: + if not isinstance(content_type, str): + raise TypeError( + "content_type must be an instance of str. " "Got: %s" % content_type + ) + headers[hdrs.CONTENT_TYPE] = content_type + self._is_multipart = True + if content_transfer_encoding is not None: + if not isinstance(content_transfer_encoding, str): + raise TypeError( + "content_transfer_encoding must be an instance" + " of str. Got: %s" % content_transfer_encoding + ) + headers[hdrs.CONTENT_TRANSFER_ENCODING] = content_transfer_encoding + self._is_multipart = True + + self._fields.append((type_options, headers, value)) + + def add_fields(self, *fields: Any) -> None: + to_add = list(fields) + + while to_add: + rec = to_add.pop(0) + + if isinstance(rec, io.IOBase): + k = guess_filename(rec, "unknown") + self.add_field(k, rec) # type: ignore[arg-type] + + elif isinstance(rec, (MultiDictProxy, MultiDict)): + to_add.extend(rec.items()) + + elif isinstance(rec, (list, tuple)) and len(rec) == 2: + k, fp = rec + self.add_field(k, fp) # type: ignore[arg-type] + + else: + raise TypeError( + "Only io.IOBase, multidict and (name, file) " + "pairs allowed, use .add_field() for passing " + "more complex parameters, got {!r}".format(rec) + ) + + def _gen_form_urlencoded(self) -> payload.BytesPayload: + # form data (x-www-form-urlencoded) + data = [] + for type_options, _, value in self._fields: + data.append((type_options["name"], value)) + + charset = self._charset if self._charset is not None else "utf-8" + + if charset == "utf-8": + content_type = "application/x-www-form-urlencoded" + else: + content_type = "application/x-www-form-urlencoded; " "charset=%s" % charset + + return payload.BytesPayload( + urlencode(data, doseq=True, encoding=charset).encode(), + content_type=content_type, + ) + + def _gen_form_data(self) -> multipart.MultipartWriter: + """Encode a list of fields using the multipart/form-data MIME format""" + if self._is_processed: + raise RuntimeError("Form data has been processed already") + for dispparams, headers, value in self._fields: + try: + if hdrs.CONTENT_TYPE in headers: + part = payload.get_payload( + value, + content_type=headers[hdrs.CONTENT_TYPE], + headers=headers, + encoding=self._charset, + ) + else: + part = payload.get_payload( + value, headers=headers, encoding=self._charset + ) + except Exception as exc: + raise TypeError( + "Can not serialize value type: %r\n " + "headers: %r\n value: %r" % (type(value), headers, value) + ) from exc + + if dispparams: + part.set_content_disposition( + "form-data", quote_fields=self._quote_fields, **dispparams + ) + # FIXME cgi.FieldStorage doesn't likes body parts with + # Content-Length which were sent via chunked transfer encoding + assert part.headers is not None + part.headers.popall(hdrs.CONTENT_LENGTH, None) + + self._writer.append_payload(part) + + self._is_processed = True + return self._writer + + def __call__(self) -> Payload: + if self._is_multipart: + return self._gen_form_data() + else: + return self._gen_form_urlencoded() diff --git a/aiohttp/hdrs.py b/aiohttp/hdrs.py new file mode 100644 index 0000000..2f1f5e0 --- /dev/null +++ b/aiohttp/hdrs.py @@ -0,0 +1,108 @@ +"""HTTP Headers constants.""" + +# After changing the file content call ./tools/gen.py +# to regenerate the headers parser +from typing import Final, Set + +from multidict import istr + +METH_ANY: Final[str] = "*" +METH_CONNECT: Final[str] = "CONNECT" +METH_HEAD: Final[str] = "HEAD" +METH_GET: Final[str] = "GET" +METH_DELETE: Final[str] = "DELETE" +METH_OPTIONS: Final[str] = "OPTIONS" +METH_PATCH: Final[str] = "PATCH" +METH_POST: Final[str] = "POST" +METH_PUT: Final[str] = "PUT" +METH_TRACE: Final[str] = "TRACE" + +METH_ALL: Final[Set[str]] = { + METH_CONNECT, + METH_HEAD, + METH_GET, + METH_DELETE, + METH_OPTIONS, + METH_PATCH, + METH_POST, + METH_PUT, + METH_TRACE, +} + +ACCEPT: Final[istr] = istr("Accept") +ACCEPT_CHARSET: Final[istr] = istr("Accept-Charset") +ACCEPT_ENCODING: Final[istr] = istr("Accept-Encoding") +ACCEPT_LANGUAGE: Final[istr] = istr("Accept-Language") +ACCEPT_RANGES: Final[istr] = istr("Accept-Ranges") +ACCESS_CONTROL_MAX_AGE: Final[istr] = istr("Access-Control-Max-Age") +ACCESS_CONTROL_ALLOW_CREDENTIALS: Final[istr] = istr("Access-Control-Allow-Credentials") +ACCESS_CONTROL_ALLOW_HEADERS: Final[istr] = istr("Access-Control-Allow-Headers") +ACCESS_CONTROL_ALLOW_METHODS: Final[istr] = istr("Access-Control-Allow-Methods") +ACCESS_CONTROL_ALLOW_ORIGIN: Final[istr] = istr("Access-Control-Allow-Origin") +ACCESS_CONTROL_EXPOSE_HEADERS: Final[istr] = istr("Access-Control-Expose-Headers") +ACCESS_CONTROL_REQUEST_HEADERS: Final[istr] = istr("Access-Control-Request-Headers") +ACCESS_CONTROL_REQUEST_METHOD: Final[istr] = istr("Access-Control-Request-Method") +AGE: Final[istr] = istr("Age") +ALLOW: Final[istr] = istr("Allow") +AUTHORIZATION: Final[istr] = istr("Authorization") +CACHE_CONTROL: Final[istr] = istr("Cache-Control") +CONNECTION: Final[istr] = istr("Connection") +CONTENT_DISPOSITION: Final[istr] = istr("Content-Disposition") +CONTENT_ENCODING: Final[istr] = istr("Content-Encoding") +CONTENT_LANGUAGE: Final[istr] = istr("Content-Language") +CONTENT_LENGTH: Final[istr] = istr("Content-Length") +CONTENT_LOCATION: Final[istr] = istr("Content-Location") +CONTENT_MD5: Final[istr] = istr("Content-MD5") +CONTENT_RANGE: Final[istr] = istr("Content-Range") +CONTENT_TRANSFER_ENCODING: Final[istr] = istr("Content-Transfer-Encoding") +CONTENT_TYPE: Final[istr] = istr("Content-Type") +COOKIE: Final[istr] = istr("Cookie") +DATE: Final[istr] = istr("Date") +DESTINATION: Final[istr] = istr("Destination") +DIGEST: Final[istr] = istr("Digest") +ETAG: Final[istr] = istr("Etag") +EXPECT: Final[istr] = istr("Expect") +EXPIRES: Final[istr] = istr("Expires") +FORWARDED: Final[istr] = istr("Forwarded") +FROM: Final[istr] = istr("From") +HOST: Final[istr] = istr("Host") +IF_MATCH: Final[istr] = istr("If-Match") +IF_MODIFIED_SINCE: Final[istr] = istr("If-Modified-Since") +IF_NONE_MATCH: Final[istr] = istr("If-None-Match") +IF_RANGE: Final[istr] = istr("If-Range") +IF_UNMODIFIED_SINCE: Final[istr] = istr("If-Unmodified-Since") +KEEP_ALIVE: Final[istr] = istr("Keep-Alive") +LAST_EVENT_ID: Final[istr] = istr("Last-Event-ID") +LAST_MODIFIED: Final[istr] = istr("Last-Modified") +LINK: Final[istr] = istr("Link") +LOCATION: Final[istr] = istr("Location") +MAX_FORWARDS: Final[istr] = istr("Max-Forwards") +ORIGIN: Final[istr] = istr("Origin") +PRAGMA: Final[istr] = istr("Pragma") +PROXY_AUTHENTICATE: Final[istr] = istr("Proxy-Authenticate") +PROXY_AUTHORIZATION: Final[istr] = istr("Proxy-Authorization") +RANGE: Final[istr] = istr("Range") +REFERER: Final[istr] = istr("Referer") +RETRY_AFTER: Final[istr] = istr("Retry-After") +SEC_WEBSOCKET_ACCEPT: Final[istr] = istr("Sec-WebSocket-Accept") +SEC_WEBSOCKET_VERSION: Final[istr] = istr("Sec-WebSocket-Version") +SEC_WEBSOCKET_PROTOCOL: Final[istr] = istr("Sec-WebSocket-Protocol") +SEC_WEBSOCKET_EXTENSIONS: Final[istr] = istr("Sec-WebSocket-Extensions") +SEC_WEBSOCKET_KEY: Final[istr] = istr("Sec-WebSocket-Key") +SEC_WEBSOCKET_KEY1: Final[istr] = istr("Sec-WebSocket-Key1") +SERVER: Final[istr] = istr("Server") +SET_COOKIE: Final[istr] = istr("Set-Cookie") +TE: Final[istr] = istr("TE") +TRAILER: Final[istr] = istr("Trailer") +TRANSFER_ENCODING: Final[istr] = istr("Transfer-Encoding") +UPGRADE: Final[istr] = istr("Upgrade") +URI: Final[istr] = istr("URI") +USER_AGENT: Final[istr] = istr("User-Agent") +VARY: Final[istr] = istr("Vary") +VIA: Final[istr] = istr("Via") +WANT_DIGEST: Final[istr] = istr("Want-Digest") +WARNING: Final[istr] = istr("Warning") +WWW_AUTHENTICATE: Final[istr] = istr("WWW-Authenticate") +X_FORWARDED_FOR: Final[istr] = istr("X-Forwarded-For") +X_FORWARDED_HOST: Final[istr] = istr("X-Forwarded-Host") +X_FORWARDED_PROTO: Final[istr] = istr("X-Forwarded-Proto") diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py new file mode 100644 index 0000000..cbed287 --- /dev/null +++ b/aiohttp/helpers.py @@ -0,0 +1,1065 @@ +"""Various helper functions""" + +import asyncio +import base64 +import binascii +import contextlib +import dataclasses +import datetime +import enum +import functools +import inspect +import netrc +import os +import platform +import re +import sys +import time +import warnings +import weakref +from collections import namedtuple +from contextlib import suppress +from email.parser import HeaderParser +from email.utils import parsedate +from http.cookies import SimpleCookie +from math import ceil +from pathlib import Path +from types import TracebackType +from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generator, + Generic, + Iterable, + Iterator, + List, + Mapping, + Optional, + Pattern, + Protocol, + Tuple, + Type, + TypeVar, + Union, + final, + get_args, + overload, +) +from urllib.parse import quote +from urllib.request import getproxies, proxy_bypass + +from multidict import CIMultiDict, MultiDict, MultiDictProxy +from yarl import URL + +from . import hdrs +from .log import client_logger +from .typedefs import PathLike # noqa + +if sys.version_info >= (3, 11): + import asyncio as async_timeout +else: + import async_timeout + +__all__ = ("BasicAuth", "ChainMapProxy", "ETag") + +PY_310 = sys.version_info >= (3, 10) + +COOKIE_MAX_LENGTH = 4096 + +_T = TypeVar("_T") +_S = TypeVar("_S") + +_SENTINEL = enum.Enum("_SENTINEL", "sentinel") +sentinel = _SENTINEL.sentinel + +NO_EXTENSIONS = bool(os.environ.get("AIOHTTP_NO_EXTENSIONS")) + +DEBUG = sys.flags.dev_mode or ( + not sys.flags.ignore_environment and bool(os.environ.get("PYTHONASYNCIODEBUG")) +) + + +CHAR = {chr(i) for i in range(0, 128)} +CTL = {chr(i) for i in range(0, 32)} | { + chr(127), +} +SEPARATORS = { + "(", + ")", + "<", + ">", + "@", + ",", + ";", + ":", + "\\", + '"', + "/", + "[", + "]", + "?", + "=", + "{", + "}", + " ", + chr(9), +} +TOKEN = CHAR ^ CTL ^ SEPARATORS + + +class noop: + def __await__(self) -> Generator[None, None, None]: + yield + + +json_re = re.compile(r"(?:application/|[\w.-]+/[\w.+-]+?\+)json$", re.IGNORECASE) + + +class BasicAuth(namedtuple("BasicAuth", ["login", "password", "encoding"])): + """Http basic authentication helper.""" + + def __new__( + cls, login: str, password: str = "", encoding: str = "latin1" + ) -> "BasicAuth": + if login is None: + raise ValueError("None is not allowed as login value") + + if password is None: + raise ValueError("None is not allowed as password value") + + if ":" in login: + raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)') + + return super().__new__(cls, login, password, encoding) + + @classmethod + def decode(cls, auth_header: str, encoding: str = "latin1") -> "BasicAuth": + """Create a BasicAuth object from an Authorization HTTP header.""" + try: + auth_type, encoded_credentials = auth_header.split(" ", 1) + except ValueError: + raise ValueError("Could not parse authorization header.") + + if auth_type.lower() != "basic": + raise ValueError("Unknown authorization method %s" % auth_type) + + try: + decoded = base64.b64decode( + encoded_credentials.encode("ascii"), validate=True + ).decode(encoding) + except binascii.Error: + raise ValueError("Invalid base64 encoding.") + + try: + # RFC 2617 HTTP Authentication + # https://www.ietf.org/rfc/rfc2617.txt + # the colon must be present, but the username and password may be + # otherwise blank. + username, password = decoded.split(":", 1) + except ValueError: + raise ValueError("Invalid credentials.") + + return cls(username, password, encoding=encoding) + + @classmethod + def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth"]: + """Create BasicAuth from url.""" + if not isinstance(url, URL): + raise TypeError("url should be yarl.URL instance") + if url.user is None: + return None + return cls(url.user, url.password or "", encoding=encoding) + + def encode(self) -> str: + """Encode credentials.""" + creds = (f"{self.login}:{self.password}").encode(self.encoding) + return "Basic %s" % base64.b64encode(creds).decode(self.encoding) + + +def strip_auth_from_url(url: URL) -> Tuple[URL, Optional[BasicAuth]]: + auth = BasicAuth.from_url(url) + if auth is None: + return url, None + else: + return url.with_user(None), auth + + +def netrc_from_env() -> Optional[netrc.netrc]: + """Load netrc from file. + + Attempt to load it from the path specified by the env-var + NETRC or in the default location in the user's home directory. + + Returns None if it couldn't be found or fails to parse. + """ + netrc_env = os.environ.get("NETRC") + + if netrc_env is not None: + netrc_path = Path(netrc_env) + else: + try: + home_dir = Path.home() + except RuntimeError as e: # pragma: no cover + # if pathlib can't resolve home, it may raise a RuntimeError + client_logger.debug( + "Could not resolve home directory when " + "trying to look for .netrc file: %s", + e, + ) + return None + + netrc_path = home_dir / ( + "_netrc" if platform.system() == "Windows" else ".netrc" + ) + + try: + return netrc.netrc(str(netrc_path)) + except netrc.NetrcParseError as e: + client_logger.warning("Could not parse .netrc file: %s", e) + except OSError as e: + netrc_exists = False + with contextlib.suppress(OSError): + netrc_exists = netrc_path.is_file() + # we couldn't read the file (doesn't exist, permissions, etc.) + if netrc_env or netrc_exists: + # only warn if the environment wanted us to load it, + # or it appears like the default file does actually exist + client_logger.warning("Could not read .netrc file: %s", e) + + return None + + +@dataclasses.dataclass(frozen=True) +class ProxyInfo: + proxy: URL + proxy_auth: Optional[BasicAuth] + + +def basicauth_from_netrc(netrc_obj: Optional[netrc.netrc], host: str) -> BasicAuth: + """ + Return :py:class:`~aiohttp.BasicAuth` credentials for ``host`` from ``netrc_obj``. + + :raises LookupError: if ``netrc_obj`` is :py:data:`None` or if no + entry is found for the ``host``. + """ + if netrc_obj is None: + raise LookupError("No .netrc file found") + auth_from_netrc = netrc_obj.authenticators(host) + + if auth_from_netrc is None: + raise LookupError(f"No entry for {host!s} found in the `.netrc` file.") + login, account, password = auth_from_netrc + + # TODO(PY311): username = login or account + # Up to python 3.10, account could be None if not specified, + # and login will be empty string if not specified. From 3.11, + # login and account will be empty string if not specified. + username = login if (login or account is None) else account + + # TODO(PY311): Remove this, as password will be empty string + # if not specified + if password is None: + password = "" + + return BasicAuth(username, password) + + +def proxies_from_env() -> Dict[str, ProxyInfo]: + proxy_urls = { + k: URL(v) + for k, v in getproxies().items() + if k in ("http", "https", "ws", "wss") + } + netrc_obj = netrc_from_env() + stripped = {k: strip_auth_from_url(v) for k, v in proxy_urls.items()} + ret = {} + for proto, val in stripped.items(): + proxy, auth = val + if proxy.scheme in ("https", "wss"): + client_logger.warning( + "%s proxies %s are not supported, ignoring", proxy.scheme.upper(), proxy + ) + continue + if netrc_obj and auth is None: + if proxy.host is not None: + try: + auth = basicauth_from_netrc(netrc_obj, proxy.host) + except LookupError: + auth = None + ret[proto] = ProxyInfo(proxy, auth) + return ret + + +def get_env_proxy_for_url(url: URL) -> Tuple[URL, Optional[BasicAuth]]: + """Get a permitted proxy for the given URL from the env.""" + if url.host is not None and proxy_bypass(url.host): + raise LookupError(f"Proxying is disallowed for `{url.host!r}`") + + proxies_in_env = proxies_from_env() + try: + proxy_info = proxies_in_env[url.scheme] + except KeyError: + raise LookupError(f"No proxies found for `{url!s}` in the env") + else: + return proxy_info.proxy, proxy_info.proxy_auth + + +@dataclasses.dataclass(frozen=True) +class MimeType: + type: str + subtype: str + suffix: str + parameters: "MultiDictProxy[str]" + + +@functools.lru_cache(maxsize=56) +def parse_mimetype(mimetype: str) -> MimeType: + """Parses a MIME type into its components. + + mimetype is a MIME type string. + + Returns a MimeType object. + + Example: + + >>> parse_mimetype('text/html; charset=utf-8') + MimeType(type='text', subtype='html', suffix='', + parameters={'charset': 'utf-8'}) + + """ + if not mimetype: + return MimeType( + type="", subtype="", suffix="", parameters=MultiDictProxy(MultiDict()) + ) + + parts = mimetype.split(";") + params: MultiDict[str] = MultiDict() + for item in parts[1:]: + if not item: + continue + key, _, value = item.partition("=") + params.add(key.lower().strip(), value.strip(' "')) + + fulltype = parts[0].strip().lower() + if fulltype == "*": + fulltype = "*/*" + + mtype, _, stype = fulltype.partition("/") + stype, _, suffix = stype.partition("+") + + return MimeType( + type=mtype, subtype=stype, suffix=suffix, parameters=MultiDictProxy(params) + ) + + +def guess_filename(obj: Any, default: Optional[str] = None) -> Optional[str]: + name = getattr(obj, "name", None) + if name and isinstance(name, str) and name[0] != "<" and name[-1] != ">": + return Path(name).name + return default + + +not_qtext_re = re.compile(r"[^\041\043-\133\135-\176]") +QCONTENT = {chr(i) for i in range(0x20, 0x7F)} | {"\t"} + + +def quoted_string(content: str) -> str: + """Return 7-bit content as quoted-string. + + Format content into a quoted-string as defined in RFC5322 for + Internet Message Format. Notice that this is not the 8-bit HTTP + format, but the 7-bit email format. Content must be in usascii or + a ValueError is raised. + """ + if not (QCONTENT > set(content)): + raise ValueError(f"bad content for quoted-string {content!r}") + return not_qtext_re.sub(lambda x: "\\" + x.group(0), content) + + +def content_disposition_header( + disptype: str, quote_fields: bool = True, _charset: str = "utf-8", **params: str +) -> str: + """Sets ``Content-Disposition`` header for MIME. + + This is the MIME payload Content-Disposition header from RFC 2183 + and RFC 7579 section 4.2, not the HTTP Content-Disposition from + RFC 6266. + + disptype is a disposition type: inline, attachment, form-data. + Should be valid extension token (see RFC 2183) + + quote_fields performs value quoting to 7-bit MIME headers + according to RFC 7578. Set to quote_fields to False if recipient + can take 8-bit file names and field values. + + _charset specifies the charset to use when quote_fields is True. + + params is a dict with disposition params. + """ + if not disptype or not (TOKEN > set(disptype)): + raise ValueError("bad content disposition type {!r}" "".format(disptype)) + + value = disptype + if params: + lparams = [] + for key, val in params.items(): + if not key or not (TOKEN > set(key)): + raise ValueError( + "bad content disposition parameter" " {!r}={!r}".format(key, val) + ) + if quote_fields: + if key.lower() == "filename": + qval = quote(val, "", encoding=_charset) + lparams.append((key, '"%s"' % qval)) + else: + try: + qval = quoted_string(val) + except ValueError: + qval = "".join( + (_charset, "''", quote(val, "", encoding=_charset)) + ) + lparams.append((key + "*", qval)) + else: + lparams.append((key, '"%s"' % qval)) + else: + qval = val.replace("\\", "\\\\").replace('"', '\\"') + lparams.append((key, '"%s"' % qval)) + sparams = "; ".join("=".join(pair) for pair in lparams) + value = "; ".join((value, sparams)) + return value + + +def is_expected_content_type( + response_content_type: str, expected_content_type: str +) -> bool: + """Checks if received content type is processable as an expected one. + + Both arguments should be given without parameters. + """ + if expected_content_type == "application/json": + return json_re.match(response_content_type) is not None + return expected_content_type in response_content_type + + +class _TSelf(Protocol, Generic[_T]): + _cache: Dict[str, _T] + + +class reify(Generic[_T]): + """Use as a class method decorator. + + It operates almost exactly like + the Python `@property` decorator, but it puts the result of the + method it decorates into the instance dict after the first call, + effectively replacing the function it decorates with an instance + variable. It is, in Python parlance, a data descriptor. + """ + + def __init__(self, wrapped: Callable[..., _T]) -> None: + self.wrapped = wrapped + self.__doc__ = wrapped.__doc__ + self.name = wrapped.__name__ + + def __get__(self, inst: _TSelf[_T], owner: Optional[Type[Any]] = None) -> _T: + try: + try: + return inst._cache[self.name] + except KeyError: + val = self.wrapped(inst) + inst._cache[self.name] = val + return val + except AttributeError: + if inst is None: + return self + raise + + def __set__(self, inst: _TSelf[_T], value: _T) -> None: + raise AttributeError("reified property is read-only") + + +reify_py = reify + +try: + from ._helpers import reify as reify_c + + if not NO_EXTENSIONS: + reify = reify_c # type: ignore[misc,assignment] +except ImportError: + pass + +_ipv4_pattern = ( + r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}" + r"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" +) +_ipv6_pattern = ( + r"^(?:(?:(?:[A-F0-9]{1,4}:){6}|(?=(?:[A-F0-9]{0,4}:){0,6}" + r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}$)(([0-9A-F]{1,4}:){0,5}|:)" + r"((:[0-9A-F]{1,4}){1,5}:|:)|::(?:[A-F0-9]{1,4}:){5})" + r"(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}" + r"(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[A-F0-9]{1,4}:){7}" + r"[A-F0-9]{1,4}|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}$)" + r"(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)|(?:[A-F0-9]{1,4}:){7}" + r":|:(:[A-F0-9]{1,4}){7})$" +) +_ipv4_regex = re.compile(_ipv4_pattern) +_ipv6_regex = re.compile(_ipv6_pattern, flags=re.IGNORECASE) +_ipv4_regexb = re.compile(_ipv4_pattern.encode("ascii")) +_ipv6_regexb = re.compile(_ipv6_pattern.encode("ascii"), flags=re.IGNORECASE) + + +def _is_ip_address( + regex: Pattern[str], regexb: Pattern[bytes], host: Optional[Union[str, bytes]] +) -> bool: + if host is None: + return False + if isinstance(host, str): + return bool(regex.match(host)) + elif isinstance(host, (bytes, bytearray, memoryview)): + return bool(regexb.match(host)) + else: + raise TypeError(f"{host} [{type(host)}] is not a str or bytes") + + +is_ipv4_address = functools.partial(_is_ip_address, _ipv4_regex, _ipv4_regexb) +is_ipv6_address = functools.partial(_is_ip_address, _ipv6_regex, _ipv6_regexb) + + +def is_ip_address(host: Optional[Union[str, bytes, bytearray, memoryview]]) -> bool: + return is_ipv4_address(host) or is_ipv6_address(host) + + +def next_whole_second() -> datetime.datetime: + """Return current time rounded up to the next whole second.""" + return datetime.datetime.now(datetime.timezone.utc).replace( + microsecond=0 + ) + datetime.timedelta(seconds=0) + + +_cached_current_datetime: Optional[int] = None +_cached_formatted_datetime = "" + + +def rfc822_formatted_time() -> str: + global _cached_current_datetime + global _cached_formatted_datetime + + now = int(time.time()) + if now != _cached_current_datetime: + # Weekday and month names for HTTP date/time formatting; + # always English! + # Tuples are constants stored in codeobject! + _weekdayname = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") + _monthname = ( + "", # Dummy so we can use 1-based month numbers + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ) + + year, month, day, hh, mm, ss, wd, *tail = time.gmtime(now) + _cached_formatted_datetime = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( + _weekdayname[wd], + day, + _monthname[month], + year, + hh, + mm, + ss, + ) + _cached_current_datetime = now + return _cached_formatted_datetime + + +def _weakref_handle(info: "Tuple[weakref.ref[object], str]") -> None: + ref, name = info + ob = ref() + if ob is not None: + with suppress(Exception): + getattr(ob, name)() + + +def weakref_handle( + ob: object, + name: str, + timeout: Optional[float], + loop: asyncio.AbstractEventLoop, + timeout_ceil_threshold: float = 5, +) -> Optional[asyncio.TimerHandle]: + if timeout is not None and timeout > 0: + when = loop.time() + timeout + if timeout >= timeout_ceil_threshold: + when = ceil(when) + + return loop.call_at(when, _weakref_handle, (weakref.ref(ob), name)) + return None + + +def call_later( + cb: Callable[[], Any], + timeout: Optional[float], + loop: asyncio.AbstractEventLoop, + timeout_ceil_threshold: float = 5, +) -> Optional[asyncio.TimerHandle]: + if timeout is not None and timeout > 0: + when = loop.time() + timeout + if timeout > timeout_ceil_threshold: + when = ceil(when) + return loop.call_at(when, cb) + return None + + +class TimeoutHandle: + """Timeout handle""" + + def __init__( + self, + loop: asyncio.AbstractEventLoop, + timeout: Optional[float], + ceil_threshold: float = 5, + ) -> None: + self._timeout = timeout + self._loop = loop + self._ceil_threshold = ceil_threshold + self._callbacks: List[ + Tuple[Callable[..., None], Tuple[Any, ...], Dict[str, Any]] + ] = [] + + def register( + self, callback: Callable[..., None], *args: Any, **kwargs: Any + ) -> None: + self._callbacks.append((callback, args, kwargs)) + + def close(self) -> None: + self._callbacks.clear() + + def start(self) -> Optional[asyncio.Handle]: + timeout = self._timeout + if timeout is not None and timeout > 0: + when = self._loop.time() + timeout + if timeout >= self._ceil_threshold: + when = ceil(when) + return self._loop.call_at(when, self.__call__) + else: + return None + + def timer(self) -> "BaseTimerContext": + if self._timeout is not None and self._timeout > 0: + timer = TimerContext(self._loop) + self.register(timer.timeout) + return timer + else: + return TimerNoop() + + def __call__(self) -> None: + for cb, args, kwargs in self._callbacks: + with suppress(Exception): + cb(*args, **kwargs) + + self._callbacks.clear() + + +class BaseTimerContext(ContextManager["BaseTimerContext"]): + def assert_timeout(self) -> None: + """Raise TimeoutError if timeout has been exceeded.""" + + +class TimerNoop(BaseTimerContext): + def __enter__(self) -> BaseTimerContext: + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + return + + +class TimerContext(BaseTimerContext): + """Low resolution timeout context manager""" + + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + self._loop = loop + self._tasks: List[asyncio.Task[Any]] = [] + self._cancelled = False + + def assert_timeout(self) -> None: + """Raise TimeoutError if timer has already been cancelled.""" + if self._cancelled: + raise asyncio.TimeoutError from None + + def __enter__(self) -> BaseTimerContext: + task = asyncio.current_task(loop=self._loop) + + if task is None: + raise RuntimeError( + "Timeout context manager should be used " "inside a task" + ) + + if self._cancelled: + raise asyncio.TimeoutError from None + + self._tasks.append(task) + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Optional[bool]: + if self._tasks: + self._tasks.pop() # type: ignore[unused-awaitable] + + if exc_type is asyncio.CancelledError and self._cancelled: + raise asyncio.TimeoutError from None + return None + + def timeout(self) -> None: + if not self._cancelled: + for task in set(self._tasks): + task.cancel() + + self._cancelled = True + + +def ceil_timeout( + delay: Optional[float], ceil_threshold: float = 5 +) -> async_timeout.Timeout: + if delay is None or delay <= 0: + return async_timeout.timeout(None) + + loop = asyncio.get_running_loop() + now = loop.time() + when = now + delay + if delay > ceil_threshold: + when = ceil(when) + return async_timeout.timeout_at(when) + + +class HeadersMixin: + __slots__ = ("_content_type", "_content_dict", "_stored_content_type") + + def __init__(self) -> None: + super().__init__() + self._content_type: Optional[str] = None + self._content_dict: Optional[Dict[str, str]] = None + self._stored_content_type: Union[str, _SENTINEL] = sentinel + + def _parse_content_type(self, raw: str) -> None: + self._stored_content_type = raw + if raw is None: + # default value according to RFC 2616 + self._content_type = "application/octet-stream" + self._content_dict = {} + else: + msg = HeaderParser().parsestr("Content-Type: " + raw) + self._content_type = msg.get_content_type() + params = msg.get_params(()) + self._content_dict = dict(params[1:]) # First element is content type again + + @property + def content_type(self) -> str: + """The value of content part for Content-Type HTTP header.""" + raw = self._headers.get(hdrs.CONTENT_TYPE) # type: ignore[attr-defined] + if self._stored_content_type != raw: + self._parse_content_type(raw) + return self._content_type # type: ignore[return-value] + + @property + def charset(self) -> Optional[str]: + """The value of charset part for Content-Type HTTP header.""" + raw = self._headers.get(hdrs.CONTENT_TYPE) # type: ignore[attr-defined] + if self._stored_content_type != raw: + self._parse_content_type(raw) + return self._content_dict.get("charset") # type: ignore[union-attr] + + @property + def content_length(self) -> Optional[int]: + """The value of Content-Length HTTP header.""" + content_length = self._headers.get( # type: ignore[attr-defined] + hdrs.CONTENT_LENGTH + ) + + if content_length is not None: + return int(content_length) + else: + return None + + +def set_result(fut: "asyncio.Future[_T]", result: _T) -> None: + if not fut.done(): + fut.set_result(result) + + +def set_exception(fut: "asyncio.Future[_T]", exc: BaseException) -> None: + if not fut.done(): + fut.set_exception(exc) + + +@functools.total_ordering +class AppKey(Generic[_T]): + """Keys for static typing support in Application.""" + + __slots__ = ("_name", "_t", "__orig_class__") + + # This may be set by Python when instantiating with a generic type. We need to + # support this, in order to support types that are not concrete classes, + # like Iterable, which can't be passed as the second parameter to __init__. + __orig_class__: Type[object] + + def __init__(self, name: str, t: Optional[Type[_T]] = None): + # Prefix with module name to help deduplicate key names. + frame = inspect.currentframe() + while frame: + if frame.f_code.co_name == "": + module: str = frame.f_globals["__name__"] + break + frame = frame.f_back + else: + raise RuntimeError("Failed to get module name.") + + # https://github.com/python/mypy/issues/14209 + self._name = module + "." + name # type: ignore[possibly-undefined] + self._t = t + + def __lt__(self, other: object) -> bool: + if isinstance(other, AppKey): + return self._name < other._name + return True # Order AppKey above other types. + + def __repr__(self) -> str: + t = self._t + if t is None: + with suppress(AttributeError): + # Set to type arg. + t = get_args(self.__orig_class__)[0] + + if t is None: + t_repr = "<>" + elif isinstance(t, type): + if t.__module__ == "builtins": + t_repr = t.__qualname__ + else: + t_repr = f"{t.__module__}.{t.__qualname__}" + else: + t_repr = repr(t) + return f"" + + +@final +class ChainMapProxy(Mapping[Union[str, AppKey[Any]], Any]): + __slots__ = ("_maps",) + + def __init__(self, maps: Iterable[Mapping[Union[str, AppKey[Any]], Any]]) -> None: + self._maps = tuple(maps) + + def __init_subclass__(cls) -> None: + raise TypeError( + "Inheritance class {} from ChainMapProxy " + "is forbidden".format(cls.__name__) + ) + + @overload # type: ignore[override] + def __getitem__(self, key: AppKey[_T]) -> _T: + ... + + @overload + def __getitem__(self, key: str) -> Any: + ... + + def __getitem__(self, key: Union[str, AppKey[_T]]) -> Any: + for mapping in self._maps: + try: + return mapping[key] + except KeyError: + pass + raise KeyError(key) + + @overload # type: ignore[override] + def get(self, key: AppKey[_T], default: _S) -> Union[_T, _S]: + ... + + @overload + def get(self, key: AppKey[_T], default: None = ...) -> Optional[_T]: + ... + + @overload + def get(self, key: str, default: Any = ...) -> Any: + ... + + def get(self, key: Union[str, AppKey[_T]], default: Any = None) -> Any: + try: + return self[key] + except KeyError: + return default + + def __len__(self) -> int: + # reuses stored hash values if possible + return len(set().union(*self._maps)) + + def __iter__(self) -> Iterator[Union[str, AppKey[Any]]]: + d: Dict[Union[str, AppKey[Any]], Any] = {} + for mapping in reversed(self._maps): + # reuses stored hash values if possible + d.update(mapping) + return iter(d) + + def __contains__(self, key: object) -> bool: + return any(key in m for m in self._maps) + + def __bool__(self) -> bool: + return any(self._maps) + + def __repr__(self) -> str: + content = ", ".join(map(repr, self._maps)) + return f"ChainMapProxy({content})" + + +class CookieMixin: + # The `_cookies` slots is not defined here because non-empty slots cannot + # be combined with an Exception base class, as is done in HTTPException. + # CookieMixin subclasses with slots should define the `_cookies` + # slot themselves. + __slots__ = () + + def __init__(self) -> None: + super().__init__() + # Mypy doesn't like that _cookies isn't in __slots__. + # See the comment on this class's __slots__ for why this is OK. + self._cookies: SimpleCookie[str] = SimpleCookie() # type: ignore[misc] + + @property + def cookies(self) -> "SimpleCookie[str]": + return self._cookies + + def set_cookie( + self, + name: str, + value: str, + *, + expires: Optional[str] = None, + domain: Optional[str] = None, + max_age: Optional[Union[int, str]] = None, + path: str = "/", + secure: Optional[bool] = None, + httponly: Optional[bool] = None, + version: Optional[str] = None, + samesite: Optional[str] = None, + ) -> None: + """Set or update response cookie. + + Sets new cookie or updates existent with new value. + Also updates only those params which are not None. + """ + old = self._cookies.get(name) + if old is not None and old.coded_value == "": + # deleted cookie + self._cookies.pop(name, None) + + self._cookies[name] = value + c = self._cookies[name] + + if expires is not None: + c["expires"] = expires + elif c.get("expires") == "Thu, 01 Jan 1970 00:00:00 GMT": + del c["expires"] + + if domain is not None: + c["domain"] = domain + + if max_age is not None: + c["max-age"] = str(max_age) + elif "max-age" in c: + del c["max-age"] + + c["path"] = path + + if secure is not None: + c["secure"] = secure + if httponly is not None: + c["httponly"] = httponly + if version is not None: + c["version"] = version + if samesite is not None: + c["samesite"] = samesite + + if DEBUG: + cookie_length = len(c.output(header="")[1:]) + if cookie_length > COOKIE_MAX_LENGTH: + warnings.warn( + "The size of is too large, it might get ignored by the client.", + UserWarning, + stacklevel=2, + ) + + def del_cookie( + self, name: str, *, domain: Optional[str] = None, path: str = "/" + ) -> None: + """Delete cookie. + + Creates new empty expired cookie. + """ + # TODO: do we need domain/path here? + self._cookies.pop(name, None) + self.set_cookie( + name, + "", + max_age=0, + expires="Thu, 01 Jan 1970 00:00:00 GMT", + domain=domain, + path=path, + ) + + +def populate_with_cookies( + headers: "CIMultiDict[str]", cookies: "SimpleCookie[str]" +) -> None: + for cookie in cookies.values(): + value = cookie.output(header="")[1:] + headers.add(hdrs.SET_COOKIE, value) + + +# https://tools.ietf.org/html/rfc7232#section-2.3 +_ETAGC = r"[!#-}\x80-\xff]+" +_ETAGC_RE = re.compile(_ETAGC) +_QUOTED_ETAG = rf'(W/)?"({_ETAGC})"' +QUOTED_ETAG_RE = re.compile(_QUOTED_ETAG) +LIST_QUOTED_ETAG_RE = re.compile(rf"({_QUOTED_ETAG})(?:\s*,\s*|$)|(.)") + +ETAG_ANY = "*" + + +@dataclasses.dataclass(frozen=True) +class ETag: + value: str + is_weak: bool = False + + +def validate_etag_value(value: str) -> None: + if value != ETAG_ANY and not _ETAGC_RE.fullmatch(value): + raise ValueError( + f"Value {value!r} is not a valid etag. Maybe it contains '\"'?" + ) + + +def parse_http_date(date_str: Optional[str]) -> Optional[datetime.datetime]: + """Process a date string, return a datetime object""" + if date_str is not None: + timetuple = parsedate(date_str) + if timetuple is not None: + with suppress(ValueError): + return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc) + return None diff --git a/aiohttp/http.py b/aiohttp/http.py new file mode 100644 index 0000000..244d71c --- /dev/null +++ b/aiohttp/http.py @@ -0,0 +1,60 @@ +import sys + +from . import __version__ +from .http_exceptions import HttpProcessingError +from .http_parser import ( + HeadersParser, + HttpParser, + HttpRequestParser, + HttpResponseParser, + RawRequestMessage, + RawResponseMessage, +) +from .http_websocket import ( + WS_CLOSED_MESSAGE, + WS_CLOSING_MESSAGE, + WS_KEY, + WebSocketError, + WebSocketReader, + WebSocketWriter, + WSCloseCode, + WSMessage, + WSMsgType, + ws_ext_gen, + ws_ext_parse, +) +from .http_writer import HttpVersion, HttpVersion10, HttpVersion11, StreamWriter + +__all__ = ( + "HttpProcessingError", + "SERVER_SOFTWARE", + # .http_writer + "StreamWriter", + "HttpVersion", + "HttpVersion10", + "HttpVersion11", + # .http_parser + "HeadersParser", + "HttpParser", + "HttpRequestParser", + "HttpResponseParser", + "RawRequestMessage", + "RawResponseMessage", + # .http_websocket + "WS_CLOSED_MESSAGE", + "WS_CLOSING_MESSAGE", + "WS_KEY", + "WebSocketReader", + "WebSocketWriter", + "ws_ext_gen", + "ws_ext_parse", + "WSMessage", + "WebSocketError", + "WSMsgType", + "WSCloseCode", +) + + +SERVER_SOFTWARE: str = "Python/{0[0]}.{0[1]} aiohttp/{1}".format( + sys.version_info, __version__ +) diff --git a/aiohttp/http_exceptions.py b/aiohttp/http_exceptions.py new file mode 100644 index 0000000..728824f --- /dev/null +++ b/aiohttp/http_exceptions.py @@ -0,0 +1,105 @@ +"""Low-level http related exceptions.""" + + +from textwrap import indent +from typing import Optional, Union + +from .typedefs import _CIMultiDict + +__all__ = ("HttpProcessingError",) + + +class HttpProcessingError(Exception): + """HTTP error. + + Shortcut for raising HTTP errors with custom code, message and headers. + + code: HTTP Error code. + message: (optional) Error message. + headers: (optional) Headers to be sent in response, a list of pairs + """ + + code = 0 + message = "" + headers = None + + def __init__( + self, + *, + code: Optional[int] = None, + message: str = "", + headers: Optional[_CIMultiDict] = None, + ) -> None: + if code is not None: + self.code = code + self.headers = headers + self.message = message + + def __str__(self) -> str: + msg = indent(self.message, " ") + return f"{self.code}, message:\n{msg}" + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}: {self.code}, message={self.message!r}>" + + +class BadHttpMessage(HttpProcessingError): + code = 400 + message = "Bad Request" + + def __init__(self, message: str, *, headers: Optional[_CIMultiDict] = None) -> None: + super().__init__(message=message, headers=headers) + self.args = (message,) + + +class HttpBadRequest(BadHttpMessage): + code = 400 + message = "Bad Request" + + +class PayloadEncodingError(BadHttpMessage): + """Base class for payload errors""" + + +class ContentEncodingError(PayloadEncodingError): + """Content encoding error.""" + + +class TransferEncodingError(PayloadEncodingError): + """transfer encoding error.""" + + +class ContentLengthError(PayloadEncodingError): + """Not enough data for satisfy content length header.""" + + +class LineTooLong(BadHttpMessage): + def __init__( + self, line: str, limit: str = "Unknown", actual_size: str = "Unknown" + ) -> None: + super().__init__( + f"Got more than {limit} bytes ({actual_size}) when reading {line}." + ) + self.args = (line, limit, actual_size) + + +class InvalidHeader(BadHttpMessage): + def __init__(self, hdr: Union[bytes, str]) -> None: + if isinstance(hdr, bytes): + hdr = hdr.decode("utf-8", "surrogateescape") + super().__init__(f"Invalid HTTP Header: {hdr}") + self.hdr = hdr + self.args = (hdr,) + + +class BadStatusLine(BadHttpMessage): + def __init__(self, line: str = "") -> None: + if not isinstance(line, str): + line = repr(line) + super().__init__(f"Bad status line {line!r}") + self.args = (line,) + self.line = line + + +class InvalidURLError(BadHttpMessage): + pass diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py new file mode 100644 index 0000000..6eb30e4 --- /dev/null +++ b/aiohttp/http_parser.py @@ -0,0 +1,930 @@ +import abc +import asyncio +import re +import string +from contextlib import suppress +from enum import IntEnum +from typing import ( + Final, + Generic, + List, + NamedTuple, + Optional, + Pattern, + Set, + Tuple, + Type, + TypeVar, + Union, +) + +from multidict import CIMultiDict, CIMultiDictProxy, istr +from yarl import URL + +from . import hdrs +from .base_protocol import BaseProtocol +from .compression_utils import HAS_BROTLI, BrotliDecompressor, ZLibDecompressor +from .helpers import NO_EXTENSIONS, BaseTimerContext +from .http_exceptions import ( + BadHttpMessage, + BadStatusLine, + ContentEncodingError, + ContentLengthError, + InvalidHeader, + LineTooLong, + TransferEncodingError, +) +from .http_writer import HttpVersion, HttpVersion10 +from .log import internal_logger +from .streams import EMPTY_PAYLOAD, StreamReader +from .typedefs import RawHeaders + +__all__ = ( + "HeadersParser", + "HttpParser", + "HttpRequestParser", + "HttpResponseParser", + "RawRequestMessage", + "RawResponseMessage", +) + +ASCIISET: Final[Set[str]] = set(string.printable) + +# See https://tools.ietf.org/html/rfc7230#section-3.1.1 +# and https://tools.ietf.org/html/rfc7230#appendix-B +# +# method = token +# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / +# "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +# token = 1*tchar +METHRE: Final[Pattern[str]] = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+") +VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d+).(\d+)") +HDRRE: Final[Pattern[bytes]] = re.compile(rb"[\x00-\x1F\x7F()<>@,;:\[\]={} \t\\\\\"]") + + +class RawRequestMessage(NamedTuple): + method: str + path: str + version: HttpVersion + headers: CIMultiDictProxy[str] + raw_headers: RawHeaders + should_close: bool + compression: Optional[str] + upgrade: bool + chunked: bool + url: URL + + +class RawResponseMessage(NamedTuple): + version: HttpVersion + code: int + reason: str + headers: CIMultiDictProxy[str] + raw_headers: RawHeaders + should_close: bool + compression: Optional[str] + upgrade: bool + chunked: bool + + +_MsgT = TypeVar("_MsgT", RawRequestMessage, RawResponseMessage) + + +class ParseState(IntEnum): + PARSE_NONE = 0 + PARSE_LENGTH = 1 + PARSE_CHUNKED = 2 + PARSE_UNTIL_EOF = 3 + + +class ChunkState(IntEnum): + PARSE_CHUNKED_SIZE = 0 + PARSE_CHUNKED_CHUNK = 1 + PARSE_CHUNKED_CHUNK_EOF = 2 + PARSE_MAYBE_TRAILERS = 3 + PARSE_TRAILERS = 4 + + +class HeadersParser: + def __init__( + self, + max_line_size: int = 8190, + max_field_size: int = 8190, + ) -> None: + self.max_line_size = max_line_size + self.max_field_size = max_field_size + + def parse_headers( + self, lines: List[bytes] + ) -> Tuple["CIMultiDictProxy[str]", RawHeaders]: + headers: CIMultiDict[str] = CIMultiDict() + raw_headers = [] + + lines_idx = 1 + line = lines[1] + line_count = len(lines) + + while line: + # Parse initial header name : value pair. + try: + bname, bvalue = line.split(b":", 1) + except ValueError: + raise InvalidHeader(line) from None + + bname = bname.strip(b" \t") + bvalue = bvalue.lstrip() + if HDRRE.search(bname): + raise InvalidHeader(bname) + if len(bname) > self.max_field_size: + raise LineTooLong( + "request header name {}".format( + bname.decode("utf8", "backslashreplace") + ), + str(self.max_field_size), + str(len(bname)), + ) + + header_length = len(bvalue) + + # next line + lines_idx += 1 + line = lines[lines_idx] + + # consume continuation lines + continuation = line and line[0] in (32, 9) # (' ', '\t') + + if continuation: + bvalue_lst = [bvalue] + while continuation: + header_length += len(line) + if header_length > self.max_field_size: + raise LineTooLong( + "request header field {}".format( + bname.decode("utf8", "backslashreplace") + ), + str(self.max_field_size), + str(header_length), + ) + bvalue_lst.append(line) + + # next line + lines_idx += 1 + if lines_idx < line_count: + line = lines[lines_idx] + if line: + continuation = line[0] in (32, 9) # (' ', '\t') + else: + line = b"" + break + bvalue = b"".join(bvalue_lst) + else: + if header_length > self.max_field_size: + raise LineTooLong( + "request header field {}".format( + bname.decode("utf8", "backslashreplace") + ), + str(self.max_field_size), + str(header_length), + ) + + bvalue = bvalue.strip() + name = bname.decode("utf-8", "surrogateescape") + value = bvalue.decode("utf-8", "surrogateescape") + + headers.add(name, value) + raw_headers.append((bname, bvalue)) + + return (CIMultiDictProxy(headers), tuple(raw_headers)) + + +class HttpParser(abc.ABC, Generic[_MsgT]): + def __init__( + self, + protocol: BaseProtocol, + loop: asyncio.AbstractEventLoop, + limit: int, + max_line_size: int = 8190, + max_field_size: int = 8190, + timer: Optional[BaseTimerContext] = None, + code: Optional[int] = None, + method: Optional[str] = None, + readall: bool = False, + payload_exception: Optional[Type[BaseException]] = None, + response_with_body: bool = True, + read_until_eof: bool = False, + auto_decompress: bool = True, + ) -> None: + self.protocol = protocol + self.loop = loop + self.max_line_size = max_line_size + self.max_field_size = max_field_size + self.timer = timer + self.code = code + self.method = method + self.readall = readall + self.payload_exception = payload_exception + self.response_with_body = response_with_body + self.read_until_eof = read_until_eof + + self._lines: List[bytes] = [] + self._tail = b"" + self._upgraded = False + self._payload = None + self._payload_parser: Optional[HttpPayloadParser] = None + self._auto_decompress = auto_decompress + self._limit = limit + self._headers_parser = HeadersParser(max_line_size, max_field_size) + + @abc.abstractmethod + def parse_message(self, lines: List[bytes]) -> _MsgT: + pass + + def feed_eof(self) -> Optional[_MsgT]: + if self._payload_parser is not None: + self._payload_parser.feed_eof() + self._payload_parser = None + else: + # try to extract partial message + if self._tail: + self._lines.append(self._tail) + + if self._lines: + if self._lines[-1] != "\r\n": + self._lines.append(b"") + with suppress(Exception): + return self.parse_message(self._lines) + return None + + def feed_data( + self, + data: bytes, + SEP: bytes = b"\r\n", + EMPTY: bytes = b"", + CONTENT_LENGTH: istr = hdrs.CONTENT_LENGTH, + METH_CONNECT: str = hdrs.METH_CONNECT, + SEC_WEBSOCKET_KEY1: istr = hdrs.SEC_WEBSOCKET_KEY1, + ) -> Tuple[List[Tuple[_MsgT, StreamReader]], bool, bytes]: + messages = [] + + if self._tail: + data, self._tail = self._tail + data, b"" + + data_len = len(data) + start_pos = 0 + loop = self.loop + + while start_pos < data_len: + # read HTTP message (request/response line + headers), \r\n\r\n + # and split by lines + if self._payload_parser is None and not self._upgraded: + pos = data.find(SEP, start_pos) + # consume \r\n + if pos == start_pos and not self._lines: + start_pos = pos + 2 + continue + + if pos >= start_pos: + # line found + self._lines.append(data[start_pos:pos]) + start_pos = pos + 2 + + # \r\n\r\n found + if self._lines[-1] == EMPTY: + try: + msg: _MsgT = self.parse_message(self._lines) + finally: + self._lines.clear() + + def get_content_length() -> Optional[int]: + # payload length + length_hdr = msg.headers.get(CONTENT_LENGTH) + if length_hdr is None: + return None + + try: + length = int(length_hdr) + except ValueError: + raise InvalidHeader(CONTENT_LENGTH) + + if length < 0: + raise InvalidHeader(CONTENT_LENGTH) + + return length + + length = get_content_length() + # do not support old websocket spec + if SEC_WEBSOCKET_KEY1 in msg.headers: + raise InvalidHeader(SEC_WEBSOCKET_KEY1) + + self._upgraded = msg.upgrade + + method = getattr(msg, "method", self.method) + + assert self.protocol is not None + # calculate payload + if ( + (length is not None and length > 0) + or msg.chunked + and not msg.upgrade + ): + payload = StreamReader( + self.protocol, + timer=self.timer, + loop=loop, + limit=self._limit, + ) + payload_parser = HttpPayloadParser( + payload, + length=length, + chunked=msg.chunked, + method=method, + compression=msg.compression, + code=self.code, + readall=self.readall, + response_with_body=self.response_with_body, + auto_decompress=self._auto_decompress, + ) + if not payload_parser.done: + self._payload_parser = payload_parser + elif method == METH_CONNECT: + assert isinstance(msg, RawRequestMessage) + payload = StreamReader( + self.protocol, + timer=self.timer, + loop=loop, + limit=self._limit, + ) + self._upgraded = True + self._payload_parser = HttpPayloadParser( + payload, + method=msg.method, + compression=msg.compression, + readall=True, + auto_decompress=self._auto_decompress, + ) + else: + if ( + getattr(msg, "code", 100) >= 199 + and length is None + and self.read_until_eof + ): + payload = StreamReader( + self.protocol, + timer=self.timer, + loop=loop, + limit=self._limit, + ) + payload_parser = HttpPayloadParser( + payload, + length=length, + chunked=msg.chunked, + method=method, + compression=msg.compression, + code=self.code, + readall=True, + response_with_body=self.response_with_body, + auto_decompress=self._auto_decompress, + ) + if not payload_parser.done: + self._payload_parser = payload_parser + else: + payload = EMPTY_PAYLOAD + + messages.append((msg, payload)) + else: + self._tail = data[start_pos:] + data = EMPTY + break + + # no parser, just store + elif self._payload_parser is None and self._upgraded: + assert not self._lines + break + + # feed payload + elif data and start_pos < data_len: + assert not self._lines + assert self._payload_parser is not None + try: + eof, data = self._payload_parser.feed_data(data[start_pos:]) + except BaseException as exc: + if self.payload_exception is not None: + self._payload_parser.payload.set_exception( + self.payload_exception(str(exc)) + ) + else: + self._payload_parser.payload.set_exception(exc) + + eof = True + data = b"" + + if eof: + start_pos = 0 + data_len = len(data) + self._payload_parser = None + continue + else: + break + + if data and start_pos < data_len: + data = data[start_pos:] + else: + data = EMPTY + + return messages, self._upgraded, data + + def parse_headers( + self, lines: List[bytes] + ) -> Tuple[ + "CIMultiDictProxy[str]", RawHeaders, Optional[bool], Optional[str], bool, bool + ]: + """Parses RFC 5322 headers from a stream. + + Line continuations are supported. Returns list of header name + and value pairs. Header name is in upper case. + """ + headers, raw_headers = self._headers_parser.parse_headers(lines) + close_conn = None + encoding = None + upgrade = False + chunked = False + + # keep-alive + conn = headers.get(hdrs.CONNECTION) + if conn: + v = conn.lower() + if v == "close": + close_conn = True + elif v == "keep-alive": + close_conn = False + elif v == "upgrade": + upgrade = True + + # encoding + enc = headers.get(hdrs.CONTENT_ENCODING) + if enc: + enc = enc.lower() + if enc in ("gzip", "deflate", "br"): + encoding = enc + + # chunking + te = headers.get(hdrs.TRANSFER_ENCODING) + if te is not None: + if "chunked" == te.lower(): + chunked = True + else: + raise BadHttpMessage("Request has invalid `Transfer-Encoding`") + + if hdrs.CONTENT_LENGTH in headers: + raise BadHttpMessage( + "Transfer-Encoding can't be present with Content-Length", + ) + + return (headers, raw_headers, close_conn, encoding, upgrade, chunked) + + def set_upgraded(self, val: bool) -> None: + """Set connection upgraded (to websocket) mode. + + :param bool val: new state. + """ + self._upgraded = val + + +class HttpRequestParser(HttpParser[RawRequestMessage]): + """Read request status line. + + Exception .http_exceptions.BadStatusLine + could be raised in case of any errors in status line. + Returns RawRequestMessage. + """ + + def parse_message(self, lines: List[bytes]) -> RawRequestMessage: + # request line + line = lines[0].decode("utf-8", "surrogateescape") + try: + method, path, version = line.split(None, 2) + except ValueError: + raise BadStatusLine(line) from None + + if len(path) > self.max_line_size: + raise LineTooLong( + "Status line is too long", str(self.max_line_size), str(len(path)) + ) + + # method + if not METHRE.match(method): + raise BadStatusLine(method) + + # version + try: + if version.startswith("HTTP/"): + n1, n2 = version[5:].split(".", 1) + version_o = HttpVersion(int(n1), int(n2)) + else: + raise BadStatusLine(version) + except Exception: + raise BadStatusLine(version) + + if method == "CONNECT": + # authority-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3 + url = URL.build(authority=path, encoded=True) + elif path.startswith("/"): + # origin-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1 + path_part, _hash_separator, url_fragment = path.partition("#") + path_part, _question_mark_separator, qs_part = path_part.partition("?") + + # NOTE: `yarl.URL.build()` is used to mimic what the Cython-based + # NOTE: parser does, otherwise it results into the same + # NOTE: HTTP Request-Line input producing different + # NOTE: `yarl.URL()` objects + url = URL.build( + path=path_part, + query_string=qs_part, + fragment=url_fragment, + encoded=True, + ) + else: + # absolute-form for proxy maybe, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2 + url = URL(path, encoded=True) + + # read headers + ( + headers, + raw_headers, + close, + compression, + upgrade, + chunked, + ) = self.parse_headers(lines) + + if close is None: # then the headers weren't set in the request + if version_o <= HttpVersion10: # HTTP 1.0 must asks to not close + close = True + else: # HTTP 1.1 must ask to close. + close = False + + return RawRequestMessage( + method, + path, + version_o, + headers, + raw_headers, + close, + compression, + upgrade, + chunked, + url, + ) + + +class HttpResponseParser(HttpParser[RawResponseMessage]): + """Read response status line and headers. + + BadStatusLine could be raised in case of any errors in status line. + Returns RawResponseMessage. + """ + + def parse_message(self, lines: List[bytes]) -> RawResponseMessage: + line = lines[0].decode("utf-8", "surrogateescape") + try: + version, status = line.split(None, 1) + except ValueError: + raise BadStatusLine(line) from None + + try: + status, reason = status.split(None, 1) + except ValueError: + reason = "" + + if len(reason) > self.max_line_size: + raise LineTooLong( + "Status line is too long", str(self.max_line_size), str(len(reason)) + ) + + # version + match = VERSRE.match(version) + if match is None: + raise BadStatusLine(line) + version_o = HttpVersion(int(match.group(1)), int(match.group(2))) + + # The status code is a three-digit number + try: + status_i = int(status) + except ValueError: + raise BadStatusLine(line) from None + + if status_i > 999: + raise BadStatusLine(line) + + # read headers + ( + headers, + raw_headers, + close, + compression, + upgrade, + chunked, + ) = self.parse_headers(lines) + + if close is None: + close = version_o <= HttpVersion10 + + return RawResponseMessage( + version_o, + status_i, + reason.strip(), + headers, + raw_headers, + close, + compression, + upgrade, + chunked, + ) + + +class HttpPayloadParser: + def __init__( + self, + payload: StreamReader, + length: Optional[int] = None, + chunked: bool = False, + compression: Optional[str] = None, + code: Optional[int] = None, + method: Optional[str] = None, + readall: bool = False, + response_with_body: bool = True, + auto_decompress: bool = True, + ) -> None: + self._length = 0 + self._type = ParseState.PARSE_NONE + self._chunk = ChunkState.PARSE_CHUNKED_SIZE + self._chunk_size = 0 + self._chunk_tail = b"" + self._auto_decompress = auto_decompress + self.done = False + + # payload decompression wrapper + if response_with_body and compression and self._auto_decompress: + real_payload: Union[StreamReader, DeflateBuffer] = DeflateBuffer( + payload, compression + ) + else: + real_payload = payload + + # payload parser + if not response_with_body: + # don't parse payload if it's not expected to be received + self._type = ParseState.PARSE_NONE + real_payload.feed_eof() + self.done = True + + elif chunked: + self._type = ParseState.PARSE_CHUNKED + elif length is not None: + self._type = ParseState.PARSE_LENGTH + self._length = length + if self._length == 0: + real_payload.feed_eof() + self.done = True + else: + if readall and code != 204: + self._type = ParseState.PARSE_UNTIL_EOF + elif method in ("PUT", "POST"): + internal_logger.warning( # pragma: no cover + "Content-Length or Transfer-Encoding header is required" + ) + self._type = ParseState.PARSE_NONE + real_payload.feed_eof() + self.done = True + + self.payload = real_payload + + def feed_eof(self) -> None: + if self._type == ParseState.PARSE_UNTIL_EOF: + self.payload.feed_eof() + elif self._type == ParseState.PARSE_LENGTH: + raise ContentLengthError( + "Not enough data for satisfy content length header." + ) + elif self._type == ParseState.PARSE_CHUNKED: + raise TransferEncodingError( + "Not enough data for satisfy transfer length header." + ) + + def feed_data( + self, chunk: bytes, SEP: bytes = b"\r\n", CHUNK_EXT: bytes = b";" + ) -> Tuple[bool, bytes]: + # Read specified amount of bytes + if self._type == ParseState.PARSE_LENGTH: + required = self._length + chunk_len = len(chunk) + + if required >= chunk_len: + self._length = required - chunk_len + self.payload.feed_data(chunk, chunk_len) + if self._length == 0: + self.payload.feed_eof() + return True, b"" + else: + self._length = 0 + self.payload.feed_data(chunk[:required], required) + self.payload.feed_eof() + return True, chunk[required:] + + # Chunked transfer encoding parser + elif self._type == ParseState.PARSE_CHUNKED: + if self._chunk_tail: + chunk = self._chunk_tail + chunk + self._chunk_tail = b"" + + while chunk: + # read next chunk size + if self._chunk == ChunkState.PARSE_CHUNKED_SIZE: + pos = chunk.find(SEP) + if pos >= 0: + i = chunk.find(CHUNK_EXT, 0, pos) + if i >= 0: + size_b = chunk[:i] # strip chunk-extensions + else: + size_b = chunk[:pos] + + try: + size = int(bytes(size_b), 16) + except ValueError: + exc = TransferEncodingError( + chunk[:pos].decode("ascii", "surrogateescape") + ) + self.payload.set_exception(exc) + raise exc from None + + chunk = chunk[pos + 2 :] + if size == 0: # eof marker + self._chunk = ChunkState.PARSE_MAYBE_TRAILERS + else: + self._chunk = ChunkState.PARSE_CHUNKED_CHUNK + self._chunk_size = size + self.payload.begin_http_chunk_receiving() + else: + self._chunk_tail = chunk + return False, b"" + + # read chunk and feed buffer + if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK: + required = self._chunk_size + chunk_len = len(chunk) + + if required > chunk_len: + self._chunk_size = required - chunk_len + self.payload.feed_data(chunk, chunk_len) + return False, b"" + else: + self._chunk_size = 0 + self.payload.feed_data(chunk[:required], required) + chunk = chunk[required:] + self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF + self.payload.end_http_chunk_receiving() + + # toss the CRLF at the end of the chunk + if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK_EOF: + if chunk[:2] == SEP: + chunk = chunk[2:] + self._chunk = ChunkState.PARSE_CHUNKED_SIZE + else: + self._chunk_tail = chunk + return False, b"" + + # if stream does not contain trailer, after 0\r\n + # we should get another \r\n otherwise + # trailers needs to be skiped until \r\n\r\n + if self._chunk == ChunkState.PARSE_MAYBE_TRAILERS: + head = chunk[:2] + if head == SEP: + # end of stream + self.payload.feed_eof() + return True, chunk[2:] + # Both CR and LF, or only LF may not be received yet. It is + # expected that CRLF or LF will be shown at the very first + # byte next time, otherwise trailers should come. The last + # CRLF which marks the end of response might not be + # contained in the same TCP segment which delivered the + # size indicator. + if not head: + return False, b"" + if head == SEP[:1]: + self._chunk_tail = head + return False, b"" + self._chunk = ChunkState.PARSE_TRAILERS + + # read and discard trailer up to the CRLF terminator + if self._chunk == ChunkState.PARSE_TRAILERS: + pos = chunk.find(SEP) + if pos >= 0: + chunk = chunk[pos + 2 :] + self._chunk = ChunkState.PARSE_MAYBE_TRAILERS + else: + self._chunk_tail = chunk + return False, b"" + + # Read all bytes until eof + elif self._type == ParseState.PARSE_UNTIL_EOF: + self.payload.feed_data(chunk, len(chunk)) + + return False, b"" + + +class DeflateBuffer: + """DeflateStream decompress stream and feed data into specified stream.""" + + def __init__(self, out: StreamReader, encoding: Optional[str]) -> None: + self.out = out + self.size = 0 + self.encoding = encoding + self._started_decoding = False + + self.decompressor: Union[BrotliDecompressor, ZLibDecompressor] + if encoding == "br": + if not HAS_BROTLI: # pragma: no cover + raise ContentEncodingError( + "Can not decode content-encoding: brotli (br). " + "Please install `Brotli`" + ) + self.decompressor = BrotliDecompressor() + else: + self.decompressor = ZLibDecompressor(encoding=encoding) + + def set_exception(self, exc: BaseException) -> None: + self.out.set_exception(exc) + + def feed_data(self, chunk: bytes, size: int) -> None: + if not size: + return + + self.size += size + + # RFC1950 + # bits 0..3 = CM = 0b1000 = 8 = "deflate" + # bits 4..7 = CINFO = 1..7 = windows size. + if ( + not self._started_decoding + and self.encoding == "deflate" + and chunk[0] & 0xF != 8 + ): + # Change the decoder to decompress incorrectly compressed data + # Actually we should issue a warning about non-RFC-compliant data. + self.decompressor = ZLibDecompressor( + encoding=self.encoding, suppress_deflate_header=True + ) + + try: + chunk = self.decompressor.decompress_sync(chunk) + except Exception: + raise ContentEncodingError( + "Can not decode content-encoding: %s" % self.encoding + ) + + self._started_decoding = True + + if chunk: + self.out.feed_data(chunk, len(chunk)) + + def feed_eof(self) -> None: + chunk = self.decompressor.flush() + + if chunk or self.size > 0: + self.out.feed_data(chunk, len(chunk)) + # decompressor is not brotli unless encoding is "br" + if self.encoding == "deflate" and not self.decompressor.eof: # type: ignore[union-attr] + raise ContentEncodingError("deflate") + + self.out.feed_eof() + + def begin_http_chunk_receiving(self) -> None: + self.out.begin_http_chunk_receiving() + + def end_http_chunk_receiving(self) -> None: + self.out.end_http_chunk_receiving() + + +HttpRequestParserPy = HttpRequestParser +HttpResponseParserPy = HttpResponseParser +RawRequestMessagePy = RawRequestMessage +RawResponseMessagePy = RawResponseMessage + +try: + if not NO_EXTENSIONS: + from ._http_parser import ( # type: ignore[import,no-redef] + HttpRequestParser, + HttpResponseParser, + RawRequestMessage, + RawResponseMessage, + ) + + HttpRequestParserC = HttpRequestParser + HttpResponseParserC = HttpResponseParser + RawRequestMessageC = RawRequestMessage + RawResponseMessageC = RawResponseMessage +except ImportError: # pragma: no cover + pass diff --git a/aiohttp/http_websocket.py b/aiohttp/http_websocket.py new file mode 100644 index 0000000..deb8ab9 --- /dev/null +++ b/aiohttp/http_websocket.py @@ -0,0 +1,710 @@ +"""WebSocket protocol versions 13 and 8.""" + +import asyncio +import functools +import json +import random +import re +import sys +import zlib +from enum import IntEnum +from struct import Struct +from typing import ( + Any, + Callable, + Final, + List, + NamedTuple, + Optional, + Pattern, + Set, + Tuple, + Union, + cast, +) + +from .base_protocol import BaseProtocol +from .compression_utils import ZLibCompressor, ZLibDecompressor +from .helpers import NO_EXTENSIONS +from .streams import DataQueue + +__all__ = ( + "WS_CLOSED_MESSAGE", + "WS_CLOSING_MESSAGE", + "WS_KEY", + "WebSocketReader", + "WebSocketWriter", + "WSMessage", + "WebSocketError", + "WSMsgType", + "WSCloseCode", +) + + +class WSCloseCode(IntEnum): + OK = 1000 + GOING_AWAY = 1001 + PROTOCOL_ERROR = 1002 + UNSUPPORTED_DATA = 1003 + ABNORMAL_CLOSURE = 1006 + INVALID_TEXT = 1007 + POLICY_VIOLATION = 1008 + MESSAGE_TOO_BIG = 1009 + MANDATORY_EXTENSION = 1010 + INTERNAL_ERROR = 1011 + SERVICE_RESTART = 1012 + TRY_AGAIN_LATER = 1013 + BAD_GATEWAY = 1014 + + +ALLOWED_CLOSE_CODES: Final[Set[int]] = {int(i) for i in WSCloseCode} + + +class WSMsgType(IntEnum): + # websocket spec types + CONTINUATION = 0x0 + TEXT = 0x1 + BINARY = 0x2 + PING = 0x9 + PONG = 0xA + CLOSE = 0x8 + + # aiohttp specific types + CLOSING = 0x100 + CLOSED = 0x101 + ERROR = 0x102 + + +WS_KEY: Final[bytes] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + +UNPACK_LEN2 = Struct("!H").unpack_from +UNPACK_LEN3 = Struct("!Q").unpack_from +UNPACK_CLOSE_CODE = Struct("!H").unpack +PACK_LEN1 = Struct("!BB").pack +PACK_LEN2 = Struct("!BBH").pack +PACK_LEN3 = Struct("!BBQ").pack +PACK_CLOSE_CODE = Struct("!H").pack +MSG_SIZE: Final[int] = 2**14 +DEFAULT_LIMIT: Final[int] = 2**16 + + +class WSMessage(NamedTuple): + type: WSMsgType + # To type correctly, this would need some kind of tagged union for each type. + data: Any + extra: Optional[str] + + def json(self, *, loads: Callable[[Any], Any] = json.loads) -> Any: + """Return parsed JSON data. + + .. versionadded:: 0.22 + """ + return loads(self.data) + + +WS_CLOSED_MESSAGE = WSMessage(WSMsgType.CLOSED, None, None) +WS_CLOSING_MESSAGE = WSMessage(WSMsgType.CLOSING, None, None) + + +class WebSocketError(Exception): + """WebSocket protocol parser error.""" + + def __init__(self, code: int, message: str) -> None: + self.code = code + super().__init__(code, message) + + def __str__(self) -> str: + return cast(str, self.args[1]) + + +class WSHandshakeError(Exception): + """WebSocket protocol handshake error.""" + + +native_byteorder: Final[str] = sys.byteorder + + +# Used by _websocket_mask_python +@functools.lru_cache() +def _xor_table() -> List[bytes]: + return [bytes(a ^ b for a in range(256)) for b in range(256)] + + +def _websocket_mask_python(mask: bytes, data: bytearray) -> None: + """Websocket masking function. + + `mask` is a `bytes` object of length 4; `data` is a `bytearray` + object of any length. The contents of `data` are masked with `mask`, + as specified in section 5.3 of RFC 6455. + + Note that this function mutates the `data` argument. + + This pure-python implementation may be replaced by an optimized + version when available. + + """ + assert isinstance(data, bytearray), data + assert len(mask) == 4, mask + + if data: + _XOR_TABLE = _xor_table() + a, b, c, d = (_XOR_TABLE[n] for n in mask) + data[::4] = data[::4].translate(a) + data[1::4] = data[1::4].translate(b) + data[2::4] = data[2::4].translate(c) + data[3::4] = data[3::4].translate(d) + + +if NO_EXTENSIONS: # pragma: no cover + _websocket_mask = _websocket_mask_python +else: + try: + from ._websocket import _websocket_mask_cython # type: ignore[import] + + _websocket_mask = _websocket_mask_cython + except ImportError: # pragma: no cover + _websocket_mask = _websocket_mask_python + +_WS_DEFLATE_TRAILING: Final[bytes] = bytes([0x00, 0x00, 0xFF, 0xFF]) + + +_WS_EXT_RE: Final[Pattern[str]] = re.compile( + r"^(?:;\s*(?:" + r"(server_no_context_takeover)|" + r"(client_no_context_takeover)|" + r"(server_max_window_bits(?:=(\d+))?)|" + r"(client_max_window_bits(?:=(\d+))?)))*$" +) + +_WS_EXT_RE_SPLIT: Final[Pattern[str]] = re.compile(r"permessage-deflate([^,]+)?") + + +def ws_ext_parse(extstr: Optional[str], isserver: bool = False) -> Tuple[int, bool]: + if not extstr: + return 0, False + + compress = 0 + notakeover = False + for ext in _WS_EXT_RE_SPLIT.finditer(extstr): + defext = ext.group(1) + # Return compress = 15 when get `permessage-deflate` + if not defext: + compress = 15 + break + match = _WS_EXT_RE.match(defext) + if match: + compress = 15 + if isserver: + # Server never fail to detect compress handshake. + # Server does not need to send max wbit to client + if match.group(4): + compress = int(match.group(4)) + # Group3 must match if group4 matches + # Compress wbit 8 does not support in zlib + # If compress level not support, + # CONTINUE to next extension + if compress > 15 or compress < 9: + compress = 0 + continue + if match.group(1): + notakeover = True + # Ignore regex group 5 & 6 for client_max_window_bits + break + else: + if match.group(6): + compress = int(match.group(6)) + # Group5 must match if group6 matches + # Compress wbit 8 does not support in zlib + # If compress level not support, + # FAIL the parse progress + if compress > 15 or compress < 9: + raise WSHandshakeError("Invalid window size") + if match.group(2): + notakeover = True + # Ignore regex group 5 & 6 for client_max_window_bits + break + # Return Fail if client side and not match + elif not isserver: + raise WSHandshakeError("Extension for deflate not supported" + ext.group(1)) + + return compress, notakeover + + +def ws_ext_gen( + compress: int = 15, isserver: bool = False, server_notakeover: bool = False +) -> str: + # client_notakeover=False not used for server + # compress wbit 8 does not support in zlib + if compress < 9 or compress > 15: + raise ValueError( + "Compress wbits must between 9 and 15, " "zlib does not support wbits=8" + ) + enabledext = ["permessage-deflate"] + if not isserver: + enabledext.append("client_max_window_bits") + + if compress < 15: + enabledext.append("server_max_window_bits=" + str(compress)) + if server_notakeover: + enabledext.append("server_no_context_takeover") + # if client_notakeover: + # enabledext.append('client_no_context_takeover') + return "; ".join(enabledext) + + +class WSParserState(IntEnum): + READ_HEADER = 1 + READ_PAYLOAD_LENGTH = 2 + READ_PAYLOAD_MASK = 3 + READ_PAYLOAD = 4 + + +class WebSocketReader: + def __init__( + self, queue: DataQueue[WSMessage], max_msg_size: int, compress: bool = True + ) -> None: + self.queue = queue + self._max_msg_size = max_msg_size + + self._exc: Optional[BaseException] = None + self._partial = bytearray() + self._state = WSParserState.READ_HEADER + + self._opcode: Optional[int] = None + self._frame_fin = False + self._frame_opcode: Optional[int] = None + self._frame_payload = bytearray() + + self._tail = b"" + self._has_mask = False + self._frame_mask: Optional[bytes] = None + self._payload_length = 0 + self._payload_length_flag = 0 + self._compressed: Optional[bool] = None + self._decompressobj: Optional[ZLibDecompressor] = None + self._compress = compress + + def feed_eof(self) -> None: + self.queue.feed_eof() + + def feed_data(self, data: bytes) -> Tuple[bool, bytes]: + if self._exc: + return True, data + + try: + return self._feed_data(data) + except Exception as exc: + self._exc = exc + self.queue.set_exception(exc) + return True, b"" + + def _feed_data(self, data: bytes) -> Tuple[bool, bytes]: + for fin, opcode, payload, compressed in self.parse_frame(data): + if compressed and not self._decompressobj: + self._decompressobj = ZLibDecompressor(suppress_deflate_header=True) + if opcode == WSMsgType.CLOSE: + if len(payload) >= 2: + close_code = UNPACK_CLOSE_CODE(payload[:2])[0] + if close_code < 3000 and close_code not in ALLOWED_CLOSE_CODES: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + f"Invalid close code: {close_code}", + ) + try: + close_message = payload[2:].decode("utf-8") + except UnicodeDecodeError as exc: + raise WebSocketError( + WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message" + ) from exc + msg = WSMessage(WSMsgType.CLOSE, close_code, close_message) + elif payload: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + f"Invalid close frame: {fin} {opcode} {payload!r}", + ) + else: + msg = WSMessage(WSMsgType.CLOSE, 0, "") + + self.queue.feed_data(msg, 0) + + elif opcode == WSMsgType.PING: + self.queue.feed_data( + WSMessage(WSMsgType.PING, payload, ""), len(payload) + ) + + elif opcode == WSMsgType.PONG: + self.queue.feed_data( + WSMessage(WSMsgType.PONG, payload, ""), len(payload) + ) + + elif ( + opcode not in (WSMsgType.TEXT, WSMsgType.BINARY) + and self._opcode is None + ): + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, f"Unexpected opcode={opcode!r}" + ) + else: + # load text/binary + if not fin: + # got partial frame payload + if opcode != WSMsgType.CONTINUATION: + self._opcode = opcode + self._partial.extend(payload) + if self._max_msg_size and len(self._partial) >= self._max_msg_size: + raise WebSocketError( + WSCloseCode.MESSAGE_TOO_BIG, + "Message size {} exceeds limit {}".format( + len(self._partial), self._max_msg_size + ), + ) + else: + # previous frame was non finished + # we should get continuation opcode + if self._partial: + if opcode != WSMsgType.CONTINUATION: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "The opcode in non-fin frame is expected " + "to be zero, got {!r}".format(opcode), + ) + + if opcode == WSMsgType.CONTINUATION: + assert self._opcode is not None + opcode = self._opcode + self._opcode = None + + self._partial.extend(payload) + if self._max_msg_size and len(self._partial) >= self._max_msg_size: + raise WebSocketError( + WSCloseCode.MESSAGE_TOO_BIG, + "Message size {} exceeds limit {}".format( + len(self._partial), self._max_msg_size + ), + ) + + # Decompress process must to be done after all packets + # received. + if compressed: + assert self._decompressobj is not None + self._partial.extend(_WS_DEFLATE_TRAILING) + payload_merged = self._decompressobj.decompress_sync( + self._partial, self._max_msg_size + ) + if self._decompressobj.unconsumed_tail: + left = len(self._decompressobj.unconsumed_tail) + raise WebSocketError( + WSCloseCode.MESSAGE_TOO_BIG, + "Decompressed message size {} exceeds limit {}".format( + self._max_msg_size + left, self._max_msg_size + ), + ) + else: + payload_merged = bytes(self._partial) + + self._partial.clear() + + if opcode == WSMsgType.TEXT: + try: + text = payload_merged.decode("utf-8") + self.queue.feed_data( + WSMessage(WSMsgType.TEXT, text, ""), len(text) + ) + except UnicodeDecodeError as exc: + raise WebSocketError( + WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message" + ) from exc + else: + self.queue.feed_data( + WSMessage(WSMsgType.BINARY, payload_merged, ""), + len(payload_merged), + ) + + return False, b"" + + def parse_frame( + self, buf: bytes + ) -> List[Tuple[bool, Optional[int], bytearray, Optional[bool]]]: + """Return the next frame from the socket.""" + frames = [] + if self._tail: + buf, self._tail = self._tail + buf, b"" + + start_pos = 0 + buf_length = len(buf) + + while True: + # read header + if self._state == WSParserState.READ_HEADER: + if buf_length - start_pos >= 2: + data = buf[start_pos : start_pos + 2] + start_pos += 2 + first_byte, second_byte = data + + fin = (first_byte >> 7) & 1 + rsv1 = (first_byte >> 6) & 1 + rsv2 = (first_byte >> 5) & 1 + rsv3 = (first_byte >> 4) & 1 + opcode = first_byte & 0xF + + # frame-fin = %x0 ; more frames of this message follow + # / %x1 ; final frame of this message + # frame-rsv1 = %x0 ; + # 1 bit, MUST be 0 unless negotiated otherwise + # frame-rsv2 = %x0 ; + # 1 bit, MUST be 0 unless negotiated otherwise + # frame-rsv3 = %x0 ; + # 1 bit, MUST be 0 unless negotiated otherwise + # + # Remove rsv1 from this test for deflate development + if rsv2 or rsv3 or (rsv1 and not self._compress): + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Received frame with non-zero reserved bits", + ) + + if opcode > 0x7 and fin == 0: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Received fragmented control frame", + ) + + has_mask = (second_byte >> 7) & 1 + length = second_byte & 0x7F + + # Control frames MUST have a payload + # length of 125 bytes or less + if opcode > 0x7 and length > 125: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Control frame payload cannot be " "larger than 125 bytes", + ) + + # Set compress status if last package is FIN + # OR set compress status if this is first fragment + # Raise error if not first fragment with rsv1 = 0x1 + if self._frame_fin or self._compressed is None: + self._compressed = True if rsv1 else False + elif rsv1: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Received frame with non-zero reserved bits", + ) + + self._frame_fin = bool(fin) + self._frame_opcode = opcode + self._has_mask = bool(has_mask) + self._payload_length_flag = length + self._state = WSParserState.READ_PAYLOAD_LENGTH + else: + break + + # read payload length + if self._state == WSParserState.READ_PAYLOAD_LENGTH: + length = self._payload_length_flag + if length == 126: + if buf_length - start_pos >= 2: + data = buf[start_pos : start_pos + 2] + start_pos += 2 + length = UNPACK_LEN2(data)[0] + self._payload_length = length + self._state = ( + WSParserState.READ_PAYLOAD_MASK + if self._has_mask + else WSParserState.READ_PAYLOAD + ) + else: + break + elif length > 126: + if buf_length - start_pos >= 8: + data = buf[start_pos : start_pos + 8] + start_pos += 8 + length = UNPACK_LEN3(data)[0] + self._payload_length = length + self._state = ( + WSParserState.READ_PAYLOAD_MASK + if self._has_mask + else WSParserState.READ_PAYLOAD + ) + else: + break + else: + self._payload_length = length + self._state = ( + WSParserState.READ_PAYLOAD_MASK + if self._has_mask + else WSParserState.READ_PAYLOAD + ) + + # read payload mask + if self._state == WSParserState.READ_PAYLOAD_MASK: + if buf_length - start_pos >= 4: + self._frame_mask = buf[start_pos : start_pos + 4] + start_pos += 4 + self._state = WSParserState.READ_PAYLOAD + else: + break + + if self._state == WSParserState.READ_PAYLOAD: + length = self._payload_length + payload = self._frame_payload + + chunk_len = buf_length - start_pos + if length >= chunk_len: + self._payload_length = length - chunk_len + payload.extend(buf[start_pos:]) + start_pos = buf_length + else: + self._payload_length = 0 + payload.extend(buf[start_pos : start_pos + length]) + start_pos = start_pos + length + + if self._payload_length == 0: + if self._has_mask: + assert self._frame_mask is not None + _websocket_mask(self._frame_mask, payload) + + frames.append( + (self._frame_fin, self._frame_opcode, payload, self._compressed) + ) + + self._frame_payload = bytearray() + self._state = WSParserState.READ_HEADER + else: + break + + self._tail = buf[start_pos:] + + return frames + + +class WebSocketWriter: + def __init__( + self, + protocol: BaseProtocol, + transport: asyncio.Transport, + *, + use_mask: bool = False, + limit: int = DEFAULT_LIMIT, + random: Any = random.Random(), + compress: int = 0, + notakeover: bool = False, + ) -> None: + self.protocol = protocol + self.transport = transport + self.use_mask = use_mask + self.randrange = random.randrange + self.compress = compress + self.notakeover = notakeover + self._closing = False + self._limit = limit + self._output_size = 0 + self._compressobj: Any = None # actually compressobj + + async def _send_frame( + self, message: bytes, opcode: int, compress: Optional[int] = None + ) -> None: + """Send a frame over the websocket with message as its payload.""" + if self._closing and not (opcode & WSMsgType.CLOSE): + raise ConnectionResetError("Cannot write to closing transport") + + rsv = 0 + + # Only compress larger packets (disabled) + # Does small packet needs to be compressed? + # if self.compress and opcode < 8 and len(message) > 124: + if (compress or self.compress) and opcode < 8: + if compress: + # Do not set self._compress if compressing is for this frame + compressobj = ZLibCompressor(level=zlib.Z_BEST_SPEED, wbits=-compress) + else: # self.compress + if not self._compressobj: + self._compressobj = ZLibCompressor( + level=zlib.Z_BEST_SPEED, wbits=-self.compress + ) + compressobj = self._compressobj + + message = await compressobj.compress(message) + message += compressobj.flush( + zlib.Z_FULL_FLUSH if self.notakeover else zlib.Z_SYNC_FLUSH + ) + if message.endswith(_WS_DEFLATE_TRAILING): + message = message[:-4] + rsv = rsv | 0x40 + + msg_length = len(message) + + use_mask = self.use_mask + if use_mask: + mask_bit = 0x80 + else: + mask_bit = 0 + + if msg_length < 126: + header = PACK_LEN1(0x80 | rsv | opcode, msg_length | mask_bit) + elif msg_length < (1 << 16): + header = PACK_LEN2(0x80 | rsv | opcode, 126 | mask_bit, msg_length) + else: + header = PACK_LEN3(0x80 | rsv | opcode, 127 | mask_bit, msg_length) + if use_mask: + mask = self.randrange(0, 0xFFFFFFFF) + mask = mask.to_bytes(4, "big") + message = bytearray(message) + _websocket_mask(mask, message) + self._write(header + mask + message) + self._output_size += len(header) + len(mask) + len(message) + else: + if len(message) > MSG_SIZE: + self._write(header) + self._write(message) + else: + self._write(header + message) + + self._output_size += len(header) + len(message) + + if self._output_size > self._limit: + self._output_size = 0 + await self.protocol._drain_helper() + + def _write(self, data: bytes) -> None: + if self.transport.is_closing(): + raise ConnectionResetError("Cannot write to closing transport") + self.transport.write(data) + + async def pong(self, message: Union[bytes, str] = b"") -> None: + """Send pong message.""" + if isinstance(message, str): + message = message.encode("utf-8") + await self._send_frame(message, WSMsgType.PONG) + + async def ping(self, message: Union[bytes, str] = b"") -> None: + """Send ping message.""" + if isinstance(message, str): + message = message.encode("utf-8") + await self._send_frame(message, WSMsgType.PING) + + async def send( + self, + message: Union[str, bytes], + binary: bool = False, + compress: Optional[int] = None, + ) -> None: + """Send a frame over the websocket with message as its payload.""" + if isinstance(message, str): + message = message.encode("utf-8") + if binary: + await self._send_frame(message, WSMsgType.BINARY, compress) + else: + await self._send_frame(message, WSMsgType.TEXT, compress) + + async def close(self, code: int = 1000, message: Union[bytes, str] = b"") -> None: + """Close the websocket, sending the specified code and message.""" + if isinstance(message, str): + message = message.encode("utf-8") + try: + await self._send_frame( + PACK_CLOSE_CODE(code) + message, opcode=WSMsgType.CLOSE + ) + finally: + self._closing = True diff --git a/aiohttp/http_writer.py b/aiohttp/http_writer.py new file mode 100644 index 0000000..8f2d908 --- /dev/null +++ b/aiohttp/http_writer.py @@ -0,0 +1,198 @@ +"""Http related parsers and protocol.""" + +import asyncio +import zlib +from typing import Any, Awaitable, Callable, NamedTuple, Optional, Union # noqa + +from multidict import CIMultiDict + +from .abc import AbstractStreamWriter +from .base_protocol import BaseProtocol +from .compression_utils import ZLibCompressor +from .helpers import NO_EXTENSIONS + +__all__ = ("StreamWriter", "HttpVersion", "HttpVersion10", "HttpVersion11") + + +class HttpVersion(NamedTuple): + major: int + minor: int + + +HttpVersion10 = HttpVersion(1, 0) +HttpVersion11 = HttpVersion(1, 1) + + +_T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]] +_T_OnHeadersSent = Optional[Callable[["CIMultiDict[str]"], Awaitable[None]]] + + +class StreamWriter(AbstractStreamWriter): + def __init__( + self, + protocol: BaseProtocol, + loop: asyncio.AbstractEventLoop, + on_chunk_sent: _T_OnChunkSent = None, + on_headers_sent: _T_OnHeadersSent = None, + ) -> None: + self._protocol = protocol + + self.loop = loop + self.length = None + self.chunked = False + self.buffer_size = 0 + self.output_size = 0 + + self._eof = False + self._compress: Optional[ZLibCompressor] = None + self._drain_waiter = None + + self._on_chunk_sent: _T_OnChunkSent = on_chunk_sent + self._on_headers_sent: _T_OnHeadersSent = on_headers_sent + + @property + def transport(self) -> Optional[asyncio.Transport]: + return self._protocol.transport + + @property + def protocol(self) -> BaseProtocol: + return self._protocol + + def enable_chunking(self) -> None: + self.chunked = True + + def enable_compression( + self, encoding: str = "deflate", strategy: int = zlib.Z_DEFAULT_STRATEGY + ) -> None: + self._compress = ZLibCompressor(encoding=encoding, strategy=strategy) + + def _write(self, chunk: bytes) -> None: + size = len(chunk) + self.buffer_size += size + self.output_size += size + transport = self.transport + if not self._protocol.connected or transport is None or transport.is_closing(): + raise ConnectionResetError("Cannot write to closing transport") + transport.write(chunk) + + async def write( + self, chunk: bytes, *, drain: bool = True, LIMIT: int = 0x10000 + ) -> None: + """Writes chunk of data to a stream. + + write_eof() indicates end of stream. + writer can't be used after write_eof() method being called. + write() return drain future. + """ + if self._on_chunk_sent is not None: + await self._on_chunk_sent(chunk) + + if isinstance(chunk, memoryview): + if chunk.nbytes != len(chunk): + # just reshape it + chunk = chunk.cast("c") + + if self._compress is not None: + chunk = await self._compress.compress(chunk) + if not chunk: + return + + if self.length is not None: + chunk_len = len(chunk) + if self.length >= chunk_len: + self.length = self.length - chunk_len + else: + chunk = chunk[: self.length] + self.length = 0 + if not chunk: + return + + if chunk: + if self.chunked: + chunk_len_pre = ("%x\r\n" % len(chunk)).encode("ascii") + chunk = chunk_len_pre + chunk + b"\r\n" + + self._write(chunk) + + if self.buffer_size > LIMIT and drain: + self.buffer_size = 0 + await self.drain() + + async def write_headers( + self, status_line: str, headers: "CIMultiDict[str]" + ) -> None: + """Write request/response status and headers.""" + if self._on_headers_sent is not None: + await self._on_headers_sent(headers) + + # status + headers + buf = _serialize_headers(status_line, headers) + self._write(buf) + + async def write_eof(self, chunk: bytes = b"") -> None: + if self._eof: + return + + if chunk and self._on_chunk_sent is not None: + await self._on_chunk_sent(chunk) + + if self._compress: + if chunk: + chunk = await self._compress.compress(chunk) + + chunk += self._compress.flush() + if chunk and self.chunked: + chunk_len = ("%x\r\n" % len(chunk)).encode("ascii") + chunk = chunk_len + chunk + b"\r\n0\r\n\r\n" + else: + if self.chunked: + if chunk: + chunk_len = ("%x\r\n" % len(chunk)).encode("ascii") + chunk = chunk_len + chunk + b"\r\n0\r\n\r\n" + else: + chunk = b"0\r\n\r\n" + + if chunk: + self._write(chunk) + + await self.drain() + + self._eof = True + + async def drain(self) -> None: + """Flush the write buffer. + + The intended use is to write + + await w.write(data) + await w.drain() + """ + if self._protocol.transport is not None: + await self._protocol._drain_helper() + + +def _safe_header(string: str) -> str: + if "\r" in string or "\n" in string: + raise ValueError( + "Newline or carriage return detected in headers. " + "Potential header injection attack." + ) + return string + + +def _py_serialize_headers(status_line: str, headers: "CIMultiDict[str]") -> bytes: + headers_gen = (_safe_header(k) + ": " + _safe_header(v) for k, v in headers.items()) + line = status_line + "\r\n" + "\r\n".join(headers_gen) + "\r\n\r\n" + return line.encode("utf-8") + + +_serialize_headers = _py_serialize_headers + +try: + import aiohttp._http_writer as _http_writer # type: ignore[import] + + _c_serialize_headers = _http_writer._serialize_headers + if not NO_EXTENSIONS: + _serialize_headers = _c_serialize_headers +except ImportError: + pass diff --git a/aiohttp/locks.py b/aiohttp/locks.py new file mode 100644 index 0000000..de2dc83 --- /dev/null +++ b/aiohttp/locks.py @@ -0,0 +1,41 @@ +import asyncio +import collections +from typing import Any, Deque, Optional + + +class EventResultOrError: + """Event asyncio lock helper class. + + Wraps the Event asyncio lock allowing either to awake the + locked Tasks without any error or raising an exception. + + thanks to @vorpalsmith for the simple design. + """ + + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + self._loop = loop + self._exc: Optional[BaseException] = None + self._event = asyncio.Event() + self._waiters: Deque[asyncio.Future[Any]] = collections.deque() + + def set(self, exc: Optional[BaseException] = None) -> None: + self._exc = exc + self._event.set() + + async def wait(self) -> Any: + waiter = self._loop.create_task(self._event.wait()) + self._waiters.append(waiter) + try: + val = await waiter + finally: + self._waiters.remove(waiter) + + if self._exc is not None: + raise self._exc + + return val + + def cancel(self) -> None: + """Cancel all waiters""" + for waiter in self._waiters: + waiter.cancel() diff --git a/aiohttp/log.py b/aiohttp/log.py new file mode 100644 index 0000000..3cecea2 --- /dev/null +++ b/aiohttp/log.py @@ -0,0 +1,8 @@ +import logging + +access_logger = logging.getLogger("aiohttp.access") +client_logger = logging.getLogger("aiohttp.client") +internal_logger = logging.getLogger("aiohttp.internal") +server_logger = logging.getLogger("aiohttp.server") +web_logger = logging.getLogger("aiohttp.web") +ws_logger = logging.getLogger("aiohttp.websocket") diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py new file mode 100644 index 0000000..dd56ce0 --- /dev/null +++ b/aiohttp/multipart.py @@ -0,0 +1,1031 @@ +import base64 +import binascii +import json +import re +import uuid +import warnings +import zlib +from collections import deque +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + AsyncIterator, + Deque, + Dict, + Iterator, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + Union, + cast, +) +from urllib.parse import parse_qsl, unquote, urlencode + +from multidict import CIMultiDict, CIMultiDictProxy, MultiMapping + +from .compression_utils import ZLibCompressor, ZLibDecompressor +from .hdrs import ( + CONTENT_DISPOSITION, + CONTENT_ENCODING, + CONTENT_LENGTH, + CONTENT_TRANSFER_ENCODING, + CONTENT_TYPE, +) +from .helpers import CHAR, TOKEN, parse_mimetype, reify +from .http import HeadersParser +from .payload import ( + JsonPayload, + LookupError, + Order, + Payload, + StringPayload, + get_payload, + payload_type, +) +from .streams import StreamReader + +__all__ = ( + "MultipartReader", + "MultipartWriter", + "BodyPartReader", + "BadContentDispositionHeader", + "BadContentDispositionParam", + "parse_content_disposition", + "content_disposition_filename", +) + + +if TYPE_CHECKING: # pragma: no cover + from .client_reqrep import ClientResponse + + +class BadContentDispositionHeader(RuntimeWarning): + pass + + +class BadContentDispositionParam(RuntimeWarning): + pass + + +def parse_content_disposition( + header: Optional[str], +) -> Tuple[Optional[str], Dict[str, str]]: + def is_token(string: str) -> bool: + return bool(string) and TOKEN >= set(string) + + def is_quoted(string: str) -> bool: + return string[0] == string[-1] == '"' + + def is_rfc5987(string: str) -> bool: + return is_token(string) and string.count("'") == 2 + + def is_extended_param(string: str) -> bool: + return string.endswith("*") + + def is_continuous_param(string: str) -> bool: + pos = string.find("*") + 1 + if not pos: + return False + substring = string[pos:-1] if string.endswith("*") else string[pos:] + return substring.isdigit() + + def unescape(text: str, *, chars: str = "".join(map(re.escape, CHAR))) -> str: + return re.sub(f"\\\\([{chars}])", "\\1", text) + + if not header: + return None, {} + + disptype, *parts = header.split(";") + if not is_token(disptype): + warnings.warn(BadContentDispositionHeader(header)) + return None, {} + + params: Dict[str, str] = {} + while parts: + item = parts.pop(0) + + if "=" not in item: + warnings.warn(BadContentDispositionHeader(header)) + return None, {} + + key, value = item.split("=", 1) + key = key.lower().strip() + value = value.lstrip() + + if key in params: + warnings.warn(BadContentDispositionHeader(header)) + return None, {} + + if not is_token(key): + warnings.warn(BadContentDispositionParam(item)) + continue + + elif is_continuous_param(key): + if is_quoted(value): + value = unescape(value[1:-1]) + elif not is_token(value): + warnings.warn(BadContentDispositionParam(item)) + continue + + elif is_extended_param(key): + if is_rfc5987(value): + encoding, _, value = value.split("'", 2) + encoding = encoding or "utf-8" + else: + warnings.warn(BadContentDispositionParam(item)) + continue + + try: + value = unquote(value, encoding, "strict") + except UnicodeDecodeError: # pragma: nocover + warnings.warn(BadContentDispositionParam(item)) + continue + + else: + failed = True + if is_quoted(value): + failed = False + value = unescape(value[1:-1].lstrip("\\/")) + elif is_token(value): + failed = False + elif parts: + # maybe just ; in filename, in any case this is just + # one case fix, for proper fix we need to redesign parser + _value = f"{value};{parts[0]}" + if is_quoted(_value): + parts.pop(0) + value = unescape(_value[1:-1].lstrip("\\/")) + failed = False + + if failed: + warnings.warn(BadContentDispositionHeader(header)) + return None, {} + + params[key] = value + + return disptype.lower(), params + + +def content_disposition_filename( + params: Mapping[str, str], name: str = "filename" +) -> Optional[str]: + name_suf = "%s*" % name + if not params: + return None + elif name_suf in params: + return params[name_suf] + elif name in params: + return params[name] + else: + parts = [] + fnparams = sorted( + (key, value) for key, value in params.items() if key.startswith(name_suf) + ) + for num, (key, value) in enumerate(fnparams): + _, tail = key.split("*", 1) + if tail.endswith("*"): + tail = tail[:-1] + if tail == str(num): + parts.append(value) + else: + break + if not parts: + return None + value = "".join(parts) + if "'" in value: + encoding, _, value = value.split("'", 2) + encoding = encoding or "utf-8" + return unquote(value, encoding, "strict") + return value + + +class MultipartResponseWrapper: + """Wrapper around the MultipartReader. + + It takes care about + underlying connection and close it when it needs in. + """ + + def __init__( + self, + resp: "ClientResponse", + stream: "MultipartReader", + ) -> None: + self.resp = resp + self.stream = stream + + def __aiter__(self) -> "MultipartResponseWrapper": + return self + + async def __anext__( + self, + ) -> Union["MultipartReader", "BodyPartReader"]: + part = await self.next() + if part is None: + raise StopAsyncIteration + return part + + def at_eof(self) -> bool: + """Returns True when all response data had been read.""" + return self.resp.content.at_eof() + + async def next( + self, + ) -> Optional[Union["MultipartReader", "BodyPartReader"]]: + """Emits next multipart reader object.""" + item = await self.stream.next() + if self.stream.at_eof(): + await self.release() + return item + + async def release(self) -> None: + """Release the connection gracefully. + + All remaining content is read to the void. + """ + await self.resp.release() + + +class BodyPartReader: + """Multipart reader for single body part.""" + + chunk_size = 8192 + + def __init__( + self, + boundary: bytes, + headers: "CIMultiDictProxy[str]", + content: StreamReader, + *, + _newline: bytes = b"\r\n", + ) -> None: + self.headers = headers + self._boundary = boundary + self._newline = _newline + self._content = content + self._at_eof = False + length = self.headers.get(CONTENT_LENGTH, None) + self._length = int(length) if length is not None else None + self._read_bytes = 0 + self._unread: Deque[bytes] = deque() + self._prev_chunk: Optional[bytes] = None + self._content_eof = 0 + self._cache: Dict[str, Any] = {} + + def __aiter__(self) -> AsyncIterator["BodyPartReader"]: + return self # type: ignore[return-value] + + async def __anext__(self) -> bytes: + part = await self.next() + if part is None: + raise StopAsyncIteration + return part + + async def next(self) -> Optional[bytes]: + item = await self.read() + if not item: + return None + return item + + async def read(self, *, decode: bool = False) -> bytes: + """Reads body part data. + + decode: Decodes data following by encoding + method from Content-Encoding header. If it missed + data remains untouched + """ + if self._at_eof: + return b"" + data = bytearray() + while not self._at_eof: + data.extend(await self.read_chunk(self.chunk_size)) + if decode: + return self.decode(data) + return data + + async def read_chunk(self, size: int = chunk_size) -> bytes: + """Reads body part content chunk of the specified size. + + size: chunk size + """ + if self._at_eof: + return b"" + if self._length: + chunk = await self._read_chunk_from_length(size) + else: + chunk = await self._read_chunk_from_stream(size) + + # For the case of base64 data, we must read a fragment of size with a + # remainder of 0 by dividing by 4 for string without symbols \n or \r + encoding = self.headers.get(CONTENT_TRANSFER_ENCODING) + if encoding and encoding.lower() == "base64": + stripped_chunk = b"".join(chunk.split()) + remainder = len(stripped_chunk) % 4 + + while remainder != 0 and not self.at_eof(): + over_chunk_size = 4 - remainder + over_chunk = b"" + + if self._prev_chunk: + over_chunk = self._prev_chunk[:over_chunk_size] + self._prev_chunk = self._prev_chunk[len(over_chunk) :] + + if len(over_chunk) != over_chunk_size: + over_chunk += await self._content.read(4 - len(over_chunk)) + + if not over_chunk: + self._at_eof = True + + stripped_chunk += b"".join(over_chunk.split()) + chunk += over_chunk + remainder = len(stripped_chunk) % 4 + + self._read_bytes += len(chunk) + if self._read_bytes == self._length: + self._at_eof = True + if self._at_eof: + newline = await self._content.readline() + assert ( + newline == self._newline + ), "reader did not read all the data or it is malformed" + return chunk + + async def _read_chunk_from_length(self, size: int) -> bytes: + # Reads body part content chunk of the specified size. + # The body part must has Content-Length header with proper value. + assert self._length is not None, "Content-Length required for chunked read" + chunk_size = min(size, self._length - self._read_bytes) + chunk = await self._content.read(chunk_size) + return chunk + + async def _read_chunk_from_stream(self, size: int) -> bytes: + # Reads content chunk of body part with unknown length. + # The Content-Length header for body part is not necessary. + assert ( + size >= len(self._boundary) + 2 + ), "Chunk size must be greater or equal than boundary length + 2" + first_chunk = self._prev_chunk is None + if first_chunk: + self._prev_chunk = await self._content.read(size) + + chunk = await self._content.read(size) + self._content_eof += int(self._content.at_eof()) + assert self._content_eof < 3, "Reading after EOF" + assert self._prev_chunk is not None + window = self._prev_chunk + chunk + + intermeditate_boundary = self._newline + self._boundary + + if first_chunk: + pos = 0 + else: + pos = max(0, len(self._prev_chunk) - len(intermeditate_boundary)) + + idx = window.find(intermeditate_boundary, pos) + if idx >= 0: + # pushing boundary back to content + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + self._content.unread_data(window[idx:]) + if size > idx: + self._prev_chunk = self._prev_chunk[:idx] + chunk = window[len(self._prev_chunk) : idx] + if not chunk: + self._at_eof = True + + result = self._prev_chunk + self._prev_chunk = chunk + return result + + async def readline(self) -> bytes: + """Reads body part by line by line.""" + if self._at_eof: + return b"" + + if self._unread: + line = self._unread.popleft() + else: + line = await self._content.readline() + + if line.startswith(self._boundary): + # the very last boundary may not come with \r\n, + # so set single rules for everyone + sline = line.rstrip(b"\r\n") + boundary = self._boundary + last_boundary = self._boundary + b"--" + # ensure that we read exactly the boundary, not something alike + if sline == boundary or sline == last_boundary: + self._at_eof = True + self._unread.append(line) + return b"" + else: + next_line = await self._content.readline() + if next_line.startswith(self._boundary): + # strip newline but only once + line = line[: -len(self._newline)] + self._unread.append(next_line) + + return line + + async def release(self) -> None: + """Like read(), but reads all the data to the void.""" + if self._at_eof: + return + while not self._at_eof: + await self.read_chunk(self.chunk_size) + + async def text(self, *, encoding: Optional[str] = None) -> str: + """Like read(), but assumes that body part contains text data.""" + data = await self.read(decode=True) + # see https://www.w3.org/TR/html5/forms.html#multipart/form-data-encoding-algorithm + # and https://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html#dom-xmlhttprequest-send + encoding = encoding or self.get_charset(default="utf-8") + return data.decode(encoding) + + async def json(self, *, encoding: Optional[str] = None) -> Optional[Dict[str, Any]]: + """Like read(), but assumes that body parts contains JSON data.""" + data = await self.read(decode=True) + if not data: + return None + encoding = encoding or self.get_charset(default="utf-8") + return cast(Dict[str, Any], json.loads(data.decode(encoding))) + + async def form(self, *, encoding: Optional[str] = None) -> List[Tuple[str, str]]: + """Like read(), but assumes that body parts contain form urlencoded data.""" + data = await self.read(decode=True) + if not data: + return [] + if encoding is not None: + real_encoding = encoding + else: + real_encoding = self.get_charset(default="utf-8") + try: + decoded_data = data.rstrip().decode(real_encoding) + except UnicodeDecodeError: + raise ValueError("data cannot be decoded with %s encoding" % real_encoding) + + return parse_qsl( + decoded_data, + keep_blank_values=True, + encoding=real_encoding, + ) + + def at_eof(self) -> bool: + """Returns True if the boundary was reached or False otherwise.""" + return self._at_eof + + def decode(self, data: bytes) -> bytes: + """Decodes data. + + Decoding is done according the specified Content-Encoding + or Content-Transfer-Encoding headers value. + """ + if CONTENT_TRANSFER_ENCODING in self.headers: + data = self._decode_content_transfer(data) + if CONTENT_ENCODING in self.headers: + return self._decode_content(data) + return data + + def _decode_content(self, data: bytes) -> bytes: + encoding = self.headers.get(CONTENT_ENCODING, "").lower() + if encoding == "identity": + return data + if encoding in {"deflate", "gzip"}: + return ZLibDecompressor( + encoding=encoding, + suppress_deflate_header=True, + ).decompress_sync(data) + + raise RuntimeError(f"unknown content encoding: {encoding}") + + def _decode_content_transfer(self, data: bytes) -> bytes: + encoding = self.headers.get(CONTENT_TRANSFER_ENCODING, "").lower() + + if encoding == "base64": + return base64.b64decode(data) + elif encoding == "quoted-printable": + return binascii.a2b_qp(data) + elif encoding in ("binary", "8bit", "7bit"): + return data + else: + raise RuntimeError( + "unknown content transfer encoding: {}" "".format(encoding) + ) + + def get_charset(self, default: str) -> str: + """Returns charset parameter from Content-Type header or default.""" + ctype = self.headers.get(CONTENT_TYPE, "") + mimetype = parse_mimetype(ctype) + return mimetype.parameters.get("charset", default) + + @reify + def name(self) -> Optional[str]: + """Returns name specified in Content-Disposition header. + + If the header is missing or malformed, returns None. + """ + _, params = parse_content_disposition(self.headers.get(CONTENT_DISPOSITION)) + return content_disposition_filename(params, "name") + + @reify + def filename(self) -> Optional[str]: + """Returns filename specified in Content-Disposition header. + + Returns None if the header is missing or malformed. + """ + _, params = parse_content_disposition(self.headers.get(CONTENT_DISPOSITION)) + return content_disposition_filename(params, "filename") + + +@payload_type(BodyPartReader, order=Order.try_first) +class BodyPartReaderPayload(Payload): + def __init__(self, value: BodyPartReader, *args: Any, **kwargs: Any) -> None: + super().__init__(value, *args, **kwargs) + + params: Dict[str, str] = {} + if value.name is not None: + params["name"] = value.name + if value.filename is not None: + params["filename"] = value.filename + + if params: + self.set_content_disposition("attachment", True, **params) + + async def write(self, writer: Any) -> None: + field = self._value + chunk = await field.read_chunk(size=2**16) + while chunk: + await writer.write(field.decode(chunk)) + chunk = await field.read_chunk(size=2**16) + + +class MultipartReader: + """Multipart body reader.""" + + #: Response wrapper, used when multipart readers constructs from response. + response_wrapper_cls = MultipartResponseWrapper + #: Multipart reader class, used to handle multipart/* body parts. + #: None points to type(self) + multipart_reader_cls = None + #: Body part reader class for non multipart/* content types. + part_reader_cls = BodyPartReader + + def __init__( + self, + headers: Mapping[str, str], + content: StreamReader, + *, + _newline: bytes = b"\r\n", + ) -> None: + self.headers = headers + self._boundary = ("--" + self._get_boundary()).encode() + self._newline = _newline + self._content = content + self._last_part: Optional[Union["MultipartReader", BodyPartReader]] = None + self._at_eof = False + self._at_bof = True + self._unread: List[bytes] = [] + + def __aiter__( + self, + ) -> AsyncIterator["BodyPartReader"]: + return self # type: ignore[return-value] + + async def __anext__( + self, + ) -> Optional[Union["MultipartReader", BodyPartReader]]: + part = await self.next() + if part is None: + raise StopAsyncIteration + return part + + @classmethod + def from_response( + cls, + response: "ClientResponse", + ) -> MultipartResponseWrapper: + """Constructs reader instance from HTTP response. + + :param response: :class:`~aiohttp.client.ClientResponse` instance + """ + obj = cls.response_wrapper_cls( + response, cls(response.headers, response.content) + ) + return obj + + def at_eof(self) -> bool: + """Returns True if the final boundary was reached, false otherwise.""" + return self._at_eof + + async def next( + self, + ) -> Optional[Union["MultipartReader", BodyPartReader]]: + """Emits the next multipart body part.""" + # So, if we're at BOF, we need to skip till the boundary. + if self._at_eof: + return None + await self._maybe_release_last_part() + if self._at_bof: + await self._read_until_first_boundary() + self._at_bof = False + else: + await self._read_boundary() + if self._at_eof: # we just read the last boundary, nothing to do there + return None + self._last_part = await self.fetch_next_part() + return self._last_part + + async def release(self) -> None: + """Reads all the body parts to the void till the final boundary.""" + while not self._at_eof: + item = await self.next() + if item is None: + break + await item.release() + + async def fetch_next_part( + self, + ) -> Union["MultipartReader", BodyPartReader]: + """Returns the next body part reader.""" + headers = await self._read_headers() + return self._get_part_reader(headers) + + def _get_part_reader( + self, + headers: "CIMultiDictProxy[str]", + ) -> Union["MultipartReader", BodyPartReader]: + """Dispatches the response by the `Content-Type` header. + + Returns a suitable reader instance. + + :param dict headers: Response headers + """ + ctype = headers.get(CONTENT_TYPE, "") + mimetype = parse_mimetype(ctype) + + if mimetype.type == "multipart": + if self.multipart_reader_cls is None: + return type(self)(headers, self._content) + return self.multipart_reader_cls( + headers, self._content, _newline=self._newline + ) + else: + return self.part_reader_cls( + self._boundary, headers, self._content, _newline=self._newline + ) + + def _get_boundary(self) -> str: + mimetype = parse_mimetype(self.headers[CONTENT_TYPE]) + + assert mimetype.type == "multipart", "multipart/* content type expected" + + if "boundary" not in mimetype.parameters: + raise ValueError( + "boundary missed for Content-Type: %s" % self.headers[CONTENT_TYPE] + ) + + boundary = mimetype.parameters["boundary"] + if len(boundary) > 70: + raise ValueError("boundary %r is too long (70 chars max)" % boundary) + + return boundary + + async def _readline(self) -> bytes: + if self._unread: + return self._unread.pop() + return await self._content.readline() + + async def _read_until_first_boundary(self) -> None: + while True: + chunk = await self._readline() + if chunk == b"": + raise ValueError( + "Could not find starting boundary %r" % (self._boundary) + ) + newline = None + end_boundary = self._boundary + b"--" + if chunk.startswith(end_boundary): + _, newline = chunk.split(end_boundary, 1) + elif chunk.startswith(self._boundary): + _, newline = chunk.split(self._boundary, 1) + if newline is not None: + assert newline in (b"\r\n", b"\n"), (newline, chunk, self._boundary) + self._newline = newline + + chunk = chunk.rstrip() + if chunk == self._boundary: + return + elif chunk == end_boundary: + self._at_eof = True + return + + async def _read_boundary(self) -> None: + chunk = (await self._readline()).rstrip() + if chunk == self._boundary: + pass + elif chunk == self._boundary + b"--": + self._at_eof = True + epilogue = await self._readline() + next_line = await self._readline() + + # the epilogue is expected and then either the end of input or the + # parent multipart boundary, if the parent boundary is found then + # it should be marked as unread and handed to the parent for + # processing + if next_line[:2] == b"--": + self._unread.append(next_line) + # otherwise the request is likely missing an epilogue and both + # lines should be passed to the parent for processing + # (this handles the old behavior gracefully) + else: + self._unread.extend([next_line, epilogue]) + else: + raise ValueError(f"Invalid boundary {chunk!r}, expected {self._boundary!r}") + + async def _read_headers(self) -> "CIMultiDictProxy[str]": + lines = [b""] + while True: + chunk = await self._content.readline() + chunk = chunk.strip() + lines.append(chunk) + if not chunk: + break + parser = HeadersParser() + headers, raw_headers = parser.parse_headers(lines) + return headers + + async def _maybe_release_last_part(self) -> None: + """Ensures that the last read body part is read completely.""" + if self._last_part is not None: + if not self._last_part.at_eof(): + await self._last_part.release() + self._unread.extend(self._last_part._unread) + self._last_part = None + + +_Part = Tuple[Payload, str, str] + + +class MultipartWriter(Payload): + """Multipart body writer.""" + + def __init__(self, subtype: str = "mixed", boundary: Optional[str] = None) -> None: + boundary = boundary if boundary is not None else uuid.uuid4().hex + # The underlying Payload API demands a str (utf-8), not bytes, + # so we need to ensure we don't lose anything during conversion. + # As a result, require the boundary to be ASCII only. + # In both situations. + + try: + self._boundary = boundary.encode("ascii") + except UnicodeEncodeError: + raise ValueError("boundary should contain ASCII only chars") from None + + if len(boundary) > 70: + raise ValueError("boundary %r is too long (70 chars max)" % boundary) + + ctype = f"multipart/{subtype}; boundary={self._boundary_value}" + + super().__init__(None, content_type=ctype) + + self._parts: List[_Part] = [] + + def __enter__(self) -> "MultipartWriter": + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + pass + + def __iter__(self) -> Iterator[_Part]: + return iter(self._parts) + + def __len__(self) -> int: + return len(self._parts) + + def __bool__(self) -> bool: + return True + + _valid_tchar_regex = re.compile(rb"\A[!#$%&'*+\-.^_`|~\w]+\Z") + _invalid_qdtext_char_regex = re.compile(rb"[\x00-\x08\x0A-\x1F\x7F]") + + @property + def _boundary_value(self) -> str: + """Wrap boundary parameter value in quotes, if necessary. + + Reads self.boundary and returns a unicode string. + """ + # Refer to RFCs 7231, 7230, 5234. + # + # parameter = token "=" ( token / quoted-string ) + # token = 1*tchar + # quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE + # qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text + # obs-text = %x80-FF + # quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + # / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + # / DIGIT / ALPHA + # ; any VCHAR, except delimiters + # VCHAR = %x21-7E + value = self._boundary + if re.match(self._valid_tchar_regex, value): + return value.decode("ascii") # cannot fail + + if re.search(self._invalid_qdtext_char_regex, value): + raise ValueError("boundary value contains invalid characters") + + # escape %x5C and %x22 + quoted_value_content = value.replace(b"\\", b"\\\\") + quoted_value_content = quoted_value_content.replace(b'"', b'\\"') + + return '"' + quoted_value_content.decode("ascii") + '"' + + @property + def boundary(self) -> str: + return self._boundary.decode("ascii") + + def append(self, obj: Any, headers: Optional[MultiMapping[str]] = None) -> Payload: + if headers is None: + headers = CIMultiDict() + + if isinstance(obj, Payload): + obj.headers.update(headers) + return self.append_payload(obj) + else: + try: + payload = get_payload(obj, headers=headers) + except LookupError: + raise TypeError("Cannot create payload from %r" % obj) + else: + return self.append_payload(payload) + + def append_payload(self, payload: Payload) -> Payload: + """Adds a new body part to multipart writer.""" + # compression + encoding: Optional[str] = payload.headers.get( + CONTENT_ENCODING, + "", + ).lower() + if encoding and encoding not in ("deflate", "gzip", "identity"): + raise RuntimeError(f"unknown content encoding: {encoding}") + if encoding == "identity": + encoding = None + + # te encoding + te_encoding: Optional[str] = payload.headers.get( + CONTENT_TRANSFER_ENCODING, + "", + ).lower() + if te_encoding not in ("", "base64", "quoted-printable", "binary"): + raise RuntimeError( + "unknown content transfer encoding: {}" "".format(te_encoding) + ) + if te_encoding == "binary": + te_encoding = None + + # size + size = payload.size + if size is not None and not (encoding or te_encoding): + payload.headers[CONTENT_LENGTH] = str(size) + + self._parts.append((payload, encoding, te_encoding)) # type: ignore[arg-type] + return payload + + def append_json( + self, obj: Any, headers: Optional[MultiMapping[str]] = None + ) -> Payload: + """Helper to append JSON part.""" + if headers is None: + headers = CIMultiDict() + + return self.append_payload(JsonPayload(obj, headers=headers)) + + def append_form( + self, + obj: Union[Sequence[Tuple[str, str]], Mapping[str, str]], + headers: Optional[MultiMapping[str]] = None, + ) -> Payload: + """Helper to append form urlencoded part.""" + assert isinstance(obj, (Sequence, Mapping)) + + if headers is None: + headers = CIMultiDict() + + if isinstance(obj, Mapping): + obj = list(obj.items()) + data = urlencode(obj, doseq=True) + + return self.append_payload( + StringPayload( + data, headers=headers, content_type="application/x-www-form-urlencoded" + ) + ) + + @property + def size(self) -> Optional[int]: + """Size of the payload.""" + total = 0 + for part, encoding, te_encoding in self._parts: + if encoding or te_encoding or part.size is None: + return None + + total += int( + 2 + + len(self._boundary) + + 2 + + part.size # b'--'+self._boundary+b'\r\n' + + len(part._binary_headers) + + 2 # b'\r\n' + ) + + total += 2 + len(self._boundary) + 4 # b'--'+self._boundary+b'--\r\n' + return total + + async def write(self, writer: Any, close_boundary: bool = True) -> None: + """Write body.""" + for part, encoding, te_encoding in self._parts: + await writer.write(b"--" + self._boundary + b"\r\n") + await writer.write(part._binary_headers) + + if encoding or te_encoding: + w = MultipartPayloadWriter(writer) + if encoding: + w.enable_compression(encoding) + if te_encoding: + w.enable_encoding(te_encoding) + await part.write(w) # type: ignore[arg-type] + await w.write_eof() + else: + await part.write(writer) + + await writer.write(b"\r\n") + + if close_boundary: + await writer.write(b"--" + self._boundary + b"--\r\n") + + +class MultipartPayloadWriter: + def __init__(self, writer: Any) -> None: + self._writer = writer + self._encoding: Optional[str] = None + self._compress: Optional[ZLibCompressor] = None + self._encoding_buffer: Optional[bytearray] = None + + def enable_encoding(self, encoding: str) -> None: + if encoding == "base64": + self._encoding = encoding + self._encoding_buffer = bytearray() + elif encoding == "quoted-printable": + self._encoding = "quoted-printable" + + def enable_compression( + self, encoding: str = "deflate", strategy: int = zlib.Z_DEFAULT_STRATEGY + ) -> None: + self._compress = ZLibCompressor( + encoding=encoding, + suppress_deflate_header=True, + strategy=strategy, + ) + + async def write_eof(self) -> None: + if self._compress is not None: + chunk = self._compress.flush() + if chunk: + self._compress = None + await self.write(chunk) + + if self._encoding == "base64": + if self._encoding_buffer: + await self._writer.write(base64.b64encode(self._encoding_buffer)) + + async def write(self, chunk: bytes) -> None: + if self._compress is not None: + if chunk: + chunk = await self._compress.compress(chunk) + if not chunk: + return + + if self._encoding == "base64": + buf = self._encoding_buffer + assert buf is not None + buf.extend(chunk) + + if buf: + div, mod = divmod(len(buf), 3) + enc_chunk, self._encoding_buffer = (buf[: div * 3], buf[div * 3 :]) + if enc_chunk: + b64chunk = base64.b64encode(enc_chunk) + await self._writer.write(b64chunk) + elif self._encoding == "quoted-printable": + await self._writer.write(binascii.b2a_qp(chunk)) + else: + await self._writer.write(chunk) diff --git a/aiohttp/payload.py b/aiohttp/payload.py new file mode 100644 index 0000000..6b4a795 --- /dev/null +++ b/aiohttp/payload.py @@ -0,0 +1,458 @@ +import asyncio +import enum +import io +import json +import mimetypes +import os +import warnings +from abc import ABC, abstractmethod +from itertools import chain +from typing import ( + IO, + TYPE_CHECKING, + Any, + ByteString, + Dict, + Final, + Iterable, + Optional, + TextIO, + Tuple, + Type, + Union, +) + +from multidict import CIMultiDict + +from . import hdrs +from .abc import AbstractStreamWriter +from .helpers import ( + _SENTINEL, + content_disposition_header, + guess_filename, + parse_mimetype, + sentinel, +) +from .streams import StreamReader +from .typedefs import JSONEncoder, _CIMultiDict + +__all__ = ( + "PAYLOAD_REGISTRY", + "get_payload", + "payload_type", + "Payload", + "BytesPayload", + "StringPayload", + "IOBasePayload", + "BytesIOPayload", + "BufferedReaderPayload", + "TextIOPayload", + "StringIOPayload", + "JsonPayload", + "AsyncIterablePayload", +) + +TOO_LARGE_BYTES_BODY: Final[int] = 2**20 # 1 MB + +if TYPE_CHECKING: # pragma: no cover + from typing import List + + +class LookupError(Exception): + pass + + +class Order(str, enum.Enum): + normal = "normal" + try_first = "try_first" + try_last = "try_last" + + +def get_payload(data: Any, *args: Any, **kwargs: Any) -> "Payload": + return PAYLOAD_REGISTRY.get(data, *args, **kwargs) + + +def register_payload( + factory: Type["Payload"], type: Any, *, order: Order = Order.normal +) -> None: + PAYLOAD_REGISTRY.register(factory, type, order=order) + + +class payload_type: + def __init__(self, type: Any, *, order: Order = Order.normal) -> None: + self.type = type + self.order = order + + def __call__(self, factory: Type["Payload"]) -> Type["Payload"]: + register_payload(factory, self.type, order=self.order) + return factory + + +PayloadType = Type["Payload"] +_PayloadRegistryItem = Tuple[PayloadType, Any] + + +class PayloadRegistry: + """Payload registry. + + note: we need zope.interface for more efficient adapter search + """ + + def __init__(self) -> None: + self._first: List[_PayloadRegistryItem] = [] + self._normal: List[_PayloadRegistryItem] = [] + self._last: List[_PayloadRegistryItem] = [] + + def get( + self, + data: Any, + *args: Any, + _CHAIN: "Type[chain[_PayloadRegistryItem]]" = chain, + **kwargs: Any, + ) -> "Payload": + if isinstance(data, Payload): + return data + for factory, type in _CHAIN(self._first, self._normal, self._last): + if isinstance(data, type): + return factory(data, *args, **kwargs) + + raise LookupError() + + def register( + self, factory: PayloadType, type: Any, *, order: Order = Order.normal + ) -> None: + if order is Order.try_first: + self._first.append((factory, type)) + elif order is Order.normal: + self._normal.append((factory, type)) + elif order is Order.try_last: + self._last.append((factory, type)) + else: + raise ValueError(f"Unsupported order {order!r}") + + +class Payload(ABC): + _default_content_type: str = "application/octet-stream" + _size: Optional[int] = None + + def __init__( + self, + value: Any, + headers: Optional[ + Union[_CIMultiDict, Dict[str, str], Iterable[Tuple[str, str]]] + ] = None, + content_type: Union[None, str, _SENTINEL] = sentinel, + filename: Optional[str] = None, + encoding: Optional[str] = None, + **kwargs: Any, + ) -> None: + self._encoding = encoding + self._filename = filename + self._headers: _CIMultiDict = CIMultiDict() + self._value = value + if content_type is not sentinel and content_type is not None: + assert isinstance(content_type, str) + self._headers[hdrs.CONTENT_TYPE] = content_type + elif self._filename is not None: + content_type = mimetypes.guess_type(self._filename)[0] + if content_type is None: + content_type = self._default_content_type + self._headers[hdrs.CONTENT_TYPE] = content_type + else: + self._headers[hdrs.CONTENT_TYPE] = self._default_content_type + self._headers.update(headers or {}) + + @property + def size(self) -> Optional[int]: + """Size of the payload.""" + return self._size + + @property + def filename(self) -> Optional[str]: + """Filename of the payload.""" + return self._filename + + @property + def headers(self) -> _CIMultiDict: + """Custom item headers""" + return self._headers + + @property + def _binary_headers(self) -> bytes: + return ( + "".join([k + ": " + v + "\r\n" for k, v in self.headers.items()]).encode( + "utf-8" + ) + + b"\r\n" + ) + + @property + def encoding(self) -> Optional[str]: + """Payload encoding""" + return self._encoding + + @property + def content_type(self) -> str: + """Content type""" + return self._headers[hdrs.CONTENT_TYPE] + + def set_content_disposition( + self, + disptype: str, + quote_fields: bool = True, + _charset: str = "utf-8", + **params: Any, + ) -> None: + """Sets ``Content-Disposition`` header.""" + self._headers[hdrs.CONTENT_DISPOSITION] = content_disposition_header( + disptype, quote_fields=quote_fields, _charset=_charset, **params + ) + + @abstractmethod + async def write(self, writer: AbstractStreamWriter) -> None: + """Write payload. + + writer is an AbstractStreamWriter instance: + """ + + +class BytesPayload(Payload): + def __init__(self, value: ByteString, *args: Any, **kwargs: Any) -> None: + if not isinstance(value, (bytes, bytearray, memoryview)): + raise TypeError(f"value argument must be byte-ish, not {type(value)!r}") + + if "content_type" not in kwargs: + kwargs["content_type"] = "application/octet-stream" + + super().__init__(value, *args, **kwargs) + + if isinstance(value, memoryview): + self._size = value.nbytes + else: + self._size = len(value) + + if self._size > TOO_LARGE_BYTES_BODY: + warnings.warn( + "Sending a large body directly with raw bytes might" + " lock the event loop. You should probably pass an " + "io.BytesIO object instead", + ResourceWarning, + source=self, + ) + + async def write(self, writer: AbstractStreamWriter) -> None: + await writer.write(self._value) + + +class StringPayload(BytesPayload): + def __init__( + self, + value: str, + *args: Any, + encoding: Optional[str] = None, + content_type: Optional[str] = None, + **kwargs: Any, + ) -> None: + if encoding is None: + if content_type is None: + real_encoding = "utf-8" + content_type = "text/plain; charset=utf-8" + else: + mimetype = parse_mimetype(content_type) + real_encoding = mimetype.parameters.get("charset", "utf-8") + else: + if content_type is None: + content_type = "text/plain; charset=%s" % encoding + real_encoding = encoding + + super().__init__( + value.encode(real_encoding), + encoding=real_encoding, + content_type=content_type, + *args, + **kwargs, + ) + + +class StringIOPayload(StringPayload): + def __init__(self, value: IO[str], *args: Any, **kwargs: Any) -> None: + super().__init__(value.read(), *args, **kwargs) + + +class IOBasePayload(Payload): + _value: IO[Any] + + def __init__( + self, value: IO[Any], disposition: str = "attachment", *args: Any, **kwargs: Any + ) -> None: + if "filename" not in kwargs: + kwargs["filename"] = guess_filename(value) + + super().__init__(value, *args, **kwargs) + + if self._filename is not None and disposition is not None: + if hdrs.CONTENT_DISPOSITION not in self.headers: + self.set_content_disposition(disposition, filename=self._filename) + + async def write(self, writer: AbstractStreamWriter) -> None: + loop = asyncio.get_event_loop() + try: + chunk = await loop.run_in_executor(None, self._value.read, 2**16) + while chunk: + await writer.write(chunk) + chunk = await loop.run_in_executor(None, self._value.read, 2**16) + finally: + await loop.run_in_executor(None, self._value.close) + + +class TextIOPayload(IOBasePayload): + _value: TextIO + + def __init__( + self, + value: TextIO, + *args: Any, + encoding: Optional[str] = None, + content_type: Optional[str] = None, + **kwargs: Any, + ) -> None: + if encoding is None: + if content_type is None: + encoding = "utf-8" + content_type = "text/plain; charset=utf-8" + else: + mimetype = parse_mimetype(content_type) + encoding = mimetype.parameters.get("charset", "utf-8") + else: + if content_type is None: + content_type = "text/plain; charset=%s" % encoding + + super().__init__( + value, + content_type=content_type, + encoding=encoding, + *args, + **kwargs, + ) + + @property + def size(self) -> Optional[int]: + try: + return os.fstat(self._value.fileno()).st_size - self._value.tell() + except OSError: + return None + + async def write(self, writer: AbstractStreamWriter) -> None: + loop = asyncio.get_event_loop() + try: + chunk = await loop.run_in_executor(None, self._value.read, 2**16) + while chunk: + data = ( + chunk.encode(encoding=self._encoding) + if self._encoding + else chunk.encode() + ) + await writer.write(data) + chunk = await loop.run_in_executor(None, self._value.read, 2**16) + finally: + await loop.run_in_executor(None, self._value.close) + + +class BytesIOPayload(IOBasePayload): + @property + def size(self) -> int: + position = self._value.tell() + end = self._value.seek(0, os.SEEK_END) + self._value.seek(position) + return end - position + + +class BufferedReaderPayload(IOBasePayload): + @property + def size(self) -> Optional[int]: + try: + return os.fstat(self._value.fileno()).st_size - self._value.tell() + except OSError: + # data.fileno() is not supported, e.g. + # io.BufferedReader(io.BytesIO(b'data')) + return None + + +class JsonPayload(BytesPayload): + def __init__( + self, + value: Any, + encoding: str = "utf-8", + content_type: str = "application/json", + dumps: JSONEncoder = json.dumps, + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__( + dumps(value).encode(encoding), + content_type=content_type, + encoding=encoding, + *args, + **kwargs, + ) + + +if TYPE_CHECKING: # pragma: no cover + from typing import AsyncIterable, AsyncIterator + + _AsyncIterator = AsyncIterator[bytes] + _AsyncIterable = AsyncIterable[bytes] +else: + from collections.abc import AsyncIterable, AsyncIterator + + _AsyncIterator = AsyncIterator + _AsyncIterable = AsyncIterable + + +class AsyncIterablePayload(Payload): + _iter: Optional[_AsyncIterator] = None + + def __init__(self, value: _AsyncIterable, *args: Any, **kwargs: Any) -> None: + if not isinstance(value, AsyncIterable): + raise TypeError( + "value argument must support " + "collections.abc.AsyncIterable interface, " + "got {!r}".format(type(value)) + ) + + if "content_type" not in kwargs: + kwargs["content_type"] = "application/octet-stream" + + super().__init__(value, *args, **kwargs) + + self._iter = value.__aiter__() + + async def write(self, writer: AbstractStreamWriter) -> None: + if self._iter: + try: + # iter is not None check prevents rare cases + # when the case iterable is used twice + while True: + chunk = await self._iter.__anext__() + await writer.write(chunk) + except StopAsyncIteration: + self._iter = None + + +class StreamReaderPayload(AsyncIterablePayload): + def __init__(self, value: StreamReader, *args: Any, **kwargs: Any) -> None: + super().__init__(value.iter_any(), *args, **kwargs) + + +PAYLOAD_REGISTRY = PayloadRegistry() +PAYLOAD_REGISTRY.register(BytesPayload, (bytes, bytearray, memoryview)) +PAYLOAD_REGISTRY.register(StringPayload, str) +PAYLOAD_REGISTRY.register(StringIOPayload, io.StringIO) +PAYLOAD_REGISTRY.register(TextIOPayload, io.TextIOBase) +PAYLOAD_REGISTRY.register(BytesIOPayload, io.BytesIO) +PAYLOAD_REGISTRY.register(BufferedReaderPayload, (io.BufferedReader, io.BufferedRandom)) +PAYLOAD_REGISTRY.register(IOBasePayload, io.IOBase) +PAYLOAD_REGISTRY.register(StreamReaderPayload, StreamReader) +# try_last for giving a chance to more specialized async interables like +# multidict.BodyPartReaderPayload override the default +PAYLOAD_REGISTRY.register(AsyncIterablePayload, AsyncIterable, order=Order.try_last) diff --git a/aiohttp/py.typed b/aiohttp/py.typed new file mode 100644 index 0000000..f5642f7 --- /dev/null +++ b/aiohttp/py.typed @@ -0,0 +1 @@ +Marker diff --git a/aiohttp/pytest_plugin.py b/aiohttp/pytest_plugin.py new file mode 100644 index 0000000..8bbe46f --- /dev/null +++ b/aiohttp/pytest_plugin.py @@ -0,0 +1,363 @@ +import asyncio +import contextlib +import inspect +import warnings +from typing import Any, Awaitable, Callable, Dict, Iterator, Optional, Type, Union + +import pytest + +from aiohttp.web import Application + +from .test_utils import ( + BaseTestServer, + RawTestServer, + TestClient, + TestServer, + loop_context, + setup_test_loop, + teardown_test_loop, + unused_port as _unused_port, +) + +try: + import uvloop +except ImportError: # pragma: no cover + uvloop = None # type: ignore[assignment] + +AiohttpClient = Callable[[Union[Application, BaseTestServer]], Awaitable[TestClient]] +AiohttpRawServer = Callable[[Application], Awaitable[RawTestServer]] +AiohttpServer = Callable[[Application], Awaitable[TestServer]] + + +def pytest_addoption(parser): # type: ignore[no-untyped-def] + parser.addoption( + "--aiohttp-fast", + action="store_true", + default=False, + help="run tests faster by disabling extra checks", + ) + parser.addoption( + "--aiohttp-loop", + action="store", + default="pyloop", + help="run tests with specific loop: pyloop, uvloop or all", + ) + parser.addoption( + "--aiohttp-enable-loop-debug", + action="store_true", + default=False, + help="enable event loop debug mode", + ) + + +def pytest_fixture_setup(fixturedef): # type: ignore[no-untyped-def] + """Set up pytest fixture. + + Allow fixtures to be coroutines. Run coroutine fixtures in an event loop. + """ + func = fixturedef.func + + if inspect.isasyncgenfunction(func): + # async generator fixture + is_async_gen = True + elif asyncio.iscoroutinefunction(func): + # regular async fixture + is_async_gen = False + else: + # not an async fixture, nothing to do + return + + strip_request = False + if "request" not in fixturedef.argnames: + fixturedef.argnames += ("request",) + strip_request = True + + def wrapper(*args, **kwargs): # type: ignore[no-untyped-def] + request = kwargs["request"] + if strip_request: + del kwargs["request"] + + # if neither the fixture nor the test use the 'loop' fixture, + # 'getfixturevalue' will fail because the test is not parameterized + # (this can be removed someday if 'loop' is no longer parameterized) + if "loop" not in request.fixturenames: + raise Exception( + "Asynchronous fixtures must depend on the 'loop' fixture or " + "be used in tests depending from it." + ) + + _loop = request.getfixturevalue("loop") + + if is_async_gen: + # for async generators, we need to advance the generator once, + # then advance it again in a finalizer + gen = func(*args, **kwargs) + + def finalizer(): # type: ignore[no-untyped-def] + try: + return _loop.run_until_complete(gen.__anext__()) + except StopAsyncIteration: + pass + + request.addfinalizer(finalizer) + return _loop.run_until_complete(gen.__anext__()) + else: + return _loop.run_until_complete(func(*args, **kwargs)) + + fixturedef.func = wrapper + + +@pytest.fixture +def fast(request): # type: ignore[no-untyped-def] + """--fast config option""" + return request.config.getoption("--aiohttp-fast") + + +@pytest.fixture +def loop_debug(request): # type: ignore[no-untyped-def] + """--enable-loop-debug config option""" + return request.config.getoption("--aiohttp-enable-loop-debug") + + +@contextlib.contextmanager +def _runtime_warning_context(): # type: ignore[no-untyped-def] + """Context manager which checks for RuntimeWarnings. + + This exists specifically to + avoid "coroutine 'X' was never awaited" warnings being missed. + + If RuntimeWarnings occur in the context a RuntimeError is raised. + """ + with warnings.catch_warnings(record=True) as _warnings: + yield + rw = [ + "{w.filename}:{w.lineno}:{w.message}".format(w=w) + for w in _warnings + if w.category == RuntimeWarning + ] + if rw: + raise RuntimeError( + "{} Runtime Warning{},\n{}".format( + len(rw), "" if len(rw) == 1 else "s", "\n".join(rw) + ) + ) + + # Propagate warnings to pytest + for msg in _warnings: + warnings.showwarning( + msg.message, msg.category, msg.filename, msg.lineno, msg.file, msg.line + ) + + +@contextlib.contextmanager +def _passthrough_loop_context(loop, fast=False): # type: ignore[no-untyped-def] + """Passthrough loop context. + + Sets up and tears down a loop unless one is passed in via the loop + argument when it's passed straight through. + """ + if loop: + # loop already exists, pass it straight through + yield loop + else: + # this shadows loop_context's standard behavior + loop = setup_test_loop() + yield loop + teardown_test_loop(loop, fast=fast) + + +def pytest_pycollect_makeitem(collector, name, obj): # type: ignore[no-untyped-def] + """Fix pytest collecting for coroutines.""" + if collector.funcnamefilter(name) and asyncio.iscoroutinefunction(obj): + return list(collector._genfunctions(name, obj)) + + +def pytest_pyfunc_call(pyfuncitem): # type: ignore[no-untyped-def] + """Run coroutines in an event loop instead of a normal function call.""" + fast = pyfuncitem.config.getoption("--aiohttp-fast") + if asyncio.iscoroutinefunction(pyfuncitem.function): + existing_loop = pyfuncitem.funcargs.get( + "proactor_loop" + ) or pyfuncitem.funcargs.get("loop", None) + with _runtime_warning_context(): + with _passthrough_loop_context(existing_loop, fast=fast) as _loop: + testargs = { + arg: pyfuncitem.funcargs[arg] + for arg in pyfuncitem._fixtureinfo.argnames + } + _loop.run_until_complete(pyfuncitem.obj(**testargs)) + + return True + + +def pytest_generate_tests(metafunc): # type: ignore[no-untyped-def] + if "loop_factory" not in metafunc.fixturenames: + return + + loops = metafunc.config.option.aiohttp_loop + avail_factories: Dict[str, Type[asyncio.AbstractEventLoopPolicy]] + avail_factories = {"pyloop": asyncio.DefaultEventLoopPolicy} + + if uvloop is not None: # pragma: no cover + avail_factories["uvloop"] = uvloop.EventLoopPolicy + + if loops == "all": + loops = "pyloop,uvloop?" + + factories = {} # type: ignore[var-annotated] + for name in loops.split(","): + required = not name.endswith("?") + name = name.strip(" ?") + if name not in avail_factories: # pragma: no cover + if required: + raise ValueError( + "Unknown loop '%s', available loops: %s" + % (name, list(factories.keys())) + ) + else: + continue + factories[name] = avail_factories[name] + metafunc.parametrize( + "loop_factory", list(factories.values()), ids=list(factories.keys()) + ) + + +@pytest.fixture +def loop(loop_factory, fast, loop_debug): # type: ignore[no-untyped-def] + """Return an instance of the event loop.""" + policy = loop_factory() + asyncio.set_event_loop_policy(policy) + with loop_context(fast=fast) as _loop: + if loop_debug: + _loop.set_debug(True) # pragma: no cover + asyncio.set_event_loop(_loop) + yield _loop + + +@pytest.fixture +def proactor_loop(): # type: ignore[no-untyped-def] + policy = asyncio.WindowsProactorEventLoopPolicy() # type: ignore[attr-defined] + asyncio.set_event_loop_policy(policy) + + with loop_context(policy.new_event_loop) as _loop: + asyncio.set_event_loop(_loop) + yield _loop + + +@pytest.fixture +def aiohttp_unused_port() -> Callable[[], int]: + """Return a port that is unused on the current host.""" + return _unused_port + + +@pytest.fixture +def aiohttp_server(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpServer]: + """Factory to create a TestServer instance, given an app. + + aiohttp_server(app, **kwargs) + """ + servers = [] + + async def go(app, *, port=None, **kwargs): # type: ignore[no-untyped-def] + server = TestServer(app, port=port) + await server.start_server(**kwargs) + servers.append(server) + return server + + yield go + + async def finalize() -> None: + while servers: + await servers.pop().close() + + loop.run_until_complete(finalize()) + + +@pytest.fixture +def aiohttp_raw_server(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpRawServer]: + """Factory to create a RawTestServer instance, given a web handler. + + aiohttp_raw_server(handler, **kwargs) + """ + servers = [] + + async def go(handler, *, port=None, **kwargs): # type: ignore[no-untyped-def] + server = RawTestServer(handler, port=port) + await server.start_server(**kwargs) + servers.append(server) + return server + + yield go + + async def finalize() -> None: + while servers: + await servers.pop().close() + + loop.run_until_complete(finalize()) + + +@pytest.fixture +def aiohttp_client_cls() -> Type[TestClient]: + """ + Client class to use in ``aiohttp_client`` factory. + + Use it for passing custom ``TestClient`` implementations. + + Example:: + + class MyClient(TestClient): + async def login(self, *, user, pw): + payload = {"username": user, "password": pw} + return await self.post("/login", json=payload) + + @pytest.fixture + def aiohttp_client_cls(): + return MyClient + + def test_login(aiohttp_client): + app = web.Application() + client = await aiohttp_client(app) + await client.login(user="admin", pw="s3cr3t") + + """ + return TestClient + + +@pytest.fixture +def aiohttp_client( + loop: asyncio.AbstractEventLoop, aiohttp_client_cls: Type[TestClient] +) -> Iterator[AiohttpClient]: + """Factory to create a TestClient instance. + + aiohttp_client(app, **kwargs) + aiohttp_client(server, **kwargs) + aiohttp_client(raw_server, **kwargs) + """ + clients = [] + + async def go( + __param: Union[Application, BaseTestServer], + *, + server_kwargs: Optional[Dict[str, Any]] = None, + **kwargs: Any + ) -> TestClient: + if isinstance(__param, Application): + server_kwargs = server_kwargs or {} + server = TestServer(__param, **server_kwargs) + client = aiohttp_client_cls(server, **kwargs) + elif isinstance(__param, BaseTestServer): + client = aiohttp_client_cls(__param, **kwargs) + else: + raise ValueError("Unknown argument type: %r" % type(__param)) + + await client.start_server() + clients.append(client) + return client + + yield go + + async def finalize() -> None: + while clients: + await clients.pop().close() + + loop.run_until_complete(finalize()) diff --git a/aiohttp/resolver.py b/aiohttp/resolver.py new file mode 100644 index 0000000..d20c919 --- /dev/null +++ b/aiohttp/resolver.py @@ -0,0 +1,118 @@ +import asyncio +import socket +from typing import Any, Dict, List, Type, Union + +from .abc import AbstractResolver + +__all__ = ("ThreadedResolver", "AsyncResolver", "DefaultResolver") + +try: + import aiodns + + # aiodns_default = hasattr(aiodns.DNSResolver, 'gethostbyname') +except ImportError: # pragma: no cover + aiodns = None + +aiodns_default = False + + +class ThreadedResolver(AbstractResolver): + """Threaded resolver. + + Uses an Executor for synchronous getaddrinfo() calls. + concurrent.futures.ThreadPoolExecutor is used by default. + """ + + def __init__(self) -> None: + self._loop = asyncio.get_running_loop() + + async def resolve( + self, hostname: str, port: int = 0, family: int = socket.AF_INET + ) -> List[Dict[str, Any]]: + infos = await self._loop.getaddrinfo( + hostname, + port, + type=socket.SOCK_STREAM, + family=family, + flags=socket.AI_ADDRCONFIG, + ) + + hosts = [] + for family, _, proto, _, address in infos: + if family == socket.AF_INET6: + if len(address) < 3: + # IPv6 is not supported by Python build, + # or IPv6 is not enabled in the host + continue + if address[3]: # type: ignore[misc] + # This is essential for link-local IPv6 addresses. + # LL IPv6 is a VERY rare case. Strictly speaking, we should use + # getnameinfo() unconditionally, but performance makes sense. + host, _port = socket.getnameinfo( + address, socket.NI_NUMERICHOST | socket.NI_NUMERICSERV + ) + port = int(_port) + else: + host, port = address[:2] + else: # IPv4 + assert family == socket.AF_INET + host, port = address # type: ignore[misc] + hosts.append( + { + "hostname": hostname, + "host": host, + "port": port, + "family": family, + "proto": proto, + "flags": socket.AI_NUMERICHOST | socket.AI_NUMERICSERV, + } + ) + + return hosts + + async def close(self) -> None: + pass + + +class AsyncResolver(AbstractResolver): + """Use the `aiodns` package to make asynchronous DNS lookups""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + if aiodns is None: + raise RuntimeError("Resolver requires aiodns library") + + self._loop = asyncio.get_running_loop() + self._resolver = aiodns.DNSResolver(*args, loop=self._loop, **kwargs) + + async def resolve( + self, host: str, port: int = 0, family: int = socket.AF_INET + ) -> List[Dict[str, Any]]: + try: + resp = await self._resolver.gethostbyname(host, family) + except aiodns.error.DNSError as exc: + msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed" + raise OSError(msg) from exc + hosts = [] + for address in resp.addresses: + hosts.append( + { + "hostname": host, + "host": address, + "port": port, + "family": family, + "proto": 0, + "flags": socket.AI_NUMERICHOST | socket.AI_NUMERICSERV, + } + ) + + if not hosts: + raise OSError("DNS lookup failed") + + return hosts + + async def close(self) -> None: + self._resolver.cancel() + + +_DefaultType = Type[Union[AsyncResolver, ThreadedResolver]] +DefaultResolver: _DefaultType = AsyncResolver if aiodns_default else ThreadedResolver diff --git a/aiohttp/streams.py b/aiohttp/streams.py new file mode 100644 index 0000000..8ee088d --- /dev/null +++ b/aiohttp/streams.py @@ -0,0 +1,652 @@ +import asyncio +import collections +import warnings +from typing import ( + Awaitable, + Callable, + Deque, + Final, + Generic, + List, + Optional, + Tuple, + TypeVar, +) + +from .base_protocol import BaseProtocol +from .helpers import BaseTimerContext, TimerNoop, set_exception, set_result +from .log import internal_logger + +__all__ = ( + "EMPTY_PAYLOAD", + "EofStream", + "StreamReader", + "DataQueue", + "FlowControlDataQueue", +) + +_T = TypeVar("_T") + + +class EofStream(Exception): + """eof stream indication.""" + + +class AsyncStreamIterator(Generic[_T]): + def __init__(self, read_func: Callable[[], Awaitable[_T]]) -> None: + self.read_func = read_func + + def __aiter__(self) -> "AsyncStreamIterator[_T]": + return self + + async def __anext__(self) -> _T: + try: + rv = await self.read_func() + except EofStream: + raise StopAsyncIteration + if rv == b"": + raise StopAsyncIteration + return rv + + +class ChunkTupleAsyncStreamIterator: + def __init__(self, stream: "StreamReader") -> None: + self._stream = stream + + def __aiter__(self) -> "ChunkTupleAsyncStreamIterator": + return self + + async def __anext__(self) -> Tuple[bytes, bool]: + rv = await self._stream.readchunk() + if rv == (b"", False): + raise StopAsyncIteration + return rv + + +class AsyncStreamReaderMixin: + def __aiter__(self) -> AsyncStreamIterator[bytes]: + return AsyncStreamIterator(self.readline) # type: ignore[attr-defined] + + def iter_chunked(self, n: int) -> AsyncStreamIterator[bytes]: + """Returns an asynchronous iterator that yields chunks of size n.""" + return AsyncStreamIterator(lambda: self.read(n)) # type: ignore[attr-defined] + + def iter_any(self) -> AsyncStreamIterator[bytes]: + """Yield all available data as soon as it is received.""" + return AsyncStreamIterator(self.readany) # type: ignore[attr-defined] + + def iter_chunks(self) -> ChunkTupleAsyncStreamIterator: + """Yield chunks of data as they are received by the server. + + The yielded objects are tuples + of (bytes, bool) as returned by the StreamReader.readchunk method. + """ + return ChunkTupleAsyncStreamIterator(self) # type: ignore[arg-type] + + +class StreamReader(AsyncStreamReaderMixin): + """An enhancement of asyncio.StreamReader. + + Supports asynchronous iteration by line, chunk or as available:: + + async for line in reader: + ... + async for chunk in reader.iter_chunked(1024): + ... + async for slice in reader.iter_any(): + ... + + """ + + total_bytes = 0 + + def __init__( + self, + protocol: BaseProtocol, + limit: int, + *, + timer: Optional[BaseTimerContext] = None, + loop: asyncio.AbstractEventLoop, + ) -> None: + self._protocol = protocol + self._low_water = limit + self._high_water = limit * 2 + if loop is None: + loop = asyncio.get_event_loop() + self._loop = loop + self._size = 0 + self._cursor = 0 + self._http_chunk_splits: Optional[List[int]] = None + self._buffer: Deque[bytes] = collections.deque() + self._buffer_offset = 0 + self._eof = False + self._waiter: Optional[asyncio.Future[None]] = None + self._eof_waiter: Optional[asyncio.Future[None]] = None + self._exception: Optional[BaseException] = None + self._timer = TimerNoop() if timer is None else timer + self._eof_callbacks: List[Callable[[], None]] = [] + + def __repr__(self) -> str: + info = [self.__class__.__name__] + if self._size: + info.append("%d bytes" % self._size) + if self._eof: + info.append("eof") + if self._low_water != 2**16: # default limit + info.append("low=%d high=%d" % (self._low_water, self._high_water)) + if self._waiter: + info.append("w=%r" % self._waiter) + if self._exception: + info.append("e=%r" % self._exception) + return "<%s>" % " ".join(info) + + def get_read_buffer_limits(self) -> Tuple[int, int]: + return (self._low_water, self._high_water) + + def exception(self) -> Optional[BaseException]: + return self._exception + + def set_exception(self, exc: BaseException) -> None: + self._exception = exc + self._eof_callbacks.clear() + + waiter = self._waiter + if waiter is not None: + self._waiter = None + set_exception(waiter, exc) + + waiter = self._eof_waiter + if waiter is not None: + self._eof_waiter = None + set_exception(waiter, exc) + + def on_eof(self, callback: Callable[[], None]) -> None: + if self._eof: + try: + callback() + except Exception: + internal_logger.exception("Exception in eof callback") + else: + self._eof_callbacks.append(callback) + + def feed_eof(self) -> None: + self._eof = True + + waiter = self._waiter + if waiter is not None: + self._waiter = None + set_result(waiter, None) + + waiter = self._eof_waiter + if waiter is not None: + self._eof_waiter = None + set_result(waiter, None) + + for cb in self._eof_callbacks: + try: + cb() + except Exception: + internal_logger.exception("Exception in eof callback") + + self._eof_callbacks.clear() + + def is_eof(self) -> bool: + """Return True if 'feed_eof' was called.""" + return self._eof + + def at_eof(self) -> bool: + """Return True if the buffer is empty and 'feed_eof' was called.""" + return self._eof and not self._buffer + + async def wait_eof(self) -> None: + if self._eof: + return + + assert self._eof_waiter is None + self._eof_waiter = self._loop.create_future() + try: + await self._eof_waiter + finally: + self._eof_waiter = None + + def unread_data(self, data: bytes) -> None: + """rollback reading some data from stream, inserting it to buffer head.""" + warnings.warn( + "unread_data() is deprecated " + "and will be removed in future releases (#3260)", + DeprecationWarning, + stacklevel=2, + ) + if not data: + return + + if self._buffer_offset: + self._buffer[0] = self._buffer[0][self._buffer_offset :] + self._buffer_offset = 0 + self._size += len(data) + self._cursor -= len(data) + self._buffer.appendleft(data) + self._eof_counter = 0 + + # TODO: size is ignored, remove the param later + def feed_data(self, data: bytes, size: int = 0) -> None: + assert not self._eof, "feed_data after feed_eof" + + if not data: + return + + self._size += len(data) + self._buffer.append(data) + self.total_bytes += len(data) + + waiter = self._waiter + if waiter is not None: + self._waiter = None + set_result(waiter, None) + + if self._size > self._high_water and not self._protocol._reading_paused: + self._protocol.pause_reading() + + def begin_http_chunk_receiving(self) -> None: + if self._http_chunk_splits is None: + if self.total_bytes: + raise RuntimeError( + "Called begin_http_chunk_receiving when" "some data was already fed" + ) + self._http_chunk_splits = [] + + def end_http_chunk_receiving(self) -> None: + if self._http_chunk_splits is None: + raise RuntimeError( + "Called end_chunk_receiving without calling " + "begin_chunk_receiving first" + ) + + # self._http_chunk_splits contains logical byte offsets from start of + # the body transfer. Each offset is the offset of the end of a chunk. + # "Logical" means bytes, accessible for a user. + # If no chunks containing logical data were received, current position + # is difinitely zero. + pos = self._http_chunk_splits[-1] if self._http_chunk_splits else 0 + + if self.total_bytes == pos: + # We should not add empty chunks here. So we check for that. + # Note, when chunked + gzip is used, we can receive a chunk + # of compressed data, but that data may not be enough for gzip FSM + # to yield any uncompressed data. That's why current position may + # not change after receiving a chunk. + return + + self._http_chunk_splits.append(self.total_bytes) + + # wake up readchunk when end of http chunk received + waiter = self._waiter + if waiter is not None: + self._waiter = None + set_result(waiter, None) + + async def _wait(self, func_name: str) -> None: + # StreamReader uses a future to link the protocol feed_data() method + # to a read coroutine. Running two read coroutines at the same time + # would have an unexpected behaviour. It would not possible to know + # which coroutine would get the next data. + if self._waiter is not None: + raise RuntimeError( + "%s() called while another coroutine is " + "already waiting for incoming data" % func_name + ) + + waiter = self._waiter = self._loop.create_future() + try: + with self._timer: + await waiter + finally: + self._waiter = None + + async def readline(self) -> bytes: + return await self.readuntil() + + async def readuntil(self, separator: bytes = b"\n") -> bytes: + seplen = len(separator) + if seplen == 0: + raise ValueError("Separator should be at least one-byte string") + + if self._exception is not None: + raise self._exception + + chunk = b"" + chunk_size = 0 + not_enough = True + + while not_enough: + while self._buffer and not_enough: + offset = self._buffer_offset + ichar = self._buffer[0].find(separator, offset) + 1 + # Read from current offset to found separator or to the end. + data = self._read_nowait_chunk( + ichar - offset + seplen - 1 if ichar else -1 + ) + chunk += data + chunk_size += len(data) + if ichar: + not_enough = False + + if chunk_size > self._high_water: + raise ValueError("Chunk too big") + + if self._eof: + break + + if not_enough: + await self._wait("readuntil") + + return chunk + + async def read(self, n: int = -1) -> bytes: + if self._exception is not None: + raise self._exception + + if not n: + return b"" + + if n < 0: + # This used to just loop creating a new waiter hoping to + # collect everything in self._buffer, but that would + # deadlock if the subprocess sends more than self.limit + # bytes. So just call self.readany() until EOF. + blocks = [] + while True: + block = await self.readany() + if not block: + break + blocks.append(block) + return b"".join(blocks) + + # TODO: should be `if` instead of `while` + # because waiter maybe triggered on chunk end, + # without feeding any data + while not self._buffer and not self._eof: + await self._wait("read") + + return self._read_nowait(n) + + async def readany(self) -> bytes: + if self._exception is not None: + raise self._exception + + # TODO: should be `if` instead of `while` + # because waiter maybe triggered on chunk end, + # without feeding any data + while not self._buffer and not self._eof: + await self._wait("readany") + + return self._read_nowait(-1) + + async def readchunk(self) -> Tuple[bytes, bool]: + """Returns a tuple of (data, end_of_http_chunk). + + When chunked transfer + encoding is used, end_of_http_chunk is a boolean indicating if the end + of the data corresponds to the end of a HTTP chunk , otherwise it is + always False. + """ + while True: + if self._exception is not None: + raise self._exception + + while self._http_chunk_splits: + pos = self._http_chunk_splits.pop(0) + if pos == self._cursor: + return (b"", True) + if pos > self._cursor: + return (self._read_nowait(pos - self._cursor), True) + internal_logger.warning( + "Skipping HTTP chunk end due to data " + "consumption beyond chunk boundary" + ) + + if self._buffer: + return (self._read_nowait_chunk(-1), False) + # return (self._read_nowait(-1), False) + + if self._eof: + # Special case for signifying EOF. + # (b'', True) is not a final return value actually. + return (b"", False) + + await self._wait("readchunk") + + async def readexactly(self, n: int) -> bytes: + if self._exception is not None: + raise self._exception + + blocks: List[bytes] = [] + while n > 0: + block = await self.read(n) + if not block: + partial = b"".join(blocks) + raise asyncio.IncompleteReadError(partial, len(partial) + n) + blocks.append(block) + n -= len(block) + + return b"".join(blocks) + + def read_nowait(self, n: int = -1) -> bytes: + # default was changed to be consistent with .read(-1) + # + # I believe the most users don't know about the method and + # they are not affected. + if self._exception is not None: + raise self._exception + + if self._waiter and not self._waiter.done(): + raise RuntimeError( + "Called while some coroutine is waiting for incoming data." + ) + + return self._read_nowait(n) + + def _read_nowait_chunk(self, n: int) -> bytes: + first_buffer = self._buffer[0] + offset = self._buffer_offset + if n != -1 and len(first_buffer) - offset > n: + data = first_buffer[offset : offset + n] + self._buffer_offset += n + + elif offset: + self._buffer.popleft() + data = first_buffer[offset:] + self._buffer_offset = 0 + + else: + data = self._buffer.popleft() + + self._size -= len(data) + self._cursor += len(data) + + chunk_splits = self._http_chunk_splits + # Prevent memory leak: drop useless chunk splits + while chunk_splits and chunk_splits[0] < self._cursor: + chunk_splits.pop(0) + + if self._size < self._low_water and self._protocol._reading_paused: + self._protocol.resume_reading() + return data + + def _read_nowait(self, n: int) -> bytes: + """Read not more than n bytes, or whole buffer if n == -1""" + self._timer.assert_timeout() + + chunks = [] + while self._buffer: + chunk = self._read_nowait_chunk(n) + chunks.append(chunk) + if n != -1: + n -= len(chunk) + if n == 0: + break + + return b"".join(chunks) if chunks else b"" + + +class EmptyStreamReader(StreamReader): # lgtm [py/missing-call-to-init] + def __init__(self) -> None: + self._read_eof_chunk = False + + def __repr__(self) -> str: + return "<%s>" % self.__class__.__name__ + + def exception(self) -> Optional[BaseException]: + return None + + def set_exception(self, exc: BaseException) -> None: + pass + + def on_eof(self, callback: Callable[[], None]) -> None: + try: + callback() + except Exception: + internal_logger.exception("Exception in eof callback") + + def feed_eof(self) -> None: + pass + + def is_eof(self) -> bool: + return True + + def at_eof(self) -> bool: + return True + + async def wait_eof(self) -> None: + return + + def feed_data(self, data: bytes, n: int = 0) -> None: + pass + + async def readline(self) -> bytes: + return b"" + + async def read(self, n: int = -1) -> bytes: + return b"" + + # TODO add async def readuntil + + async def readany(self) -> bytes: + return b"" + + async def readchunk(self) -> Tuple[bytes, bool]: + if not self._read_eof_chunk: + self._read_eof_chunk = True + return (b"", False) + + return (b"", True) + + async def readexactly(self, n: int) -> bytes: + raise asyncio.IncompleteReadError(b"", n) + + def read_nowait(self, n: int = -1) -> bytes: + return b"" + + +EMPTY_PAYLOAD: Final[StreamReader] = EmptyStreamReader() + + +class DataQueue(Generic[_T]): + """DataQueue is a general-purpose blocking queue with one reader.""" + + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + self._loop = loop + self._eof = False + self._waiter: Optional[asyncio.Future[None]] = None + self._exception: Optional[BaseException] = None + self._size = 0 + self._buffer: Deque[Tuple[_T, int]] = collections.deque() + + def __len__(self) -> int: + return len(self._buffer) + + def is_eof(self) -> bool: + return self._eof + + def at_eof(self) -> bool: + return self._eof and not self._buffer + + def exception(self) -> Optional[BaseException]: + return self._exception + + def set_exception(self, exc: BaseException) -> None: + self._eof = True + self._exception = exc + + waiter = self._waiter + if waiter is not None: + self._waiter = None + set_exception(waiter, exc) + + def feed_data(self, data: _T, size: int = 0) -> None: + self._size += size + self._buffer.append((data, size)) + + waiter = self._waiter + if waiter is not None: + self._waiter = None + set_result(waiter, None) + + def feed_eof(self) -> None: + self._eof = True + + waiter = self._waiter + if waiter is not None: + self._waiter = None + set_result(waiter, None) + + async def read(self) -> _T: + if not self._buffer and not self._eof: + assert not self._waiter + self._waiter = self._loop.create_future() + try: + await self._waiter + except (asyncio.CancelledError, asyncio.TimeoutError): + self._waiter = None + raise + + if self._buffer: + data, size = self._buffer.popleft() + self._size -= size + return data + else: + if self._exception is not None: + raise self._exception + else: + raise EofStream + + def __aiter__(self) -> AsyncStreamIterator[_T]: + return AsyncStreamIterator(self.read) + + +class FlowControlDataQueue(DataQueue[_T]): + """FlowControlDataQueue resumes and pauses an underlying stream. + + It is a destination for parsed data. + """ + + def __init__( + self, protocol: BaseProtocol, limit: int, *, loop: asyncio.AbstractEventLoop + ) -> None: + super().__init__(loop=loop) + + self._protocol = protocol + self._limit = limit * 2 + + def feed_data(self, data: _T, size: int = 0) -> None: + super().feed_data(data, size) + + if self._size > self._limit and not self._protocol._reading_paused: + self._protocol.pause_reading() + + async def read(self) -> _T: + try: + return await super().read() + finally: + if self._size < self._limit and self._protocol._reading_paused: + self._protocol.resume_reading() diff --git a/aiohttp/tcp_helpers.py b/aiohttp/tcp_helpers.py new file mode 100644 index 0000000..88b2442 --- /dev/null +++ b/aiohttp/tcp_helpers.py @@ -0,0 +1,37 @@ +"""Helper methods to tune a TCP connection""" + +import asyncio +import socket +from contextlib import suppress +from typing import Optional # noqa + +__all__ = ("tcp_keepalive", "tcp_nodelay") + + +if hasattr(socket, "SO_KEEPALIVE"): + + def tcp_keepalive(transport: asyncio.Transport) -> None: + sock = transport.get_extra_info("socket") + if sock is not None: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + +else: + + def tcp_keepalive(transport: asyncio.Transport) -> None: # pragma: no cover + pass + + +def tcp_nodelay(transport: asyncio.Transport, value: bool) -> None: + sock = transport.get_extra_info("socket") + + if sock is None: + return + + if sock.family not in (socket.AF_INET, socket.AF_INET6): + return + + value = bool(value) + + # socket may be closed already, on windows OSError get raised + with suppress(OSError): + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, value) diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py new file mode 100644 index 0000000..414ae11 --- /dev/null +++ b/aiohttp/test_utils.py @@ -0,0 +1,618 @@ +"""Utilities shared by tests.""" + +import asyncio +import contextlib +import gc +import inspect +import ipaddress +import os +import socket +import sys +from abc import ABC, abstractmethod +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Iterator, + List, + Optional, + Type, + Union, + cast, +) +from unittest import IsolatedAsyncioTestCase, mock + +from aiosignal import Signal +from multidict import CIMultiDict, CIMultiDictProxy +from yarl import URL + +import aiohttp +from aiohttp.client import _RequestContextManager, _WSRequestContextManager + +from . import ClientSession, hdrs +from .abc import AbstractCookieJar +from .client_reqrep import ClientResponse +from .client_ws import ClientWebSocketResponse +from .helpers import _SENTINEL, sentinel +from .http import HttpVersion, RawRequestMessage +from .typedefs import StrOrURL +from .web import ( + Application, + AppRunner, + BaseRunner, + Request, + Server, + ServerRunner, + SockSite, + UrlMappingMatchInfo, +) +from .web_protocol import _RequestHandler + +if TYPE_CHECKING: # pragma: no cover + from ssl import SSLContext +else: + SSLContext = None + +REUSE_ADDRESS = os.name == "posix" and sys.platform != "cygwin" + + +def get_unused_port_socket( + host: str, family: socket.AddressFamily = socket.AF_INET +) -> socket.socket: + return get_port_socket(host, 0, family) + + +def get_port_socket( + host: str, port: int, family: socket.AddressFamily = socket.AF_INET +) -> socket.socket: + s = socket.socket(family, socket.SOCK_STREAM) + if REUSE_ADDRESS: + # Windows has different semantics for SO_REUSEADDR, + # so don't set it. Ref: + # https://docs.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host, port)) + return s + + +def unused_port() -> int: + """Return a port that is unused on the current host.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return cast(int, s.getsockname()[1]) + + +class BaseTestServer(ABC): + __test__ = False + + def __init__( + self, + *, + scheme: Union[str, _SENTINEL] = sentinel, + host: str = "127.0.0.1", + port: Optional[int] = None, + skip_url_asserts: bool = False, + socket_factory: Callable[ + [str, int, socket.AddressFamily], socket.socket + ] = get_port_socket, + **kwargs: Any, + ) -> None: + self.runner: Optional[BaseRunner] = None + self._root: Optional[URL] = None + self.host = host + self.port = port + self._closed = False + self.scheme = scheme + self.skip_url_asserts = skip_url_asserts + self.socket_factory = socket_factory + + async def start_server(self, **kwargs: Any) -> None: + if self.runner: + return + self._ssl = kwargs.pop("ssl", None) + self.runner = await self._make_runner(handler_cancellation=True, **kwargs) + await self.runner.setup() + if not self.port: + self.port = 0 + absolute_host = self.host + try: + version = ipaddress.ip_address(self.host).version + except ValueError: + version = 4 + if version == 6: + absolute_host = f"[{self.host}]" + family = socket.AF_INET6 if version == 6 else socket.AF_INET + _sock = self.socket_factory(self.host, self.port, family) + self.host, self.port = _sock.getsockname()[:2] + site = SockSite(self.runner, sock=_sock, ssl_context=self._ssl) + await site.start() + server = site._server + assert server is not None + sockets = server.sockets # type: ignore[attr-defined] + assert sockets is not None + self.port = sockets[0].getsockname()[1] + if self.scheme is sentinel: + if self._ssl: + scheme = "https" + else: + scheme = "http" + self.scheme = scheme + self._root = URL(f"{self.scheme}://{absolute_host}:{self.port}") + + @abstractmethod # pragma: no cover + async def _make_runner(self, **kwargs: Any) -> BaseRunner: + pass + + def make_url(self, path: StrOrURL) -> URL: + assert self._root is not None + url = URL(path) + if not self.skip_url_asserts: + assert not url.is_absolute() + return self._root.join(url) + else: + return URL(str(self._root) + str(path)) + + @property + def started(self) -> bool: + return self.runner is not None + + @property + def closed(self) -> bool: + return self._closed + + @property + def handler(self) -> Server: + # for backward compatibility + # web.Server instance + runner = self.runner + assert runner is not None + assert runner.server is not None + return runner.server + + async def close(self) -> None: + """Close all fixtures created by the test client. + + After that point, the TestClient is no longer usable. + + This is an idempotent function: running close multiple times + will not have any additional effects. + + close is also run when the object is garbage collected, and on + exit when used as a context manager. + + """ + if self.started and not self.closed: + assert self.runner is not None + await self.runner.cleanup() + self._root = None + self.port = None + self._closed = True + + async def __aenter__(self) -> "BaseTestServer": + await self.start_server() + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + await self.close() + + +class TestServer(BaseTestServer): + def __init__( + self, + app: Application, + *, + scheme: Union[str, _SENTINEL] = sentinel, + host: str = "127.0.0.1", + port: Optional[int] = None, + **kwargs: Any, + ): + self.app = app + super().__init__(scheme=scheme, host=host, port=port, **kwargs) + + async def _make_runner(self, **kwargs: Any) -> BaseRunner: + return AppRunner(self.app, **kwargs) + + +class RawTestServer(BaseTestServer): + def __init__( + self, + handler: _RequestHandler, + *, + scheme: Union[str, _SENTINEL] = sentinel, + host: str = "127.0.0.1", + port: Optional[int] = None, + **kwargs: Any, + ) -> None: + self._handler = handler + super().__init__(scheme=scheme, host=host, port=port, **kwargs) + + async def _make_runner(self, **kwargs: Any) -> ServerRunner: + srv = Server(self._handler, **kwargs) + return ServerRunner(srv, **kwargs) + + +class TestClient: + """ + A test client implementation. + + To write functional tests for aiohttp based servers. + + """ + + __test__ = False + + def __init__( + self, + server: BaseTestServer, + *, + cookie_jar: Optional[AbstractCookieJar] = None, + **kwargs: Any, + ) -> None: + if not isinstance(server, BaseTestServer): + raise TypeError( + "server must be TestServer " "instance, found type: %r" % type(server) + ) + self._server = server + if cookie_jar is None: + cookie_jar = aiohttp.CookieJar(unsafe=True) + self._session = ClientSession(cookie_jar=cookie_jar, **kwargs) + self._closed = False + self._responses: List[ClientResponse] = [] + self._websockets: List[ClientWebSocketResponse] = [] + + async def start_server(self) -> None: + await self._server.start_server() + + @property + def scheme(self) -> Union[str, object]: + return self._server.scheme + + @property + def host(self) -> str: + return self._server.host + + @property + def port(self) -> Optional[int]: + return self._server.port + + @property + def server(self) -> BaseTestServer: + return self._server + + @property + def app(self) -> Optional[Application]: + return cast(Optional[Application], getattr(self._server, "app", None)) + + @property + def session(self) -> ClientSession: + """An internal aiohttp.ClientSession. + + Unlike the methods on the TestClient, client session requests + do not automatically include the host in the url queried, and + will require an absolute path to the resource. + + """ + return self._session + + def make_url(self, path: StrOrURL) -> URL: + return self._server.make_url(path) + + async def _request( + self, method: str, path: StrOrURL, **kwargs: Any + ) -> ClientResponse: + resp = await self._session.request(method, self.make_url(path), **kwargs) + # save it to close later + self._responses.append(resp) + return resp + + def request( + self, method: str, path: StrOrURL, **kwargs: Any + ) -> _RequestContextManager: + """Routes a request to tested http server. + + The interface is identical to aiohttp.ClientSession.request, + except the loop kwarg is overridden by the instance used by the + test server. + + """ + return _RequestContextManager(self._request(method, path, **kwargs)) + + def get(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP GET request.""" + return _RequestContextManager(self._request(hdrs.METH_GET, path, **kwargs)) + + def post(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP POST request.""" + return _RequestContextManager(self._request(hdrs.METH_POST, path, **kwargs)) + + def options(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP OPTIONS request.""" + return _RequestContextManager(self._request(hdrs.METH_OPTIONS, path, **kwargs)) + + def head(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP HEAD request.""" + return _RequestContextManager(self._request(hdrs.METH_HEAD, path, **kwargs)) + + def put(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP PUT request.""" + return _RequestContextManager(self._request(hdrs.METH_PUT, path, **kwargs)) + + def patch(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP PATCH request.""" + return _RequestContextManager(self._request(hdrs.METH_PATCH, path, **kwargs)) + + def delete(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP PATCH request.""" + return _RequestContextManager(self._request(hdrs.METH_DELETE, path, **kwargs)) + + def ws_connect(self, path: StrOrURL, **kwargs: Any) -> _WSRequestContextManager: + """Initiate websocket connection. + + The api corresponds to aiohttp.ClientSession.ws_connect. + + """ + return _WSRequestContextManager(self._ws_connect(path, **kwargs)) + + async def _ws_connect( + self, path: StrOrURL, **kwargs: Any + ) -> ClientWebSocketResponse: + ws = await self._session.ws_connect(self.make_url(path), **kwargs) + self._websockets.append(ws) + return ws + + async def close(self) -> None: + """Close all fixtures created by the test client. + + After that point, the TestClient is no longer usable. + + This is an idempotent function: running close multiple times + will not have any additional effects. + + close is also run on exit when used as a(n) (asynchronous) + context manager. + + """ + if not self._closed: + for resp in self._responses: + resp.close() + for ws in self._websockets: + await ws.close() + await self._session.close() + await self._server.close() + self._closed = True + + async def __aenter__(self) -> "TestClient": + await self.start_server() + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + tb: Optional[TracebackType], + ) -> None: + await self.close() + + +class AioHTTPTestCase(IsolatedAsyncioTestCase, ABC): + """A base class to allow for unittest web applications using aiohttp. + + Provides the following: + + * self.client (aiohttp.test_utils.TestClient): an aiohttp test client. + * self.app (aiohttp.web.Application): the application returned by + self.get_application() + + Note that the TestClient's methods are asynchronous: you have to + execute function on the test client using asynchronous methods. + """ + + @abstractmethod + async def get_application(self) -> Application: + """Get application. + + This method should be overridden to return the aiohttp.web.Application + object to test. + """ + + async def asyncSetUp(self) -> None: + self.app = await self.get_application() + self.server = await self.get_server(self.app) + self.client = await self.get_client(self.server) + + await self.client.start_server() + + async def asyncTearDown(self) -> None: + await self.client.close() + + async def get_server(self, app: Application) -> TestServer: + """Return a TestServer instance.""" + return TestServer(app) + + async def get_client(self, server: TestServer) -> TestClient: + """Return a TestClient instance.""" + return TestClient(server) + + +_LOOP_FACTORY = Callable[[], asyncio.AbstractEventLoop] + + +@contextlib.contextmanager +def loop_context( + loop_factory: _LOOP_FACTORY = asyncio.new_event_loop, fast: bool = False +) -> Iterator[asyncio.AbstractEventLoop]: + """A contextmanager that creates an event_loop, for test purposes. + + Handles the creation and cleanup of a test loop. + """ + loop = setup_test_loop(loop_factory) + yield loop + teardown_test_loop(loop, fast=fast) + + +def setup_test_loop( + loop_factory: _LOOP_FACTORY = asyncio.new_event_loop, +) -> asyncio.AbstractEventLoop: + """Create and return an asyncio.BaseEventLoop instance. + + The caller should also call teardown_test_loop, + once they are done with the loop. + """ + loop = loop_factory() + asyncio.set_event_loop(loop) + return loop + + +def teardown_test_loop(loop: asyncio.AbstractEventLoop, fast: bool = False) -> None: + """Teardown and cleanup an event_loop created by setup_test_loop.""" + closed = loop.is_closed() + if not closed: + loop.call_soon(loop.stop) + loop.run_forever() + loop.close() + + if not fast: + gc.collect() + + asyncio.set_event_loop(None) + + +def _create_app_mock() -> mock.MagicMock: + def get_dict(app: Any, key: str) -> Any: + return app.__app_dict[key] + + def set_dict(app: Any, key: str, value: Any) -> None: + app.__app_dict[key] = value + + app = mock.MagicMock(spec=Application) + app.__app_dict = {} + app.__getitem__ = get_dict + app.__setitem__ = set_dict + + app.on_response_prepare = Signal(app) + app.on_response_prepare.freeze() + return app + + +def _create_transport(sslcontext: Optional[SSLContext] = None) -> mock.Mock: + transport = mock.Mock() + + def get_extra_info(key: str) -> Optional[SSLContext]: + if key == "sslcontext": + return sslcontext + else: + return None + + transport.get_extra_info.side_effect = get_extra_info + return transport + + +def make_mocked_request( + method: str, + path: str, + headers: Any = None, + *, + match_info: Any = sentinel, + version: HttpVersion = HttpVersion(1, 1), + closing: bool = False, + app: Any = None, + writer: Any = sentinel, + protocol: Any = sentinel, + transport: Any = sentinel, + payload: Any = sentinel, + sslcontext: Optional[SSLContext] = None, + client_max_size: int = 1024**2, + loop: Any = ..., +) -> Request: + """Creates mocked web.Request testing purposes. + + Useful in unit tests, when spinning full web server is overkill or + specific conditions and errors are hard to trigger. + """ + task = mock.Mock() + if loop is ...: + loop = mock.Mock() + loop.create_future.return_value = () + + if version < HttpVersion(1, 1): + closing = True + + if headers: + headers = CIMultiDictProxy(CIMultiDict(headers)) + raw_hdrs = tuple( + (k.encode("utf-8"), v.encode("utf-8")) for k, v in headers.items() + ) + else: + headers = CIMultiDictProxy(CIMultiDict()) + raw_hdrs = () + + chunked = "chunked" in headers.get(hdrs.TRANSFER_ENCODING, "").lower() + + message = RawRequestMessage( + method, + path, + version, + headers, + raw_hdrs, + closing, + None, + False, + chunked, + URL(path), + ) + if app is None: + app = _create_app_mock() + + if transport is sentinel: + transport = _create_transport(sslcontext) + + if protocol is sentinel: + protocol = mock.Mock() + protocol.transport = transport + + if writer is sentinel: + writer = mock.Mock() + writer.write_headers = make_mocked_coro(None) + writer.write = make_mocked_coro(None) + writer.write_eof = make_mocked_coro(None) + writer.drain = make_mocked_coro(None) + writer.transport = transport + + protocol.transport = transport + protocol.writer = writer + + if payload is sentinel: + payload = mock.Mock() + + req = Request( + message, payload, protocol, writer, task, loop, client_max_size=client_max_size + ) + + match_info = UrlMappingMatchInfo( + {} if match_info is sentinel else match_info, mock.Mock() + ) + match_info.add_app(app) + req._match_info = match_info + + return req + + +def make_mocked_coro( + return_value: Any = sentinel, raise_exception: Any = sentinel +) -> Any: + """Creates a coroutine mock.""" + + async def mock_coro(*args: Any, **kwargs: Any) -> Any: + if raise_exception is not sentinel: + raise raise_exception + if not inspect.isawaitable(return_value): + return return_value + await return_value + + return mock.Mock(wraps=mock_coro) diff --git a/aiohttp/tracing.py b/aiohttp/tracing.py new file mode 100644 index 0000000..37f0481 --- /dev/null +++ b/aiohttp/tracing.py @@ -0,0 +1,471 @@ +import dataclasses +from types import SimpleNamespace +from typing import TYPE_CHECKING, Awaitable, Optional, Protocol, Type, TypeVar + +from aiosignal import Signal +from multidict import CIMultiDict +from yarl import URL + +from .client_reqrep import ClientResponse + +if TYPE_CHECKING: # pragma: no cover + from .client import ClientSession + + _ParamT_contra = TypeVar("_ParamT_contra", contravariant=True) + + class _SignalCallback(Protocol[_ParamT_contra]): + def __call__( + self, + __client_session: ClientSession, + __trace_config_ctx: SimpleNamespace, + __params: _ParamT_contra, + ) -> Awaitable[None]: + ... + + +__all__ = ( + "TraceConfig", + "TraceRequestStartParams", + "TraceRequestEndParams", + "TraceRequestExceptionParams", + "TraceConnectionQueuedStartParams", + "TraceConnectionQueuedEndParams", + "TraceConnectionCreateStartParams", + "TraceConnectionCreateEndParams", + "TraceConnectionReuseconnParams", + "TraceDnsResolveHostStartParams", + "TraceDnsResolveHostEndParams", + "TraceDnsCacheHitParams", + "TraceDnsCacheMissParams", + "TraceRequestRedirectParams", + "TraceRequestChunkSentParams", + "TraceResponseChunkReceivedParams", + "TraceRequestHeadersSentParams", +) + + +class TraceConfig: + """First-class used to trace requests launched via ClientSession objects.""" + + def __init__( + self, trace_config_ctx_factory: Type[SimpleNamespace] = SimpleNamespace + ) -> None: + self._on_request_start: Signal[ + _SignalCallback[TraceRequestStartParams] + ] = Signal(self) + self._on_request_chunk_sent: Signal[ + _SignalCallback[TraceRequestChunkSentParams] + ] = Signal(self) + self._on_response_chunk_received: Signal[ + _SignalCallback[TraceResponseChunkReceivedParams] + ] = Signal(self) + self._on_request_end: Signal[_SignalCallback[TraceRequestEndParams]] = Signal( + self + ) + self._on_request_exception: Signal[ + _SignalCallback[TraceRequestExceptionParams] + ] = Signal(self) + self._on_request_redirect: Signal[ + _SignalCallback[TraceRequestRedirectParams] + ] = Signal(self) + self._on_connection_queued_start: Signal[ + _SignalCallback[TraceConnectionQueuedStartParams] + ] = Signal(self) + self._on_connection_queued_end: Signal[ + _SignalCallback[TraceConnectionQueuedEndParams] + ] = Signal(self) + self._on_connection_create_start: Signal[ + _SignalCallback[TraceConnectionCreateStartParams] + ] = Signal(self) + self._on_connection_create_end: Signal[ + _SignalCallback[TraceConnectionCreateEndParams] + ] = Signal(self) + self._on_connection_reuseconn: Signal[ + _SignalCallback[TraceConnectionReuseconnParams] + ] = Signal(self) + self._on_dns_resolvehost_start: Signal[ + _SignalCallback[TraceDnsResolveHostStartParams] + ] = Signal(self) + self._on_dns_resolvehost_end: Signal[ + _SignalCallback[TraceDnsResolveHostEndParams] + ] = Signal(self) + self._on_dns_cache_hit: Signal[ + _SignalCallback[TraceDnsCacheHitParams] + ] = Signal(self) + self._on_dns_cache_miss: Signal[ + _SignalCallback[TraceDnsCacheMissParams] + ] = Signal(self) + self._on_request_headers_sent: Signal[ + _SignalCallback[TraceRequestHeadersSentParams] + ] = Signal(self) + + self._trace_config_ctx_factory = trace_config_ctx_factory + + def trace_config_ctx( + self, trace_request_ctx: Optional[SimpleNamespace] = None + ) -> SimpleNamespace: + """Return a new trace_config_ctx instance""" + return self._trace_config_ctx_factory(trace_request_ctx=trace_request_ctx) + + def freeze(self) -> None: + self._on_request_start.freeze() + self._on_request_chunk_sent.freeze() + self._on_response_chunk_received.freeze() + self._on_request_end.freeze() + self._on_request_exception.freeze() + self._on_request_redirect.freeze() + self._on_connection_queued_start.freeze() + self._on_connection_queued_end.freeze() + self._on_connection_create_start.freeze() + self._on_connection_create_end.freeze() + self._on_connection_reuseconn.freeze() + self._on_dns_resolvehost_start.freeze() + self._on_dns_resolvehost_end.freeze() + self._on_dns_cache_hit.freeze() + self._on_dns_cache_miss.freeze() + self._on_request_headers_sent.freeze() + + @property + def on_request_start(self) -> "Signal[_SignalCallback[TraceRequestStartParams]]": + return self._on_request_start + + @property + def on_request_chunk_sent( + self, + ) -> "Signal[_SignalCallback[TraceRequestChunkSentParams]]": + return self._on_request_chunk_sent + + @property + def on_response_chunk_received( + self, + ) -> "Signal[_SignalCallback[TraceResponseChunkReceivedParams]]": + return self._on_response_chunk_received + + @property + def on_request_end(self) -> "Signal[_SignalCallback[TraceRequestEndParams]]": + return self._on_request_end + + @property + def on_request_exception( + self, + ) -> "Signal[_SignalCallback[TraceRequestExceptionParams]]": + return self._on_request_exception + + @property + def on_request_redirect( + self, + ) -> "Signal[_SignalCallback[TraceRequestRedirectParams]]": + return self._on_request_redirect + + @property + def on_connection_queued_start( + self, + ) -> "Signal[_SignalCallback[TraceConnectionQueuedStartParams]]": + return self._on_connection_queued_start + + @property + def on_connection_queued_end( + self, + ) -> "Signal[_SignalCallback[TraceConnectionQueuedEndParams]]": + return self._on_connection_queued_end + + @property + def on_connection_create_start( + self, + ) -> "Signal[_SignalCallback[TraceConnectionCreateStartParams]]": + return self._on_connection_create_start + + @property + def on_connection_create_end( + self, + ) -> "Signal[_SignalCallback[TraceConnectionCreateEndParams]]": + return self._on_connection_create_end + + @property + def on_connection_reuseconn( + self, + ) -> "Signal[_SignalCallback[TraceConnectionReuseconnParams]]": + return self._on_connection_reuseconn + + @property + def on_dns_resolvehost_start( + self, + ) -> "Signal[_SignalCallback[TraceDnsResolveHostStartParams]]": + return self._on_dns_resolvehost_start + + @property + def on_dns_resolvehost_end( + self, + ) -> "Signal[_SignalCallback[TraceDnsResolveHostEndParams]]": + return self._on_dns_resolvehost_end + + @property + def on_dns_cache_hit(self) -> "Signal[_SignalCallback[TraceDnsCacheHitParams]]": + return self._on_dns_cache_hit + + @property + def on_dns_cache_miss(self) -> "Signal[_SignalCallback[TraceDnsCacheMissParams]]": + return self._on_dns_cache_miss + + @property + def on_request_headers_sent( + self, + ) -> "Signal[_SignalCallback[TraceRequestHeadersSentParams]]": + return self._on_request_headers_sent + + +@dataclasses.dataclass(frozen=True) +class TraceRequestStartParams: + """Parameters sent by the `on_request_start` signal""" + + method: str + url: URL + headers: "CIMultiDict[str]" + + +@dataclasses.dataclass(frozen=True) +class TraceRequestChunkSentParams: + """Parameters sent by the `on_request_chunk_sent` signal""" + + method: str + url: URL + chunk: bytes + + +@dataclasses.dataclass(frozen=True) +class TraceResponseChunkReceivedParams: + """Parameters sent by the `on_response_chunk_received` signal""" + + method: str + url: URL + chunk: bytes + + +@dataclasses.dataclass(frozen=True) +class TraceRequestEndParams: + """Parameters sent by the `on_request_end` signal""" + + method: str + url: URL + headers: "CIMultiDict[str]" + response: ClientResponse + + +@dataclasses.dataclass(frozen=True) +class TraceRequestExceptionParams: + """Parameters sent by the `on_request_exception` signal""" + + method: str + url: URL + headers: "CIMultiDict[str]" + exception: BaseException + + +@dataclasses.dataclass(frozen=True) +class TraceRequestRedirectParams: + """Parameters sent by the `on_request_redirect` signal""" + + method: str + url: URL + headers: "CIMultiDict[str]" + response: ClientResponse + + +@dataclasses.dataclass(frozen=True) +class TraceConnectionQueuedStartParams: + """Parameters sent by the `on_connection_queued_start` signal""" + + +@dataclasses.dataclass(frozen=True) +class TraceConnectionQueuedEndParams: + """Parameters sent by the `on_connection_queued_end` signal""" + + +@dataclasses.dataclass(frozen=True) +class TraceConnectionCreateStartParams: + """Parameters sent by the `on_connection_create_start` signal""" + + +@dataclasses.dataclass(frozen=True) +class TraceConnectionCreateEndParams: + """Parameters sent by the `on_connection_create_end` signal""" + + +@dataclasses.dataclass(frozen=True) +class TraceConnectionReuseconnParams: + """Parameters sent by the `on_connection_reuseconn` signal""" + + +@dataclasses.dataclass(frozen=True) +class TraceDnsResolveHostStartParams: + """Parameters sent by the `on_dns_resolvehost_start` signal""" + + host: str + + +@dataclasses.dataclass(frozen=True) +class TraceDnsResolveHostEndParams: + """Parameters sent by the `on_dns_resolvehost_end` signal""" + + host: str + + +@dataclasses.dataclass(frozen=True) +class TraceDnsCacheHitParams: + """Parameters sent by the `on_dns_cache_hit` signal""" + + host: str + + +@dataclasses.dataclass(frozen=True) +class TraceDnsCacheMissParams: + """Parameters sent by the `on_dns_cache_miss` signal""" + + host: str + + +@dataclasses.dataclass(frozen=True) +class TraceRequestHeadersSentParams: + """Parameters sent by the `on_request_headers_sent` signal""" + + method: str + url: URL + headers: "CIMultiDict[str]" + + +class Trace: + """Internal dependency holder class. + + Used to keep together the main dependencies used + at the moment of send a signal. + """ + + def __init__( + self, + session: "ClientSession", + trace_config: TraceConfig, + trace_config_ctx: SimpleNamespace, + ) -> None: + self._trace_config = trace_config + self._trace_config_ctx = trace_config_ctx + self._session = session + + async def send_request_start( + self, method: str, url: URL, headers: "CIMultiDict[str]" + ) -> None: + return await self._trace_config.on_request_start.send( + self._session, + self._trace_config_ctx, + TraceRequestStartParams(method, url, headers), + ) + + async def send_request_chunk_sent( + self, method: str, url: URL, chunk: bytes + ) -> None: + return await self._trace_config.on_request_chunk_sent.send( + self._session, + self._trace_config_ctx, + TraceRequestChunkSentParams(method, url, chunk), + ) + + async def send_response_chunk_received( + self, method: str, url: URL, chunk: bytes + ) -> None: + return await self._trace_config.on_response_chunk_received.send( + self._session, + self._trace_config_ctx, + TraceResponseChunkReceivedParams(method, url, chunk), + ) + + async def send_request_end( + self, + method: str, + url: URL, + headers: "CIMultiDict[str]", + response: ClientResponse, + ) -> None: + return await self._trace_config.on_request_end.send( + self._session, + self._trace_config_ctx, + TraceRequestEndParams(method, url, headers, response), + ) + + async def send_request_exception( + self, + method: str, + url: URL, + headers: "CIMultiDict[str]", + exception: BaseException, + ) -> None: + return await self._trace_config.on_request_exception.send( + self._session, + self._trace_config_ctx, + TraceRequestExceptionParams(method, url, headers, exception), + ) + + async def send_request_redirect( + self, + method: str, + url: URL, + headers: "CIMultiDict[str]", + response: ClientResponse, + ) -> None: + return await self._trace_config._on_request_redirect.send( + self._session, + self._trace_config_ctx, + TraceRequestRedirectParams(method, url, headers, response), + ) + + async def send_connection_queued_start(self) -> None: + return await self._trace_config.on_connection_queued_start.send( + self._session, self._trace_config_ctx, TraceConnectionQueuedStartParams() + ) + + async def send_connection_queued_end(self) -> None: + return await self._trace_config.on_connection_queued_end.send( + self._session, self._trace_config_ctx, TraceConnectionQueuedEndParams() + ) + + async def send_connection_create_start(self) -> None: + return await self._trace_config.on_connection_create_start.send( + self._session, self._trace_config_ctx, TraceConnectionCreateStartParams() + ) + + async def send_connection_create_end(self) -> None: + return await self._trace_config.on_connection_create_end.send( + self._session, self._trace_config_ctx, TraceConnectionCreateEndParams() + ) + + async def send_connection_reuseconn(self) -> None: + return await self._trace_config.on_connection_reuseconn.send( + self._session, self._trace_config_ctx, TraceConnectionReuseconnParams() + ) + + async def send_dns_resolvehost_start(self, host: str) -> None: + return await self._trace_config.on_dns_resolvehost_start.send( + self._session, self._trace_config_ctx, TraceDnsResolveHostStartParams(host) + ) + + async def send_dns_resolvehost_end(self, host: str) -> None: + return await self._trace_config.on_dns_resolvehost_end.send( + self._session, self._trace_config_ctx, TraceDnsResolveHostEndParams(host) + ) + + async def send_dns_cache_hit(self, host: str) -> None: + return await self._trace_config.on_dns_cache_hit.send( + self._session, self._trace_config_ctx, TraceDnsCacheHitParams(host) + ) + + async def send_dns_cache_miss(self, host: str) -> None: + return await self._trace_config.on_dns_cache_miss.send( + self._session, self._trace_config_ctx, TraceDnsCacheMissParams(host) + ) + + async def send_request_headers( + self, method: str, url: URL, headers: "CIMultiDict[str]" + ) -> None: + return await self._trace_config._on_request_headers_sent.send( + self._session, + self._trace_config_ctx, + TraceRequestHeadersSentParams(method, url, headers), + ) diff --git a/aiohttp/typedefs.py b/aiohttp/typedefs.py new file mode 100644 index 0000000..57d95b3 --- /dev/null +++ b/aiohttp/typedefs.py @@ -0,0 +1,54 @@ +import json +import os +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Iterable, + Mapping, + Tuple, + Union, +) + +from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy, istr +from yarl import URL + +DEFAULT_JSON_ENCODER = json.dumps +DEFAULT_JSON_DECODER = json.loads + +if TYPE_CHECKING: # pragma: no cover + _CIMultiDict = CIMultiDict[str] + _CIMultiDictProxy = CIMultiDictProxy[str] + _MultiDict = MultiDict[str] + _MultiDictProxy = MultiDictProxy[str] + from http.cookies import BaseCookie, Morsel + + from .web import Request, StreamResponse +else: + _CIMultiDict = CIMultiDict + _CIMultiDictProxy = CIMultiDictProxy + _MultiDict = MultiDict + _MultiDictProxy = MultiDictProxy + +Byteish = Union[bytes, bytearray, memoryview] +JSONEncoder = Callable[[Any], str] +JSONDecoder = Callable[[str], Any] +LooseHeaders = Union[Mapping[Union[str, istr], str], _CIMultiDict, _CIMultiDictProxy] +RawHeaders = Tuple[Tuple[bytes, bytes], ...] +StrOrURL = Union[str, URL] + +LooseCookiesMappings = Mapping[str, Union[str, "BaseCookie[str]", "Morsel[Any]"]] +LooseCookiesIterables = Iterable[ + Tuple[str, Union[str, "BaseCookie[str]", "Morsel[Any]"]] +] +LooseCookies = Union[ + LooseCookiesMappings, + LooseCookiesIterables, + "BaseCookie[str]", +] + +Handler = Callable[["Request"], Awaitable["StreamResponse"]] +Middleware = Callable[["Request", Handler], Awaitable["StreamResponse"]] + +PathLike = Union[str, "os.PathLike[str]"] diff --git a/aiohttp/web.py b/aiohttp/web.py new file mode 100644 index 0000000..f87d579 --- /dev/null +++ b/aiohttp/web.py @@ -0,0 +1,573 @@ +import asyncio +import logging +import os +import socket +import sys +from argparse import ArgumentParser +from collections.abc import Iterable +from importlib import import_module +from typing import ( + Any, + Awaitable, + Callable, + Iterable as TypingIterable, + List, + Optional, + Set, + Type, + Union, + cast, +) + +from .abc import AbstractAccessLogger +from .helpers import AppKey +from .log import access_logger +from .typedefs import PathLike +from .web_app import Application, CleanupError +from .web_exceptions import ( + HTTPAccepted, + HTTPBadGateway, + HTTPBadRequest, + HTTPClientError, + HTTPConflict, + HTTPCreated, + HTTPError, + HTTPException, + HTTPExpectationFailed, + HTTPFailedDependency, + HTTPForbidden, + HTTPFound, + HTTPGatewayTimeout, + HTTPGone, + HTTPInsufficientStorage, + HTTPInternalServerError, + HTTPLengthRequired, + HTTPMethodNotAllowed, + HTTPMisdirectedRequest, + HTTPMove, + HTTPMovedPermanently, + HTTPMultipleChoices, + HTTPNetworkAuthenticationRequired, + HTTPNoContent, + HTTPNonAuthoritativeInformation, + HTTPNotAcceptable, + HTTPNotExtended, + HTTPNotFound, + HTTPNotImplemented, + HTTPNotModified, + HTTPOk, + HTTPPartialContent, + HTTPPaymentRequired, + HTTPPermanentRedirect, + HTTPPreconditionFailed, + HTTPPreconditionRequired, + HTTPProxyAuthenticationRequired, + HTTPRedirection, + HTTPRequestEntityTooLarge, + HTTPRequestHeaderFieldsTooLarge, + HTTPRequestRangeNotSatisfiable, + HTTPRequestTimeout, + HTTPRequestURITooLong, + HTTPResetContent, + HTTPSeeOther, + HTTPServerError, + HTTPServiceUnavailable, + HTTPSuccessful, + HTTPTemporaryRedirect, + HTTPTooManyRequests, + HTTPUnauthorized, + HTTPUnavailableForLegalReasons, + HTTPUnprocessableEntity, + HTTPUnsupportedMediaType, + HTTPUpgradeRequired, + HTTPUseProxy, + HTTPVariantAlsoNegotiates, + HTTPVersionNotSupported, +) +from .web_fileresponse import FileResponse +from .web_log import AccessLogger +from .web_middlewares import middleware, normalize_path_middleware +from .web_protocol import PayloadAccessError, RequestHandler, RequestPayloadError +from .web_request import BaseRequest, FileField, Request +from .web_response import ContentCoding, Response, StreamResponse, json_response +from .web_routedef import ( + AbstractRouteDef, + RouteDef, + RouteTableDef, + StaticDef, + delete, + get, + head, + options, + patch, + post, + put, + route, + static, + view, +) +from .web_runner import ( + AppRunner, + BaseRunner, + BaseSite, + GracefulExit, + NamedPipeSite, + ServerRunner, + SockSite, + TCPSite, + UnixSite, +) +from .web_server import Server +from .web_urldispatcher import ( + AbstractResource, + AbstractRoute, + DynamicResource, + PlainResource, + PrefixedSubAppResource, + Resource, + ResourceRoute, + StaticResource, + UrlDispatcher, + UrlMappingMatchInfo, + View, +) +from .web_ws import WebSocketReady, WebSocketResponse, WSMsgType + +__all__ = ( + # web_app + "AppKey", + "Application", + "CleanupError", + # web_exceptions + "HTTPAccepted", + "HTTPBadGateway", + "HTTPBadRequest", + "HTTPClientError", + "HTTPConflict", + "HTTPCreated", + "HTTPError", + "HTTPException", + "HTTPExpectationFailed", + "HTTPFailedDependency", + "HTTPForbidden", + "HTTPFound", + "HTTPGatewayTimeout", + "HTTPGone", + "HTTPInsufficientStorage", + "HTTPInternalServerError", + "HTTPLengthRequired", + "HTTPMethodNotAllowed", + "HTTPMisdirectedRequest", + "HTTPMove", + "HTTPMovedPermanently", + "HTTPMultipleChoices", + "HTTPNetworkAuthenticationRequired", + "HTTPNoContent", + "HTTPNonAuthoritativeInformation", + "HTTPNotAcceptable", + "HTTPNotExtended", + "HTTPNotFound", + "HTTPNotImplemented", + "HTTPNotModified", + "HTTPOk", + "HTTPPartialContent", + "HTTPPaymentRequired", + "HTTPPermanentRedirect", + "HTTPPreconditionFailed", + "HTTPPreconditionRequired", + "HTTPProxyAuthenticationRequired", + "HTTPRedirection", + "HTTPRequestEntityTooLarge", + "HTTPRequestHeaderFieldsTooLarge", + "HTTPRequestRangeNotSatisfiable", + "HTTPRequestTimeout", + "HTTPRequestURITooLong", + "HTTPResetContent", + "HTTPSeeOther", + "HTTPServerError", + "HTTPServiceUnavailable", + "HTTPSuccessful", + "HTTPTemporaryRedirect", + "HTTPTooManyRequests", + "HTTPUnauthorized", + "HTTPUnavailableForLegalReasons", + "HTTPUnprocessableEntity", + "HTTPUnsupportedMediaType", + "HTTPUpgradeRequired", + "HTTPUseProxy", + "HTTPVariantAlsoNegotiates", + "HTTPVersionNotSupported", + # web_fileresponse + "FileResponse", + # web_middlewares + "middleware", + "normalize_path_middleware", + # web_protocol + "PayloadAccessError", + "RequestHandler", + "RequestPayloadError", + # web_request + "BaseRequest", + "FileField", + "Request", + # web_response + "ContentCoding", + "Response", + "StreamResponse", + "json_response", + # web_routedef + "AbstractRouteDef", + "RouteDef", + "RouteTableDef", + "StaticDef", + "delete", + "get", + "head", + "options", + "patch", + "post", + "put", + "route", + "static", + "view", + # web_runner + "AppRunner", + "BaseRunner", + "BaseSite", + "GracefulExit", + "ServerRunner", + "SockSite", + "TCPSite", + "UnixSite", + "NamedPipeSite", + # web_server + "Server", + # web_urldispatcher + "AbstractResource", + "AbstractRoute", + "DynamicResource", + "PlainResource", + "PrefixedSubAppResource", + "Resource", + "ResourceRoute", + "StaticResource", + "UrlDispatcher", + "UrlMappingMatchInfo", + "View", + # web_ws + "WebSocketReady", + "WebSocketResponse", + "WSMsgType", + # web + "run_app", +) + + +try: + from ssl import SSLContext +except ImportError: # pragma: no cover + SSLContext = Any # type: ignore[misc,assignment] + +HostSequence = TypingIterable[str] + + +async def _run_app( + app: Union[Application, Awaitable[Application]], + *, + host: Optional[Union[str, HostSequence]] = None, + port: Optional[int] = None, + path: Union[PathLike, TypingIterable[PathLike], None] = None, + sock: Optional[Union[socket.socket, TypingIterable[socket.socket]]] = None, + shutdown_timeout: float = 60.0, + keepalive_timeout: float = 75.0, + ssl_context: Optional[SSLContext] = None, + print: Optional[Callable[..., None]] = print, + backlog: int = 128, + access_log_class: Type[AbstractAccessLogger] = AccessLogger, + access_log_format: str = AccessLogger.LOG_FORMAT, + access_log: Optional[logging.Logger] = access_logger, + handle_signals: bool = True, + reuse_address: Optional[bool] = None, + reuse_port: Optional[bool] = None, + handler_cancellation: bool = False, +) -> None: + # An internal function to actually do all dirty job for application running + if asyncio.iscoroutine(app): + app = await app + + app = cast(Application, app) + + runner = AppRunner( + app, + handle_signals=handle_signals, + access_log_class=access_log_class, + access_log_format=access_log_format, + access_log=access_log, + keepalive_timeout=keepalive_timeout, + handler_cancellation=handler_cancellation, + ) + + await runner.setup() + + sites: List[BaseSite] = [] + + try: + if host is not None: + if isinstance(host, (str, bytes, bytearray, memoryview)): + sites.append( + TCPSite( + runner, + host, + port, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + reuse_address=reuse_address, + reuse_port=reuse_port, + ) + ) + else: + for h in host: + sites.append( + TCPSite( + runner, + h, + port, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + reuse_address=reuse_address, + reuse_port=reuse_port, + ) + ) + elif path is None and sock is None or port is not None: + sites.append( + TCPSite( + runner, + port=port, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + reuse_address=reuse_address, + reuse_port=reuse_port, + ) + ) + + if path is not None: + if isinstance(path, (str, os.PathLike)): + sites.append( + UnixSite( + runner, + path, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + ) + ) + else: + for p in path: + sites.append( + UnixSite( + runner, + p, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + ) + ) + + if sock is not None: + if not isinstance(sock, Iterable): + sites.append( + SockSite( + runner, + sock, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + ) + ) + else: + for s in sock: + sites.append( + SockSite( + runner, + s, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + ) + ) + for site in sites: + await site.start() + + if print: # pragma: no branch + names = sorted(str(s.name) for s in runner.sites) + print( + "======== Running on {} ========\n" + "(Press CTRL+C to quit)".format(", ".join(names)) + ) + + # sleep forever by 1 hour intervals, + while True: + await asyncio.sleep(3600) + finally: + await runner.cleanup() + + +def _cancel_tasks( + to_cancel: Set["asyncio.Task[Any]"], loop: asyncio.AbstractEventLoop +) -> None: + if not to_cancel: + return + + for task in to_cancel: + task.cancel() + + loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) + + for task in to_cancel: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "unhandled exception during asyncio.run() shutdown", + "exception": task.exception(), + "task": task, + } + ) + + +def run_app( + app: Union[Application, Awaitable[Application]], + *, + debug: bool = False, + host: Optional[Union[str, HostSequence]] = None, + port: Optional[int] = None, + path: Union[PathLike, TypingIterable[PathLike], None] = None, + sock: Optional[Union[socket.socket, TypingIterable[socket.socket]]] = None, + shutdown_timeout: float = 60.0, + keepalive_timeout: float = 75.0, + ssl_context: Optional[SSLContext] = None, + print: Optional[Callable[..., None]] = print, + backlog: int = 128, + access_log_class: Type[AbstractAccessLogger] = AccessLogger, + access_log_format: str = AccessLogger.LOG_FORMAT, + access_log: Optional[logging.Logger] = access_logger, + handle_signals: bool = True, + reuse_address: Optional[bool] = None, + reuse_port: Optional[bool] = None, + handler_cancellation: bool = False, + loop: Optional[asyncio.AbstractEventLoop] = None, +) -> None: + """Run an app locally""" + if loop is None: + loop = asyncio.new_event_loop() + loop.set_debug(debug) + + # Configure if and only if in debugging mode and using the default logger + if loop.get_debug() and access_log and access_log.name == "aiohttp.access": + if access_log.level == logging.NOTSET: + access_log.setLevel(logging.DEBUG) + if not access_log.hasHandlers(): + access_log.addHandler(logging.StreamHandler()) + + main_task = loop.create_task( + _run_app( + app, + host=host, + port=port, + path=path, + sock=sock, + shutdown_timeout=shutdown_timeout, + keepalive_timeout=keepalive_timeout, + ssl_context=ssl_context, + print=print, + backlog=backlog, + access_log_class=access_log_class, + access_log_format=access_log_format, + access_log=access_log, + handle_signals=handle_signals, + reuse_address=reuse_address, + reuse_port=reuse_port, + handler_cancellation=handler_cancellation, + ) + ) + + try: + asyncio.set_event_loop(loop) + loop.run_until_complete(main_task) + except (GracefulExit, KeyboardInterrupt): # pragma: no cover + pass + finally: + _cancel_tasks({main_task}, loop) + _cancel_tasks(asyncio.all_tasks(loop), loop) + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.close() + asyncio.set_event_loop(None) + + +def main(argv: List[str]) -> None: + arg_parser = ArgumentParser( + description="aiohttp.web Application server", prog="aiohttp.web" + ) + arg_parser.add_argument( + "entry_func", + help=( + "Callable returning the `aiohttp.web.Application` instance to " + "run. Should be specified in the 'module:function' syntax." + ), + metavar="entry-func", + ) + arg_parser.add_argument( + "-H", + "--hostname", + help="TCP/IP hostname to serve on (default: %(default)r)", + default="localhost", + ) + arg_parser.add_argument( + "-P", + "--port", + help="TCP/IP port to serve on (default: %(default)r)", + type=int, + default="8080", + ) + arg_parser.add_argument( + "-U", + "--path", + help="Unix file system path to serve on. Specifying a path will cause " + "hostname and port arguments to be ignored.", + ) + args, extra_argv = arg_parser.parse_known_args(argv) + + # Import logic + mod_str, _, func_str = args.entry_func.partition(":") + if not func_str or not mod_str: + arg_parser.error("'entry-func' not in 'module:function' syntax") + if mod_str.startswith("."): + arg_parser.error("relative module names not supported") + try: + module = import_module(mod_str) + except ImportError as ex: + arg_parser.error(f"unable to import {mod_str}: {ex}") + try: + func = getattr(module, func_str) + except AttributeError: + arg_parser.error(f"module {mod_str!r} has no attribute {func_str!r}") + + # Compatibility logic + if args.path is not None and not hasattr(socket, "AF_UNIX"): + arg_parser.error( + "file system paths not supported by your operating" " environment" + ) + + logging.basicConfig(level=logging.DEBUG) + + app = func(extra_argv) + run_app(app, host=args.hostname, port=args.port, path=args.path) + arg_parser.exit(message="Stopped\n") + + +if __name__ == "__main__": # pragma: no branch + main(sys.argv[1:]) # pragma: no cover diff --git a/aiohttp/web_app.py b/aiohttp/web_app.py new file mode 100644 index 0000000..5be9608 --- /dev/null +++ b/aiohttp/web_app.py @@ -0,0 +1,445 @@ +import asyncio +import logging +import warnings +from functools import partial, update_wrapper +from typing import ( + TYPE_CHECKING, + Any, + AsyncIterator, + Awaitable, + Callable, + Dict, + Iterable, + Iterator, + List, + Mapping, + MutableMapping, + Optional, + Sequence, + Type, + TypeVar, + Union, + cast, + final, + overload, +) + +from aiosignal import Signal +from frozenlist import FrozenList + +from . import hdrs +from .helpers import AppKey +from .log import web_logger +from .typedefs import Middleware +from .web_middlewares import _fix_request_current_app +from .web_request import Request +from .web_response import StreamResponse +from .web_routedef import AbstractRouteDef +from .web_urldispatcher import ( + AbstractResource, + AbstractRoute, + Domain, + MaskDomain, + MatchedSubAppResource, + PrefixedSubAppResource, + UrlDispatcher, +) + +__all__ = ("Application", "CleanupError") + + +if TYPE_CHECKING: # pragma: no cover + _AppSignal = Signal[Callable[["Application"], Awaitable[None]]] + _RespPrepareSignal = Signal[Callable[[Request, StreamResponse], Awaitable[None]]] + _Middlewares = FrozenList[Middleware] + _MiddlewaresHandlers = Sequence[Middleware] + _Subapps = List["Application"] +else: + # No type checker mode, skip types + _AppSignal = Signal + _RespPrepareSignal = Signal + _Handler = Callable + _Middlewares = FrozenList + _MiddlewaresHandlers = Sequence + _Subapps = List + +_T = TypeVar("_T") +_U = TypeVar("_U") + + +@final +class Application(MutableMapping[Union[str, AppKey[Any]], Any]): + __slots__ = ( + "logger", + "_debug", + "_router", + "_loop", + "_handler_args", + "_middlewares", + "_middlewares_handlers", + "_run_middlewares", + "_state", + "_frozen", + "_pre_frozen", + "_subapps", + "_on_response_prepare", + "_on_startup", + "_on_shutdown", + "_on_cleanup", + "_client_max_size", + "_cleanup_ctx", + ) + + def __init__( + self, + *, + logger: logging.Logger = web_logger, + middlewares: Iterable[Middleware] = (), + handler_args: Optional[Mapping[str, Any]] = None, + client_max_size: int = 1024**2, + debug: Any = ..., # mypy doesn't support ellipsis + ) -> None: + if debug is not ...: + warnings.warn( + "debug argument is no-op since 4.0 " "and scheduled for removal in 5.0", + DeprecationWarning, + stacklevel=2, + ) + self._router = UrlDispatcher() + self._handler_args = handler_args + self.logger = logger + + self._middlewares: _Middlewares = FrozenList(middlewares) + + # initialized on freezing + self._middlewares_handlers: _MiddlewaresHandlers = tuple() + # initialized on freezing + self._run_middlewares: Optional[bool] = None + + self._state: Dict[Union[AppKey[Any], str], object] = {} + self._frozen = False + self._pre_frozen = False + self._subapps: _Subapps = [] + + self._on_response_prepare: _RespPrepareSignal = Signal(self) + self._on_startup: _AppSignal = Signal(self) + self._on_shutdown: _AppSignal = Signal(self) + self._on_cleanup: _AppSignal = Signal(self) + self._cleanup_ctx = CleanupContext() + self._on_startup.append(self._cleanup_ctx._on_startup) + self._on_cleanup.append(self._cleanup_ctx._on_cleanup) + self._client_max_size = client_max_size + + def __init_subclass__(cls: Type["Application"]) -> None: + raise TypeError( + "Inheritance class {} from web.Application " + "is forbidden".format(cls.__name__) + ) + + # MutableMapping API + + def __eq__(self, other: object) -> bool: + return self is other + + @overload # type: ignore[override] + def __getitem__(self, key: AppKey[_T]) -> _T: + ... + + @overload + def __getitem__(self, key: str) -> Any: + ... + + def __getitem__(self, key: Union[str, AppKey[_T]]) -> Any: + return self._state[key] + + def _check_frozen(self) -> None: + if self._frozen: + raise RuntimeError( + "Changing state of started or joined " "application is forbidden" + ) + + @overload # type: ignore[override] + def __setitem__(self, key: AppKey[_T], value: _T) -> None: + ... + + @overload + def __setitem__(self, key: str, value: Any) -> None: + ... + + def __setitem__(self, key: Union[str, AppKey[_T]], value: Any) -> None: + self._check_frozen() + if not isinstance(key, AppKey): + warnings.warn( + "It is recommended to use web.AppKey instances for keys.\n" + + "https://docs.aiohttp.org/en/stable/web_advanced.html" + + "#application-s-config", + stacklevel=2, + ) + self._state[key] = value + + def __delitem__(self, key: Union[str, AppKey[_T]]) -> None: + self._check_frozen() + del self._state[key] + + def __len__(self) -> int: + return len(self._state) + + def __iter__(self) -> Iterator[Union[str, AppKey[Any]]]: + return iter(self._state) + + @overload # type: ignore[override] + def get(self, key: AppKey[_T], default: None = ...) -> Optional[_T]: + ... + + @overload + def get(self, key: AppKey[_T], default: _U) -> Union[_T, _U]: + ... + + @overload + def get(self, key: str, default: Any = ...) -> Any: + ... + + def get(self, key: Union[str, AppKey[_T]], default: Any = None) -> Any: + return self._state.get(key, default) + + ######## + def _set_loop(self, loop: Optional[asyncio.AbstractEventLoop]) -> None: + warnings.warn( + "_set_loop() is no-op since 4.0 " "and scheduled for removal in 5.0", + DeprecationWarning, + stacklevel=2, + ) + + @property + def pre_frozen(self) -> bool: + return self._pre_frozen + + def pre_freeze(self) -> None: + if self._pre_frozen: + return + + self._pre_frozen = True + self._middlewares.freeze() + self._router.freeze() + self._on_response_prepare.freeze() + self._cleanup_ctx.freeze() + self._on_startup.freeze() + self._on_shutdown.freeze() + self._on_cleanup.freeze() + self._middlewares_handlers = tuple(self._prepare_middleware()) + + # If current app and any subapp do not have middlewares avoid run all + # of the code footprint that it implies, which have a middleware + # hardcoded per app that sets up the current_app attribute. If no + # middlewares are configured the handler will receive the proper + # current_app without needing all of this code. + self._run_middlewares = True if self.middlewares else False + + for subapp in self._subapps: + subapp.pre_freeze() + self._run_middlewares = self._run_middlewares or subapp._run_middlewares + + @property + def frozen(self) -> bool: + return self._frozen + + def freeze(self) -> None: + if self._frozen: + return + + self.pre_freeze() + self._frozen = True + for subapp in self._subapps: + subapp.freeze() + + @property + def debug(self) -> bool: + warnings.warn( + "debug property is deprecated since 4.0" "and scheduled for removal in 5.0", + DeprecationWarning, + stacklevel=2, + ) + return asyncio.get_event_loop().get_debug() + + def _reg_subapp_signals(self, subapp: "Application") -> None: + def reg_handler(signame: str) -> None: + subsig = getattr(subapp, signame) + + async def handler(app: "Application") -> None: + await subsig.send(subapp) + + appsig = getattr(self, signame) + appsig.append(handler) + + reg_handler("on_startup") + reg_handler("on_shutdown") + reg_handler("on_cleanup") + + def add_subapp(self, prefix: str, subapp: "Application") -> AbstractResource: + if not isinstance(prefix, str): + raise TypeError("Prefix must be str") + prefix = prefix.rstrip("/") + if not prefix: + raise ValueError("Prefix cannot be empty") + factory = partial(PrefixedSubAppResource, prefix, subapp) + return self._add_subapp(factory, subapp) + + def _add_subapp( + self, resource_factory: Callable[[], AbstractResource], subapp: "Application" + ) -> AbstractResource: + if self.frozen: + raise RuntimeError("Cannot add sub application to frozen application") + if subapp.frozen: + raise RuntimeError("Cannot add frozen application") + resource = resource_factory() + self.router.register_resource(resource) + self._reg_subapp_signals(subapp) + self._subapps.append(subapp) + subapp.pre_freeze() + return resource + + def add_domain(self, domain: str, subapp: "Application") -> AbstractResource: + if not isinstance(domain, str): + raise TypeError("Domain must be str") + elif "*" in domain: + rule: Domain = MaskDomain(domain) + else: + rule = Domain(domain) + factory = partial(MatchedSubAppResource, rule, subapp) + return self._add_subapp(factory, subapp) + + def add_routes(self, routes: Iterable[AbstractRouteDef]) -> List[AbstractRoute]: + return self.router.add_routes(routes) + + @property + def on_response_prepare(self) -> _RespPrepareSignal: + return self._on_response_prepare + + @property + def on_startup(self) -> _AppSignal: + return self._on_startup + + @property + def on_shutdown(self) -> _AppSignal: + return self._on_shutdown + + @property + def on_cleanup(self) -> _AppSignal: + return self._on_cleanup + + @property + def cleanup_ctx(self) -> "CleanupContext": + return self._cleanup_ctx + + @property + def router(self) -> UrlDispatcher: + return self._router + + @property + def middlewares(self) -> _Middlewares: + return self._middlewares + + async def startup(self) -> None: + """Causes on_startup signal + + Should be called in the event loop along with the request handler. + """ + await self.on_startup.send(self) + + async def shutdown(self) -> None: + """Causes on_shutdown signal + + Should be called before cleanup() + """ + await self.on_shutdown.send(self) + + async def cleanup(self) -> None: + """Causes on_cleanup signal + + Should be called after shutdown() + """ + if self.on_cleanup.frozen: + await self.on_cleanup.send(self) + else: + # If an exception occurs in startup, ensure cleanup contexts are completed. + await self._cleanup_ctx._on_cleanup(self) + + def _prepare_middleware(self) -> Iterator[Middleware]: + yield from reversed(self._middlewares) + yield _fix_request_current_app(self) + + async def _handle(self, request: Request) -> StreamResponse: + match_info = await self._router.resolve(request) + match_info.add_app(self) + match_info.freeze() + + resp = None + request._match_info = match_info + expect = request.headers.get(hdrs.EXPECT) + if expect: + resp = await match_info.expect_handler(request) + await request.writer.drain() + + if resp is None: + handler = match_info.handler + + if self._run_middlewares: + for app in match_info.apps[::-1]: + assert app.pre_frozen, "middleware handlers are not ready" + for m in app._middlewares_handlers: + handler = update_wrapper(partial(m, handler=handler), handler) + + resp = await handler(request) + + return resp + + def __call__(self) -> "Application": + """gunicorn compatibility""" + return self + + def __repr__(self) -> str: + return f"" + + def __bool__(self) -> bool: + return True + + +class CleanupError(RuntimeError): + @property + def exceptions(self) -> List[BaseException]: + return cast(List[BaseException], self.args[1]) + + +if TYPE_CHECKING: # pragma: no cover + _CleanupContextBase = FrozenList[Callable[[Application], AsyncIterator[None]]] +else: + _CleanupContextBase = FrozenList + + +class CleanupContext(_CleanupContextBase): + def __init__(self) -> None: + super().__init__() + self._exits: List[AsyncIterator[None]] = [] + + async def _on_startup(self, app: Application) -> None: + for cb in self: + it = cb(app).__aiter__() + await it.__anext__() + self._exits.append(it) + + async def _on_cleanup(self, app: Application) -> None: + errors = [] + for it in reversed(self._exits): + try: + await it.__anext__() + except StopAsyncIteration: + pass + except Exception as exc: + errors.append(exc) + else: + errors.append(RuntimeError(f"{it!r} has more than one 'yield'")) + if errors: + if len(errors) == 1: + raise errors[0] + else: + raise CleanupError("Multiple errors on cleanup stage", errors) diff --git a/aiohttp/web_exceptions.py b/aiohttp/web_exceptions.py new file mode 100644 index 0000000..332ca9f --- /dev/null +++ b/aiohttp/web_exceptions.py @@ -0,0 +1,509 @@ +import warnings +from http import HTTPStatus +from typing import Any, Iterable, Optional, Set, Tuple + +from multidict import CIMultiDict +from yarl import URL + +from . import hdrs +from .helpers import CookieMixin +from .typedefs import LooseHeaders, StrOrURL + +__all__ = ( + "HTTPException", + "HTTPError", + "HTTPRedirection", + "HTTPSuccessful", + "HTTPOk", + "HTTPCreated", + "HTTPAccepted", + "HTTPNonAuthoritativeInformation", + "HTTPNoContent", + "HTTPResetContent", + "HTTPPartialContent", + "HTTPMove", + "HTTPMultipleChoices", + "HTTPMovedPermanently", + "HTTPFound", + "HTTPSeeOther", + "HTTPNotModified", + "HTTPUseProxy", + "HTTPTemporaryRedirect", + "HTTPPermanentRedirect", + "HTTPClientError", + "HTTPBadRequest", + "HTTPUnauthorized", + "HTTPPaymentRequired", + "HTTPForbidden", + "HTTPNotFound", + "HTTPMethodNotAllowed", + "HTTPNotAcceptable", + "HTTPProxyAuthenticationRequired", + "HTTPRequestTimeout", + "HTTPConflict", + "HTTPGone", + "HTTPLengthRequired", + "HTTPPreconditionFailed", + "HTTPRequestEntityTooLarge", + "HTTPRequestURITooLong", + "HTTPUnsupportedMediaType", + "HTTPRequestRangeNotSatisfiable", + "HTTPExpectationFailed", + "HTTPMisdirectedRequest", + "HTTPUnprocessableEntity", + "HTTPFailedDependency", + "HTTPUpgradeRequired", + "HTTPPreconditionRequired", + "HTTPTooManyRequests", + "HTTPRequestHeaderFieldsTooLarge", + "HTTPUnavailableForLegalReasons", + "HTTPServerError", + "HTTPInternalServerError", + "HTTPNotImplemented", + "HTTPBadGateway", + "HTTPServiceUnavailable", + "HTTPGatewayTimeout", + "HTTPVersionNotSupported", + "HTTPVariantAlsoNegotiates", + "HTTPInsufficientStorage", + "HTTPNotExtended", + "HTTPNetworkAuthenticationRequired", +) + + +############################################################ +# HTTP Exceptions +############################################################ + + +class HTTPException(CookieMixin, Exception): + # You should set in subclasses: + # status = 200 + + status_code = -1 + empty_body = False + default_reason = "" # Initialized at the end of the module + + def __init__( + self, + *, + headers: Optional[LooseHeaders] = None, + reason: Optional[str] = None, + text: Optional[str] = None, + content_type: Optional[str] = None, + ) -> None: + super().__init__() + if reason is None: + reason = self.default_reason + + if text is None: + if not self.empty_body: + text = f"{self.status_code}: {reason}" + else: + if self.empty_body: + warnings.warn( + "text argument is deprecated for HTTP status {} " + "since 4.0 and scheduled for removal in 5.0 (#3462)," + "the response should be provided without a body".format( + self.status_code + ), + DeprecationWarning, + stacklevel=2, + ) + + if headers is not None: + real_headers = CIMultiDict(headers) + else: + real_headers = CIMultiDict() + + if content_type is not None: + if not text: + warnings.warn( + "content_type without text is deprecated " + "since 4.0 and scheduled for removal in 5.0 " + "(#3462)", + DeprecationWarning, + stacklevel=2, + ) + real_headers[hdrs.CONTENT_TYPE] = content_type + elif hdrs.CONTENT_TYPE not in real_headers and text: + real_headers[hdrs.CONTENT_TYPE] = "text/plain" + + self._reason = reason + self._text = text + self._headers = real_headers + self.args = () + + def __bool__(self) -> bool: + return True + + @property + def status(self) -> int: + return self.status_code + + @property + def reason(self) -> str: + return self._reason + + @property + def text(self) -> Optional[str]: + return self._text + + @property + def headers(self) -> "CIMultiDict[str]": + return self._headers + + def __str__(self) -> str: + return self.reason + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}: {self.reason}>" + + __reduce__ = object.__reduce__ + + def __getnewargs__(self) -> Tuple[Any, ...]: + return self.args + + +class HTTPError(HTTPException): + """Base class for exceptions with status codes in the 400s and 500s.""" + + +class HTTPRedirection(HTTPException): + """Base class for exceptions with status codes in the 300s.""" + + +class HTTPSuccessful(HTTPException): + """Base class for exceptions with status codes in the 200s.""" + + +class HTTPOk(HTTPSuccessful): + status_code = 200 + + +class HTTPCreated(HTTPSuccessful): + status_code = 201 + + +class HTTPAccepted(HTTPSuccessful): + status_code = 202 + + +class HTTPNonAuthoritativeInformation(HTTPSuccessful): + status_code = 203 + + +class HTTPNoContent(HTTPSuccessful): + status_code = 204 + empty_body = True + + +class HTTPResetContent(HTTPSuccessful): + status_code = 205 + empty_body = True + + +class HTTPPartialContent(HTTPSuccessful): + status_code = 206 + + +############################################################ +# 3xx redirection +############################################################ + + +class HTTPMove(HTTPRedirection): + def __init__( + self, + location: StrOrURL, + *, + headers: Optional[LooseHeaders] = None, + reason: Optional[str] = None, + text: Optional[str] = None, + content_type: Optional[str] = None, + ) -> None: + if not location: + raise ValueError("HTTP redirects need a location to redirect to.") + super().__init__( + headers=headers, reason=reason, text=text, content_type=content_type + ) + self._location = URL(location) + self.headers["Location"] = str(self.location) + + @property + def location(self) -> URL: + return self._location + + +class HTTPMultipleChoices(HTTPMove): + status_code = 300 + + +class HTTPMovedPermanently(HTTPMove): + status_code = 301 + + +class HTTPFound(HTTPMove): + status_code = 302 + + +# This one is safe after a POST (the redirected location will be +# retrieved with GET): +class HTTPSeeOther(HTTPMove): + status_code = 303 + + +class HTTPNotModified(HTTPRedirection): + # FIXME: this should include a date or etag header + status_code = 304 + empty_body = True + + +class HTTPUseProxy(HTTPMove): + # Not a move, but looks a little like one + status_code = 305 + + +class HTTPTemporaryRedirect(HTTPMove): + status_code = 307 + + +class HTTPPermanentRedirect(HTTPMove): + status_code = 308 + + +############################################################ +# 4xx client error +############################################################ + + +class HTTPClientError(HTTPError): + pass + + +class HTTPBadRequest(HTTPClientError): + status_code = 400 + + +class HTTPUnauthorized(HTTPClientError): + status_code = 401 + + +class HTTPPaymentRequired(HTTPClientError): + status_code = 402 + + +class HTTPForbidden(HTTPClientError): + status_code = 403 + + +class HTTPNotFound(HTTPClientError): + status_code = 404 + + +class HTTPMethodNotAllowed(HTTPClientError): + status_code = 405 + + def __init__( + self, + method: str, + allowed_methods: Iterable[str], + *, + headers: Optional[LooseHeaders] = None, + reason: Optional[str] = None, + text: Optional[str] = None, + content_type: Optional[str] = None, + ) -> None: + allow = ",".join(sorted(allowed_methods)) + super().__init__( + headers=headers, reason=reason, text=text, content_type=content_type + ) + self.headers["Allow"] = allow + self._allowed: Set[str] = set(allowed_methods) + self._method = method + + @property + def allowed_methods(self) -> Set[str]: + return self._allowed + + @property + def method(self) -> str: + return self._method + + +class HTTPNotAcceptable(HTTPClientError): + status_code = 406 + + +class HTTPProxyAuthenticationRequired(HTTPClientError): + status_code = 407 + + +class HTTPRequestTimeout(HTTPClientError): + status_code = 408 + + +class HTTPConflict(HTTPClientError): + status_code = 409 + + +class HTTPGone(HTTPClientError): + status_code = 410 + + +class HTTPLengthRequired(HTTPClientError): + status_code = 411 + + +class HTTPPreconditionFailed(HTTPClientError): + status_code = 412 + + +class HTTPRequestEntityTooLarge(HTTPClientError): + status_code = 413 + + def __init__(self, max_size: int, actual_size: int, **kwargs: Any) -> None: + kwargs.setdefault( + "text", + "Maximum request body size {} exceeded, " + "actual body size {}".format(max_size, actual_size), + ) + super().__init__(**kwargs) + + +class HTTPRequestURITooLong(HTTPClientError): + status_code = 414 + + +class HTTPUnsupportedMediaType(HTTPClientError): + status_code = 415 + + +class HTTPRequestRangeNotSatisfiable(HTTPClientError): + status_code = 416 + + +class HTTPExpectationFailed(HTTPClientError): + status_code = 417 + + +class HTTPMisdirectedRequest(HTTPClientError): + status_code = 421 + + +class HTTPUnprocessableEntity(HTTPClientError): + status_code = 422 + + +class HTTPFailedDependency(HTTPClientError): + status_code = 424 + + +class HTTPUpgradeRequired(HTTPClientError): + status_code = 426 + + +class HTTPPreconditionRequired(HTTPClientError): + status_code = 428 + + +class HTTPTooManyRequests(HTTPClientError): + status_code = 429 + + +class HTTPRequestHeaderFieldsTooLarge(HTTPClientError): + status_code = 431 + + +class HTTPUnavailableForLegalReasons(HTTPClientError): + status_code = 451 + + def __init__( + self, + link: StrOrURL, + *, + headers: Optional[LooseHeaders] = None, + reason: Optional[str] = None, + text: Optional[str] = None, + content_type: Optional[str] = None, + ) -> None: + super().__init__( + headers=headers, reason=reason, text=text, content_type=content_type + ) + self.headers["Link"] = f'<{str(link)}>; rel="blocked-by"' + self._link = URL(link) + + @property + def link(self) -> URL: + return self._link + + +############################################################ +# 5xx Server Error +############################################################ +# Response status codes beginning with the digit "5" indicate cases in +# which the server is aware that it has erred or is incapable of +# performing the request. Except when responding to a HEAD request, the +# server SHOULD include an entity containing an explanation of the error +# situation, and whether it is a temporary or permanent condition. User +# agents SHOULD display any included entity to the user. These response +# codes are applicable to any request method. + + +class HTTPServerError(HTTPError): + pass + + +class HTTPInternalServerError(HTTPServerError): + status_code = 500 + + +class HTTPNotImplemented(HTTPServerError): + status_code = 501 + + +class HTTPBadGateway(HTTPServerError): + status_code = 502 + + +class HTTPServiceUnavailable(HTTPServerError): + status_code = 503 + + +class HTTPGatewayTimeout(HTTPServerError): + status_code = 504 + + +class HTTPVersionNotSupported(HTTPServerError): + status_code = 505 + + +class HTTPVariantAlsoNegotiates(HTTPServerError): + status_code = 506 + + +class HTTPInsufficientStorage(HTTPServerError): + status_code = 507 + + +class HTTPNotExtended(HTTPServerError): + status_code = 510 + + +class HTTPNetworkAuthenticationRequired(HTTPServerError): + status_code = 511 + + +def _initialize_default_reason() -> None: + for obj in globals().values(): + if isinstance(obj, type) and issubclass(obj, HTTPException): + if obj.status_code >= 0: + try: + status = HTTPStatus(obj.status_code) + obj.default_reason = status.phrase + except ValueError: + pass + + +_initialize_default_reason() +del _initialize_default_reason diff --git a/aiohttp/web_fileresponse.py b/aiohttp/web_fileresponse.py new file mode 100644 index 0000000..0fbdeab --- /dev/null +++ b/aiohttp/web_fileresponse.py @@ -0,0 +1,281 @@ +import asyncio +import mimetypes +import os +import pathlib +from typing import ( + IO, + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Final, + Optional, + Tuple, + cast, +) + +from . import hdrs +from .abc import AbstractStreamWriter +from .helpers import ETAG_ANY, ETag +from .typedefs import LooseHeaders, PathLike +from .web_exceptions import ( + HTTPNotModified, + HTTPPartialContent, + HTTPPreconditionFailed, + HTTPRequestRangeNotSatisfiable, +) +from .web_response import StreamResponse + +__all__ = ("FileResponse",) + +if TYPE_CHECKING: # pragma: no cover + from .web_request import BaseRequest + + +_T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]] + + +NOSENDFILE: Final[bool] = bool(os.environ.get("AIOHTTP_NOSENDFILE")) + + +class FileResponse(StreamResponse): + """A response object can be used to send files.""" + + def __init__( + self, + path: PathLike, + chunk_size: int = 256 * 1024, + status: int = 200, + reason: Optional[str] = None, + headers: Optional[LooseHeaders] = None, + ) -> None: + super().__init__(status=status, reason=reason, headers=headers) + + self._path = pathlib.Path(path) + self._chunk_size = chunk_size + + async def _sendfile_fallback( + self, writer: AbstractStreamWriter, fobj: IO[Any], offset: int, count: int + ) -> AbstractStreamWriter: + # To keep memory usage low,fobj is transferred in chunks + # controlled by the constructor's chunk_size argument. + + chunk_size = self._chunk_size + loop = asyncio.get_event_loop() + + await loop.run_in_executor(None, fobj.seek, offset) + + chunk = await loop.run_in_executor(None, fobj.read, chunk_size) + while chunk: + await writer.write(chunk) + count = count - chunk_size + if count <= 0: + break + chunk = await loop.run_in_executor(None, fobj.read, min(chunk_size, count)) + + await writer.drain() + return writer + + async def _sendfile( + self, request: "BaseRequest", fobj: IO[Any], offset: int, count: int + ) -> AbstractStreamWriter: + writer = await super().prepare(request) + assert writer is not None + + if NOSENDFILE or self.compression: + return await self._sendfile_fallback(writer, fobj, offset, count) + + loop = request._loop + transport = request.transport + assert transport is not None + + try: + await loop.sendfile(transport, fobj, offset, count) + except NotImplementedError: + return await self._sendfile_fallback(writer, fobj, offset, count) + + await super().write_eof() + return writer + + @staticmethod + def _strong_etag_match(etag_value: str, etags: Tuple[ETag, ...]) -> bool: + if len(etags) == 1 and etags[0].value == ETAG_ANY: + return True + return any(etag.value == etag_value for etag in etags if not etag.is_weak) + + async def _not_modified( + self, request: "BaseRequest", etag_value: str, last_modified: float + ) -> Optional[AbstractStreamWriter]: + self.set_status(HTTPNotModified.status_code) + self._length_check = False + self.etag = etag_value # type: ignore[assignment] + self.last_modified = last_modified # type: ignore[assignment] + # Delete any Content-Length headers provided by user. HTTP 304 + # should always have empty response body + return await super().prepare(request) + + async def _precondition_failed( + self, request: "BaseRequest" + ) -> Optional[AbstractStreamWriter]: + self.set_status(HTTPPreconditionFailed.status_code) + self.content_length = 0 + return await super().prepare(request) + + async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter]: + filepath = self._path + + gzip = False + if "gzip" in request.headers.get(hdrs.ACCEPT_ENCODING, ""): + gzip_path = filepath.with_name(filepath.name + ".gz") + + if gzip_path.is_file(): + filepath = gzip_path + gzip = True + + loop = asyncio.get_event_loop() + st: os.stat_result = await loop.run_in_executor(None, filepath.stat) + + etag_value = f"{st.st_mtime_ns:x}-{st.st_size:x}" + last_modified = st.st_mtime + + # https://tools.ietf.org/html/rfc7232#section-6 + ifmatch = request.if_match + if ifmatch is not None and not self._strong_etag_match(etag_value, ifmatch): + return await self._precondition_failed(request) + + unmodsince = request.if_unmodified_since + if ( + unmodsince is not None + and ifmatch is None + and st.st_mtime > unmodsince.timestamp() + ): + return await self._precondition_failed(request) + + ifnonematch = request.if_none_match + if ifnonematch is not None and self._strong_etag_match(etag_value, ifnonematch): + return await self._not_modified(request, etag_value, last_modified) + + modsince = request.if_modified_since + if ( + modsince is not None + and ifnonematch is None + and st.st_mtime <= modsince.timestamp() + ): + return await self._not_modified(request, etag_value, last_modified) + + ct = None + if hdrs.CONTENT_TYPE not in self.headers: + ct, encoding = mimetypes.guess_type(str(filepath)) + if not ct: + ct = "application/octet-stream" + else: + encoding = "gzip" if gzip else None + + status = self._status + file_size = st.st_size + count = file_size + + start = None + + ifrange = request.if_range + if ifrange is None or st.st_mtime <= ifrange.timestamp(): + # If-Range header check: + # condition = cached date >= last modification date + # return 206 if True else 200. + # if False: + # Range header would not be processed, return 200 + # if True but Range header missing + # return 200 + try: + rng = request.http_range + start = rng.start + end = rng.stop + except ValueError: + # https://tools.ietf.org/html/rfc7233: + # A server generating a 416 (Range Not Satisfiable) response to + # a byte-range request SHOULD send a Content-Range header field + # with an unsatisfied-range value. + # The complete-length in a 416 response indicates the current + # length of the selected representation. + # + # Will do the same below. Many servers ignore this and do not + # send a Content-Range header with HTTP 416 + self.headers[hdrs.CONTENT_RANGE] = f"bytes */{file_size}" + self.set_status(HTTPRequestRangeNotSatisfiable.status_code) + return await super().prepare(request) + + # If a range request has been made, convert start, end slice + # notation into file pointer offset and count + if start is not None or end is not None: + if start < 0 and end is None: # return tail of file + start += file_size + if start < 0: + # if Range:bytes=-1000 in request header but file size + # is only 200, there would be trouble without this + start = 0 + count = file_size - start + else: + # rfc7233:If the last-byte-pos value is + # absent, or if the value is greater than or equal to + # the current length of the representation data, + # the byte range is interpreted as the remainder + # of the representation (i.e., the server replaces the + # value of last-byte-pos with a value that is one less than + # the current length of the selected representation). + count = ( + min(end if end is not None else file_size, file_size) - start + ) + + if start >= file_size: + # HTTP 416 should be returned in this case. + # + # According to https://tools.ietf.org/html/rfc7233: + # If a valid byte-range-set includes at least one + # byte-range-spec with a first-byte-pos that is less than + # the current length of the representation, or at least one + # suffix-byte-range-spec with a non-zero suffix-length, + # then the byte-range-set is satisfiable. Otherwise, the + # byte-range-set is unsatisfiable. + self.headers[hdrs.CONTENT_RANGE] = f"bytes */{file_size}" + self.set_status(HTTPRequestRangeNotSatisfiable.status_code) + return await super().prepare(request) + + status = HTTPPartialContent.status_code + # Even though you are sending the whole file, you should still + # return a HTTP 206 for a Range request. + self.set_status(status) + + if ct: + self.content_type = ct + if encoding: + self.headers[hdrs.CONTENT_ENCODING] = encoding + if gzip: + self.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING + + self.etag = etag_value # type: ignore[assignment] + self.last_modified = st.st_mtime # type: ignore[assignment] + self.content_length = count + + self.headers[hdrs.ACCEPT_RANGES] = "bytes" + + real_start = cast(int, start) + + if status == HTTPPartialContent.status_code: + self.headers[hdrs.CONTENT_RANGE] = "bytes {}-{}/{}".format( + real_start, real_start + count - 1, file_size + ) + + # If we are sending 0 bytes calling sendfile() will throw a ValueError + if count == 0 or request.method == hdrs.METH_HEAD or self.status in [204, 304]: + return await super().prepare(request) + + fobj = await loop.run_in_executor(None, filepath.open, "rb") + if start: # be aware that start could be None or int=0 here. + offset = start + else: + offset = 0 + + try: + return await self._sendfile(request, fobj, offset, count) + finally: + await asyncio.shield(loop.run_in_executor(None, fobj.close)) diff --git a/aiohttp/web_log.py b/aiohttp/web_log.py new file mode 100644 index 0000000..633e9e3 --- /dev/null +++ b/aiohttp/web_log.py @@ -0,0 +1,213 @@ +import datetime +import functools +import logging +import os +import re +import time as time_mod +from collections import namedtuple +from typing import Any, Callable, Dict, Iterable, List, Tuple # noqa + +from .abc import AbstractAccessLogger +from .web_request import BaseRequest +from .web_response import StreamResponse + +KeyMethod = namedtuple("KeyMethod", "key method") + + +class AccessLogger(AbstractAccessLogger): + """Helper object to log access. + + Usage: + log = logging.getLogger("spam") + log_format = "%a %{User-Agent}i" + access_logger = AccessLogger(log, log_format) + access_logger.log(request, response, time) + + Format: + %% The percent sign + %a Remote IP-address (IP-address of proxy if using reverse proxy) + %t Time when the request was started to process + %P The process ID of the child that serviced the request + %r First line of request + %s Response status code + %b Size of response in bytes, including HTTP headers + %T Time taken to serve the request, in seconds + %Tf Time taken to serve the request, in seconds with floating fraction + in .06f format + %D Time taken to serve the request, in microseconds + %{FOO}i request.headers['FOO'] + %{FOO}o response.headers['FOO'] + %{FOO}e os.environ['FOO'] + + """ + + LOG_FORMAT_MAP = { + "a": "remote_address", + "t": "request_start_time", + "P": "process_id", + "r": "first_request_line", + "s": "response_status", + "b": "response_size", + "T": "request_time", + "Tf": "request_time_frac", + "D": "request_time_micro", + "i": "request_header", + "o": "response_header", + } + + LOG_FORMAT = '%a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"' + FORMAT_RE = re.compile(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbOD]|Tf?)") + CLEANUP_RE = re.compile(r"(%[^s])") + _FORMAT_CACHE: Dict[str, Tuple[str, List[KeyMethod]]] = {} + + def __init__(self, logger: logging.Logger, log_format: str = LOG_FORMAT) -> None: + """Initialise the logger. + + logger is a logger object to be used for logging. + log_format is a string with apache compatible log format description. + + """ + super().__init__(logger, log_format=log_format) + + _compiled_format = AccessLogger._FORMAT_CACHE.get(log_format) + if not _compiled_format: + _compiled_format = self.compile_format(log_format) + AccessLogger._FORMAT_CACHE[log_format] = _compiled_format + + self._log_format, self._methods = _compiled_format + + def compile_format(self, log_format: str) -> Tuple[str, List[KeyMethod]]: + """Translate log_format into form usable by modulo formatting + + All known atoms will be replaced with %s + Also methods for formatting of those atoms will be added to + _methods in appropriate order + + For example we have log_format = "%a %t" + This format will be translated to "%s %s" + Also contents of _methods will be + [self._format_a, self._format_t] + These method will be called and results will be passed + to translated string format. + + Each _format_* method receive 'args' which is list of arguments + given to self.log + + Exceptions are _format_e, _format_i and _format_o methods which + also receive key name (by functools.partial) + + """ + # list of (key, method) tuples, we don't use an OrderedDict as users + # can repeat the same key more than once + methods = list() + + for atom in self.FORMAT_RE.findall(log_format): + if atom[1] == "": + format_key1 = self.LOG_FORMAT_MAP[atom[0]] + m = getattr(AccessLogger, "_format_%s" % atom[0]) + key_method = KeyMethod(format_key1, m) + else: + format_key2 = (self.LOG_FORMAT_MAP[atom[2]], atom[1]) + m = getattr(AccessLogger, "_format_%s" % atom[2]) + key_method = KeyMethod(format_key2, functools.partial(m, atom[1])) + + methods.append(key_method) + + log_format = self.FORMAT_RE.sub(r"%s", log_format) + log_format = self.CLEANUP_RE.sub(r"%\1", log_format) + return log_format, methods + + @staticmethod + def _format_i( + key: str, request: BaseRequest, response: StreamResponse, time: float + ) -> str: + if request is None: + return "(no headers)" + + # suboptimal, make istr(key) once + return request.headers.get(key, "-") + + @staticmethod + def _format_o( + key: str, request: BaseRequest, response: StreamResponse, time: float + ) -> str: + # suboptimal, make istr(key) once + return response.headers.get(key, "-") + + @staticmethod + def _format_a(request: BaseRequest, response: StreamResponse, time: float) -> str: + if request is None: + return "-" + ip = request.remote + return ip if ip is not None else "-" + + @staticmethod + def _format_t(request: BaseRequest, response: StreamResponse, time: float) -> str: + tz = datetime.timezone(datetime.timedelta(seconds=-time_mod.timezone)) + now = datetime.datetime.now(tz) + start_time = now - datetime.timedelta(seconds=time) + return start_time.strftime("[%d/%b/%Y:%H:%M:%S %z]") + + @staticmethod + def _format_P(request: BaseRequest, response: StreamResponse, time: float) -> str: + return "<%s>" % os.getpid() + + @staticmethod + def _format_r(request: BaseRequest, response: StreamResponse, time: float) -> str: + if request is None: + return "-" + return "{} {} HTTP/{}.{}".format( + request.method, + request.path_qs, + request.version.major, + request.version.minor, + ) + + @staticmethod + def _format_s(request: BaseRequest, response: StreamResponse, time: float) -> int: + return response.status + + @staticmethod + def _format_b(request: BaseRequest, response: StreamResponse, time: float) -> int: + return response.body_length + + @staticmethod + def _format_T(request: BaseRequest, response: StreamResponse, time: float) -> str: + return str(round(time)) + + @staticmethod + def _format_Tf(request: BaseRequest, response: StreamResponse, time: float) -> str: + return "%06f" % time + + @staticmethod + def _format_D(request: BaseRequest, response: StreamResponse, time: float) -> str: + return str(round(time * 1000000)) + + def _format_line( + self, request: BaseRequest, response: StreamResponse, time: float + ) -> Iterable[Tuple[str, Callable[[BaseRequest, StreamResponse, float], str]]]: + return [(key, method(request, response, time)) for key, method in self._methods] + + def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None: + if not self.logger.isEnabledFor(logging.INFO): + # Avoid formatting the log line if it will not be emitted. + return + try: + fmt_info = self._format_line(request, response, time) + + values = list() + extra = dict() + for key, value in fmt_info: + values.append(value) + + if key.__class__ is str: + extra[key] = value + else: + k1, k2 = key # type: ignore[misc] + dct = extra.get(k1, {}) # type: ignore[var-annotated,has-type] + dct[k2] = value # type: ignore[index,has-type] + extra[k1] = dct # type: ignore[has-type,assignment] + + self.logger.info(self._log_format % tuple(values), extra=extra) + except Exception: + self.logger.exception("Error in logging") diff --git a/aiohttp/web_middlewares.py b/aiohttp/web_middlewares.py new file mode 100644 index 0000000..4583824 --- /dev/null +++ b/aiohttp/web_middlewares.py @@ -0,0 +1,121 @@ +import re +import warnings +from typing import TYPE_CHECKING, Tuple, Type, TypeVar + +from .typedefs import Handler, Middleware +from .web_exceptions import HTTPMove, HTTPPermanentRedirect +from .web_request import Request +from .web_response import StreamResponse +from .web_urldispatcher import SystemRoute + +__all__ = ( + "middleware", + "normalize_path_middleware", +) + +if TYPE_CHECKING: # pragma: no cover + from .web_app import Application + +_Func = TypeVar("_Func") + + +async def _check_request_resolves(request: Request, path: str) -> Tuple[bool, Request]: + alt_request = request.clone(rel_url=path) + + match_info = await request.app.router.resolve(alt_request) + alt_request._match_info = match_info + + if match_info.http_exception is None: + return True, alt_request + + return False, request + + +def middleware(f: _Func) -> _Func: + warnings.warn( + "Middleware decorator is deprecated since 4.0 " + "and its behaviour is default, " + "you can simply remove this decorator.", + DeprecationWarning, + stacklevel=2, + ) + return f + + +def normalize_path_middleware( + *, + append_slash: bool = True, + remove_slash: bool = False, + merge_slashes: bool = True, + redirect_class: Type[HTTPMove] = HTTPPermanentRedirect, +) -> Middleware: + """Factory for producing a middleware that normalizes the path of a request. + + Normalizing means: + - Add or remove a trailing slash to the path. + - Double slashes are replaced by one. + + The middleware returns as soon as it finds a path that resolves + correctly. The order if both merge and append/remove are enabled is + 1) merge slashes + 2) append/remove slash + 3) both merge slashes and append/remove slash. + If the path resolves with at least one of those conditions, it will + redirect to the new path. + + Only one of `append_slash` and `remove_slash` can be enabled. If both + are `True` the factory will raise an assertion error + + If `append_slash` is `True` the middleware will append a slash when + needed. If a resource is defined with trailing slash and the request + comes without it, it will append it automatically. + + If `remove_slash` is `True`, `append_slash` must be `False`. When enabled + the middleware will remove trailing slashes and redirect if the resource + is defined + + If merge_slashes is True, merge multiple consecutive slashes in the + path into one. + """ + correct_configuration = not (append_slash and remove_slash) + assert correct_configuration, "Cannot both remove and append slash" + + async def impl(request: Request, handler: Handler) -> StreamResponse: + if isinstance(request.match_info.route, SystemRoute): + paths_to_check = [] + if "?" in request.raw_path: + path, query = request.raw_path.split("?", 1) + query = "?" + query + else: + query = "" + path = request.raw_path + + if merge_slashes: + paths_to_check.append(re.sub("//+", "/", path)) + if append_slash and not request.path.endswith("/"): + paths_to_check.append(path + "/") + if remove_slash and request.path.endswith("/"): + paths_to_check.append(path[:-1]) + if merge_slashes and append_slash: + paths_to_check.append(re.sub("//+", "/", path + "/")) + if merge_slashes and remove_slash and path.endswith("/"): + merged_slashes = re.sub("//+", "/", path) + paths_to_check.append(merged_slashes[:-1]) + + for path in paths_to_check: + path = re.sub("^//+", "/", path) # SECURITY: GHSA-v6wp-4m6f-gcjg + resolves, request = await _check_request_resolves(request, path) + if resolves: + raise redirect_class(request.raw_path + query) + + return await handler(request) + + return impl + + +def _fix_request_current_app(app: "Application") -> Middleware: + async def impl(request: Request, handler: Handler) -> StreamResponse: + with request.match_info.set_current_app(app): + return await handler(request) + + return impl diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py new file mode 100644 index 0000000..ae0aaa8 --- /dev/null +++ b/aiohttp/web_protocol.py @@ -0,0 +1,711 @@ +import asyncio +import asyncio.streams +import dataclasses +import traceback +from collections import deque +from contextlib import suppress +from html import escape as html_escape +from http import HTTPStatus +from logging import Logger +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Deque, + Optional, + Sequence, + Tuple, + Type, + Union, + cast, +) + +import yarl + +from .abc import AbstractAccessLogger, AbstractAsyncAccessLogger, AbstractStreamWriter +from .base_protocol import BaseProtocol +from .helpers import ceil_timeout +from .http import ( + HttpProcessingError, + HttpRequestParser, + HttpVersion10, + RawRequestMessage, + StreamWriter, +) +from .log import access_logger, server_logger +from .streams import EMPTY_PAYLOAD, StreamReader +from .tcp_helpers import tcp_keepalive +from .web_exceptions import HTTPException +from .web_log import AccessLogger +from .web_request import BaseRequest +from .web_response import Response, StreamResponse + +__all__ = ("RequestHandler", "RequestPayloadError", "PayloadAccessError") + +if TYPE_CHECKING: # pragma: no cover + from .web_server import Server + + +_RequestFactory = Callable[ + [ + RawRequestMessage, + StreamReader, + "RequestHandler", + AbstractStreamWriter, + "asyncio.Task[None]", + ], + BaseRequest, +] + +_RequestHandler = Callable[[BaseRequest], Awaitable[StreamResponse]] +_AnyAbstractAccessLogger = Union[ + Type[AbstractAsyncAccessLogger], + Type[AbstractAccessLogger], +] + +ERROR = RawRequestMessage( + "UNKNOWN", + "/", + HttpVersion10, + {}, # type: ignore[arg-type] + {}, # type: ignore[arg-type] + True, + None, + False, + False, + yarl.URL("/"), +) + + +class RequestPayloadError(Exception): + """Payload parsing error.""" + + +class PayloadAccessError(Exception): + """Payload was accessed after response was sent.""" + + +class AccessLoggerWrapper(AbstractAsyncAccessLogger): + """Wrap an AbstractAccessLogger so it behaves like an AbstractAsyncAccessLogger.""" + + def __init__( + self, access_logger: AbstractAccessLogger, loop: asyncio.AbstractEventLoop + ) -> None: + self.access_logger = access_logger + self._loop = loop + super().__init__() + + async def log( + self, request: BaseRequest, response: StreamResponse, request_start: float + ) -> None: + self.access_logger.log(request, response, self._loop.time() - request_start) + + +@dataclasses.dataclass(frozen=True) +class _ErrInfo: + status: int + exc: BaseException + message: str + + +_MsgType = Tuple[Union[RawRequestMessage, _ErrInfo], StreamReader] + + +class RequestHandler(BaseProtocol): + """HTTP protocol implementation. + + RequestHandler handles incoming HTTP request. It reads request line, + request headers and request payload and calls handle_request() method. + By default it always returns with 404 response. + + RequestHandler handles errors in incoming request, like bad + status line, bad headers or incomplete payload. If any error occurs, + connection gets closed. + + keepalive_timeout -- number of seconds before closing + keep-alive connection + + tcp_keepalive -- TCP keep-alive is on, default is on + + logger -- custom logger object + + access_log_class -- custom class for access_logger + + access_log -- custom logging object + + access_log_format -- access log format string + + loop -- Optional event loop + + max_line_size -- Optional maximum header line size + + max_field_size -- Optional maximum header field size + + timeout_ceil_threshold -- Optional value to specify + threshold to ceil() timeout + values + + """ + + KEEPALIVE_RESCHEDULE_DELAY = 1 + + __slots__ = ( + "_request_count", + "_keepalive", + "_manager", + "_request_handler", + "_request_factory", + "_tcp_keepalive", + "_keepalive_time", + "_keepalive_handle", + "_keepalive_timeout", + "_lingering_time", + "_messages", + "_message_tail", + "_waiter", + "_task_handler", + "_upgrade", + "_payload_parser", + "_request_parser", + "logger", + "access_log", + "access_logger", + "_close", + "_force_close", + "_current_request", + "_timeout_ceil_threshold", + ) + + def __init__( + self, + manager: "Server", + *, + loop: asyncio.AbstractEventLoop, + keepalive_timeout: float = 75.0, # NGINX default is 75 secs + tcp_keepalive: bool = True, + logger: Logger = server_logger, + access_log_class: _AnyAbstractAccessLogger = AccessLogger, + access_log: Optional[Logger] = access_logger, + access_log_format: str = AccessLogger.LOG_FORMAT, + max_line_size: int = 8190, + max_field_size: int = 8190, + lingering_time: float = 10.0, + read_bufsize: int = 2**16, + auto_decompress: bool = True, + timeout_ceil_threshold: float = 5, + ): + super().__init__(loop) + + self._request_count = 0 + self._keepalive = False + self._current_request: Optional[BaseRequest] = None + self._manager: Optional[Server] = manager + self._request_handler: Optional[_RequestHandler] = manager.request_handler + self._request_factory: Optional[_RequestFactory] = manager.request_factory + + self._tcp_keepalive = tcp_keepalive + # placeholder to be replaced on keepalive timeout setup + self._keepalive_time = 0.0 + self._keepalive_handle: Optional[asyncio.Handle] = None + self._keepalive_timeout = keepalive_timeout + self._lingering_time = float(lingering_time) + + self._messages: Deque[_MsgType] = deque() + self._message_tail = b"" + + self._waiter: Optional[asyncio.Future[None]] = None + self._task_handler: Optional[asyncio.Task[None]] = None + + self._upgrade = False + self._payload_parser: Any = None + self._request_parser: Optional[HttpRequestParser] = HttpRequestParser( + self, + loop, + read_bufsize, + max_line_size=max_line_size, + max_field_size=max_field_size, + payload_exception=RequestPayloadError, + auto_decompress=auto_decompress, + ) + + self._timeout_ceil_threshold: float = 5 + try: + self._timeout_ceil_threshold = float(timeout_ceil_threshold) + except (TypeError, ValueError): + pass + + self.logger = logger + self.access_log = access_log + if access_log: + if issubclass(access_log_class, AbstractAsyncAccessLogger): + self.access_logger: Optional[ + AbstractAsyncAccessLogger + ] = access_log_class() + else: + access_logger = access_log_class(access_log, access_log_format) + self.access_logger = AccessLoggerWrapper( + access_logger, + self._loop, + ) + else: + self.access_logger = None + + self._close = False + self._force_close = False + + def __repr__(self) -> str: + return "<{} {}>".format( + self.__class__.__name__, + "connected" if self.transport is not None else "disconnected", + ) + + @property + def keepalive_timeout(self) -> float: + return self._keepalive_timeout + + async def shutdown(self, timeout: Optional[float] = 15.0) -> None: + """Do worker process exit preparations. + + We need to clean up everything and stop accepting requests. + It is especially important for keep-alive connections. + """ + self._force_close = True + + if self._keepalive_handle is not None: + self._keepalive_handle.cancel() + + if self._waiter: + self._waiter.cancel() + + # wait for handlers + with suppress(asyncio.CancelledError, asyncio.TimeoutError): + async with ceil_timeout(timeout): + if self._current_request is not None: + self._current_request._cancel(asyncio.CancelledError()) + + if self._task_handler is not None and not self._task_handler.done(): + await self._task_handler + + # force-close non-idle handler + if self._task_handler is not None: + self._task_handler.cancel() + + if self.transport is not None: + self.transport.close() + self.transport = None + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + super().connection_made(transport) + + real_transport = cast(asyncio.Transport, transport) + if self._tcp_keepalive: + tcp_keepalive(real_transport) + + self._task_handler = self._loop.create_task(self.start()) + assert self._manager is not None + self._manager.connection_made(self, real_transport) + + def connection_lost(self, exc: Optional[BaseException]) -> None: + if self._manager is None: + return + self._manager.connection_lost(self, exc) + + super().connection_lost(exc) + + # Grab value before setting _manager to None. + handler_cancellation = self._manager.handler_cancellation + + self._manager = None + self._force_close = True + self._request_factory = None + self._request_handler = None + self._request_parser = None + + if self._keepalive_handle is not None: + self._keepalive_handle.cancel() + + if self._current_request is not None: + if exc is None: + exc = ConnectionResetError("Connection lost") + self._current_request._cancel(exc) + + if self._waiter is not None: + self._waiter.cancel() + + if handler_cancellation and self._task_handler is not None: + self._task_handler.cancel() + + self._task_handler = None + + if self._payload_parser is not None: + self._payload_parser.feed_eof() + self._payload_parser = None + + def set_parser(self, parser: Any) -> None: + # Actual type is WebReader + assert self._payload_parser is None + + self._payload_parser = parser + + if self._message_tail: + self._payload_parser.feed_data(self._message_tail) + self._message_tail = b"" + + def eof_received(self) -> None: + pass + + def data_received(self, data: bytes) -> None: + if self._force_close or self._close: + return + # parse http messages + messages: Sequence[_MsgType] + if self._payload_parser is None and not self._upgrade: + assert self._request_parser is not None + try: + messages, upgraded, tail = self._request_parser.feed_data(data) + except HttpProcessingError as exc: + messages = [ + (_ErrInfo(status=400, exc=exc, message=exc.message), EMPTY_PAYLOAD) + ] + upgraded = False + tail = b"" + + for msg, payload in messages or (): + self._request_count += 1 + self._messages.append((msg, payload)) + + waiter = self._waiter + if messages and waiter is not None and not waiter.done(): + # don't set result twice + waiter.set_result(None) + + self._upgrade = upgraded + if upgraded and tail: + self._message_tail = tail + + # no parser, just store + elif self._payload_parser is None and self._upgrade and data: + self._message_tail += data + + # feed payload + elif data: + eof, tail = self._payload_parser.feed_data(data) + if eof: + self.close() + + def keep_alive(self, val: bool) -> None: + """Set keep-alive connection mode. + + :param bool val: new state. + """ + self._keepalive = val + if self._keepalive_handle: + self._keepalive_handle.cancel() + self._keepalive_handle = None + + def close(self) -> None: + """Close connection. + + Stop accepting new pipelining messages and close + connection when handlers done processing messages. + """ + self._close = True + if self._waiter: + self._waiter.cancel() + + def force_close(self) -> None: + """Forcefully close connection.""" + self._force_close = True + if self._waiter: + self._waiter.cancel() + if self.transport is not None: + self.transport.close() + self.transport = None + + async def log_access( + self, request: BaseRequest, response: StreamResponse, request_start: float + ) -> None: + if self.access_logger is not None: + await self.access_logger.log(request, response, request_start) + + def log_debug(self, *args: Any, **kw: Any) -> None: + if self._loop.get_debug(): + self.logger.debug(*args, **kw) + + def log_exception(self, *args: Any, **kw: Any) -> None: + self.logger.exception(*args, **kw) + + def _process_keepalive(self) -> None: + if self._force_close or not self._keepalive: + return + + next = self._keepalive_time + self._keepalive_timeout + + # handler in idle state + if self._waiter: + if self._loop.time() > next: + self.force_close() + return + + # not all request handlers are done, + # reschedule itself to next second + self._keepalive_handle = self._loop.call_later( + self.KEEPALIVE_RESCHEDULE_DELAY, + self._process_keepalive, + ) + + async def _handle_request( + self, + request: BaseRequest, + start_time: float, + request_handler: Callable[[BaseRequest], Awaitable[StreamResponse]], + ) -> Tuple[StreamResponse, bool]: + assert self._request_handler is not None + try: + try: + self._current_request = request + resp = await request_handler(request) + finally: + self._current_request = None + except HTTPException as exc: + resp = Response( + status=exc.status, reason=exc.reason, text=exc.text, headers=exc.headers + ) + resp._cookies = exc._cookies + reset = await self.finish_response(request, resp, start_time) + except asyncio.CancelledError: + raise + except asyncio.TimeoutError as exc: + self.log_debug("Request handler timed out.", exc_info=exc) + resp = self.handle_error(request, 504) + reset = await self.finish_response(request, resp, start_time) + except Exception as exc: + resp = self.handle_error(request, 500, exc) + reset = await self.finish_response(request, resp, start_time) + else: + reset = await self.finish_response(request, resp, start_time) + + return resp, reset + + async def start(self) -> None: + """Process incoming request. + + It reads request line, request headers and request payload, then + calls handle_request() method. Subclass has to override + handle_request(). start() handles various exceptions in request + or response handling. Connection is being closed always unless + keep_alive(True) specified. + """ + loop = self._loop + handler = self._task_handler + assert handler is not None + manager = self._manager + assert manager is not None + keepalive_timeout = self._keepalive_timeout + resp = None + assert self._request_factory is not None + assert self._request_handler is not None + + while not self._force_close: + if not self._messages: + try: + # wait for next request + self._waiter = loop.create_future() + await self._waiter + except asyncio.CancelledError: + break + finally: + self._waiter = None + + message, payload = self._messages.popleft() + + start = loop.time() + + manager.requests_count += 1 + writer = StreamWriter(self, loop) + if isinstance(message, _ErrInfo): + # make request_factory work + request_handler = self._make_error_handler(message) + message = ERROR + else: + request_handler = self._request_handler + + request = self._request_factory(message, payload, self, writer, handler) + try: + # a new task is used for copy context vars (#3406) + task = self._loop.create_task( + self._handle_request(request, start, request_handler) + ) + try: + resp, reset = await task + except (asyncio.CancelledError, ConnectionError): + self.log_debug("Ignored premature client disconnection") + break + + # Drop the processed task from asyncio.Task.all_tasks() early + del task + # https://github.com/python/mypy/issues/14309 + if reset: # type: ignore[possibly-undefined] + self.log_debug("Ignored premature client disconnection 2") + break + + # notify server about keep-alive + self._keepalive = bool(resp.keep_alive) + + # check payload + if not payload.is_eof(): + lingering_time = self._lingering_time + # Could be force closed while awaiting above tasks. + if not self._force_close and lingering_time: # type: ignore[redundant-expr] + self.log_debug( + "Start lingering close timer for %s sec.", lingering_time + ) + + now = loop.time() + end_t = now + lingering_time + + with suppress(asyncio.TimeoutError, asyncio.CancelledError): + while not payload.is_eof() and now < end_t: + async with ceil_timeout(end_t - now): + # read and ignore + await payload.readany() + now = loop.time() + + # if payload still uncompleted + if not payload.is_eof() and not self._force_close: + self.log_debug("Uncompleted request.") + self.close() + + payload.set_exception(PayloadAccessError()) + + except asyncio.CancelledError: + self.log_debug("Ignored premature client disconnection ") + break + except RuntimeError as exc: + if self._loop.get_debug(): + self.log_exception("Unhandled runtime exception", exc_info=exc) + self.force_close() + except Exception as exc: + self.log_exception("Unhandled exception", exc_info=exc) + self.force_close() + finally: + if self.transport is None and resp is not None: + self.log_debug("Ignored premature client disconnection.") + elif not self._force_close: + if self._keepalive and not self._close: + # start keep-alive timer + if keepalive_timeout is not None: + now = self._loop.time() + self._keepalive_time = now + if self._keepalive_handle is None: + self._keepalive_handle = loop.call_at( + now + keepalive_timeout, self._process_keepalive + ) + else: + break + + # remove handler, close transport if no handlers left + if not self._force_close: + self._task_handler = None + if self.transport is not None: + self.transport.close() + + async def finish_response( + self, request: BaseRequest, resp: StreamResponse, start_time: float + ) -> bool: + """Prepare the response and write_eof, then log access. + + This has to + be called within the context of any exception so the access logger + can get exception information. Returns True if the client disconnects + prematurely. + """ + request._finish() + if self._request_parser is not None: + self._request_parser.set_upgraded(False) + self._upgrade = False + if self._message_tail: + self._request_parser.feed_data(self._message_tail) + self._message_tail = b"" + try: + prepare_meth = resp.prepare + except AttributeError: + if resp is None: + raise RuntimeError("Missing return " "statement on request handler") + else: + raise RuntimeError( + "Web-handler should return " + "a response instance, " + "got {!r}".format(resp) + ) + try: + await prepare_meth(request) + await resp.write_eof() + except ConnectionError: + await self.log_access(request, resp, start_time) + return True + else: + await self.log_access(request, resp, start_time) + return False + + def handle_error( + self, + request: BaseRequest, + status: int = 500, + exc: Optional[BaseException] = None, + message: Optional[str] = None, + ) -> StreamResponse: + """Handle errors. + + Returns HTTP response with specific status code. Logs additional + information. It always closes current connection. + """ + self.log_exception("Error handling request", exc_info=exc) + + # some data already got sent, connection is broken + if request.writer.output_size > 0: + raise ConnectionError( + "Response is sent already, cannot send another response " + "with the error message" + ) + + ct = "text/plain" + if status == HTTPStatus.INTERNAL_SERVER_ERROR: + title = "{0.value} {0.phrase}".format(HTTPStatus.INTERNAL_SERVER_ERROR) + msg = HTTPStatus.INTERNAL_SERVER_ERROR.description + tb = None + if self._loop.get_debug(): + with suppress(Exception): + tb = traceback.format_exc() + + if "text/html" in request.headers.get("Accept", ""): + if tb: + tb = html_escape(tb) + msg = f"

Traceback:

\n
{tb}
" + message = ( + "" + "{title}" + "\n

{title}

" + "\n{msg}\n\n" + ).format(title=title, msg=msg) + ct = "text/html" + else: + if tb: + msg = tb + message = title + "\n\n" + msg + + resp = Response(status=status, text=message, content_type=ct) + resp.force_close() + + return resp + + def _make_error_handler( + self, err_info: _ErrInfo + ) -> Callable[[BaseRequest], Awaitable[StreamResponse]]: + async def handler(request: BaseRequest) -> StreamResponse: + return self.handle_error( + request, err_info.status, err_info.exc, err_info.message + ) + + return handler diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py new file mode 100644 index 0000000..5181bd1 --- /dev/null +++ b/aiohttp/web_request.py @@ -0,0 +1,912 @@ +import asyncio +import dataclasses +import datetime +import io +import re +import socket +import string +import tempfile +import types +from http.cookies import SimpleCookie +from types import MappingProxyType +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Final, + Iterator, + Mapping, + MutableMapping, + Optional, + Pattern, + Set, + Tuple, + Union, + cast, +) +from urllib.parse import parse_qsl + +from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy +from yarl import URL + +from . import hdrs +from .abc import AbstractStreamWriter +from .helpers import ( + _SENTINEL, + ETAG_ANY, + LIST_QUOTED_ETAG_RE, + ChainMapProxy, + ETag, + HeadersMixin, + is_expected_content_type, + parse_http_date, + reify, + sentinel, + set_result, +) +from .http_parser import RawRequestMessage +from .http_writer import HttpVersion +from .multipart import BodyPartReader, MultipartReader +from .streams import EmptyStreamReader, StreamReader +from .typedefs import ( + DEFAULT_JSON_DECODER, + JSONDecoder, + LooseHeaders, + RawHeaders, + StrOrURL, +) +from .web_exceptions import ( + HTTPBadRequest, + HTTPRequestEntityTooLarge, + HTTPUnsupportedMediaType, +) +from .web_response import StreamResponse + +__all__ = ("BaseRequest", "FileField", "Request") + + +if TYPE_CHECKING: # pragma: no cover + from .web_app import Application + from .web_protocol import RequestHandler + from .web_urldispatcher import UrlMappingMatchInfo + + +@dataclasses.dataclass(frozen=True) +class FileField: + name: str + filename: str + file: io.BufferedReader + content_type: str + headers: "CIMultiDictProxy[str]" + + +_TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-" +# '-' at the end to prevent interpretation as range in a char class + +_TOKEN: Final[str] = rf"[{_TCHAR}]+" + +_QDTEXT: Final[str] = r"[{}]".format( + r"".join(chr(c) for c in (0x09, 0x20, 0x21) + tuple(range(0x23, 0x7F))) +) +# qdtext includes 0x5C to escape 0x5D ('\]') +# qdtext excludes obs-text (because obsoleted, and encoding not specified) + +_QUOTED_PAIR: Final[str] = r"\\[\t !-~]" + +_QUOTED_STRING: Final[str] = r'"(?:{quoted_pair}|{qdtext})*"'.format( + qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR +) + +_FORWARDED_PAIR: Final[ + str +] = r"({token})=({token}|{quoted_string})(:\d{{1,4}})?".format( + token=_TOKEN, quoted_string=_QUOTED_STRING +) + +_QUOTED_PAIR_REPLACE_RE: Final[Pattern[str]] = re.compile(r"\\([\t !-~])") +# same pattern as _QUOTED_PAIR but contains a capture group + +_FORWARDED_PAIR_RE: Final[Pattern[str]] = re.compile(_FORWARDED_PAIR) + +############################################################ +# HTTP Request +############################################################ + + +class BaseRequest(MutableMapping[str, Any], HeadersMixin): + POST_METHODS = { + hdrs.METH_PATCH, + hdrs.METH_POST, + hdrs.METH_PUT, + hdrs.METH_TRACE, + hdrs.METH_DELETE, + } + + __slots__ = ( + "_message", + "_protocol", + "_payload_writer", + "_payload", + "_headers", + "_method", + "_version", + "_rel_url", + "_post", + "_read_bytes", + "_state", + "_cache", + "_task", + "_client_max_size", + "_loop", + "_transport_sslcontext", + "_transport_peername", + "_disconnection_waiters", + "__weakref__", + ) + + def __init__( + self, + message: RawRequestMessage, + payload: StreamReader, + protocol: "RequestHandler", + payload_writer: AbstractStreamWriter, + task: "asyncio.Task[None]", + loop: asyncio.AbstractEventLoop, + *, + client_max_size: int = 1024**2, + state: Optional[Dict[str, Any]] = None, + scheme: Optional[str] = None, + host: Optional[str] = None, + remote: Optional[str] = None, + ) -> None: + super().__init__() + if state is None: + state = {} + self._message = message + self._protocol = protocol + self._payload_writer = payload_writer + + self._payload = payload + self._headers = message.headers + self._method = message.method + self._version = message.version + self._cache: Dict[str, Any] = {} + url = message.url + if url.is_absolute(): + # absolute URL is given, + # override auto-calculating url, host, and scheme + # all other properties should be good + self._cache["url"] = url + self._cache["host"] = url.host + self._cache["scheme"] = url.scheme + self._rel_url = url.relative() + else: + self._rel_url = message.url + self._post: Optional[MultiDictProxy[Union[str, bytes, FileField]]] = None + self._read_bytes: Optional[bytes] = None + + self._state = state + self._task = task + self._client_max_size = client_max_size + self._loop = loop + self._disconnection_waiters: Set[asyncio.Future[None]] = set() + + transport = self._protocol.transport + assert transport is not None + self._transport_sslcontext = transport.get_extra_info("sslcontext") + self._transport_peername = transport.get_extra_info("peername") + + if scheme is not None: + self._cache["scheme"] = scheme + if host is not None: + self._cache["host"] = host + if remote is not None: + self._cache["remote"] = remote + + def clone( + self, + *, + method: Union[str, _SENTINEL] = sentinel, + rel_url: Union[StrOrURL, _SENTINEL] = sentinel, + headers: Union[LooseHeaders, _SENTINEL] = sentinel, + scheme: Union[str, _SENTINEL] = sentinel, + host: Union[str, _SENTINEL] = sentinel, + remote: Union[str, _SENTINEL] = sentinel, + client_max_size: Union[int, _SENTINEL] = sentinel, + ) -> "BaseRequest": + """Clone itself with replacement some attributes. + + Creates and returns a new instance of Request object. If no parameters + are given, an exact copy is returned. If a parameter is not passed, it + will reuse the one from the current request object. + """ + if self._read_bytes: + raise RuntimeError("Cannot clone request " "after reading its content") + + dct: Dict[str, Any] = {} + if method is not sentinel: + dct["method"] = method + if rel_url is not sentinel: + new_url: URL = URL(rel_url) + dct["url"] = new_url + dct["path"] = str(new_url) + if headers is not sentinel: + # a copy semantic + new_headers = CIMultiDictProxy(CIMultiDict(headers)) + dct["headers"] = new_headers + dct["raw_headers"] = tuple( + (k.encode("utf-8"), v.encode("utf-8")) for k, v in new_headers.items() + ) + + message = self._message._replace(**dct) + + kwargs: Dict[str, str] = {} + if scheme is not sentinel: + kwargs["scheme"] = scheme + if host is not sentinel: + kwargs["host"] = host + if remote is not sentinel: + kwargs["remote"] = remote + if client_max_size is sentinel: + client_max_size = self._client_max_size + + return self.__class__( + message, + self._payload, + self._protocol, + self._payload_writer, + self._task, + self._loop, + client_max_size=client_max_size, + state=self._state.copy(), + **kwargs, + ) + + @property + def task(self) -> "asyncio.Task[None]": + return self._task + + @property + def protocol(self) -> "RequestHandler": + return self._protocol + + @property + def transport(self) -> Optional[asyncio.Transport]: + if self._protocol is None: + return None + return self._protocol.transport + + @property + def writer(self) -> AbstractStreamWriter: + return self._payload_writer + + @property + def client_max_size(self) -> int: + return self._client_max_size + + @reify + def rel_url(self) -> URL: + return self._rel_url + + # MutableMapping API + + def __getitem__(self, key: str) -> Any: + return self._state[key] + + def __setitem__(self, key: str, value: Any) -> None: + self._state[key] = value + + def __delitem__(self, key: str) -> None: + del self._state[key] + + def __len__(self) -> int: + return len(self._state) + + def __iter__(self) -> Iterator[str]: + return iter(self._state) + + ######## + + @reify + def secure(self) -> bool: + """A bool indicating if the request is handled with SSL.""" + return self.scheme == "https" + + @reify + def forwarded(self) -> Tuple[Mapping[str, str], ...]: + """A tuple containing all parsed Forwarded header(s). + + Makes an effort to parse Forwarded headers as specified by RFC 7239: + + - It adds one (immutable) dictionary per Forwarded 'field-value', ie + per proxy. The element corresponds to the data in the Forwarded + field-value added by the first proxy encountered by the client. Each + subsequent item corresponds to those added by later proxies. + - It checks that every value has valid syntax in general as specified + in section 4: either a 'token' or a 'quoted-string'. + - It un-escapes found escape sequences. + - It does NOT validate 'by' and 'for' contents as specified in section + 6. + - It does NOT validate 'host' contents (Host ABNF). + - It does NOT validate 'proto' contents for valid URI scheme names. + + Returns a tuple containing one or more immutable dicts + """ + elems = [] + for field_value in self._message.headers.getall(hdrs.FORWARDED, ()): + length = len(field_value) + pos = 0 + need_separator = False + elem: Dict[str, str] = {} + elems.append(types.MappingProxyType(elem)) + while 0 <= pos < length: + match = _FORWARDED_PAIR_RE.match(field_value, pos) + if match is not None: # got a valid forwarded-pair + if need_separator: + # bad syntax here, skip to next comma + pos = field_value.find(",", pos) + else: + name, value, port = match.groups() + if value[0] == '"': + # quoted string: remove quotes and unescape + value = _QUOTED_PAIR_REPLACE_RE.sub(r"\1", value[1:-1]) + if port: + value += port + elem[name.lower()] = value + pos += len(match.group(0)) + need_separator = True + elif field_value[pos] == ",": # next forwarded-element + need_separator = False + elem = {} + elems.append(types.MappingProxyType(elem)) + pos += 1 + elif field_value[pos] == ";": # next forwarded-pair + need_separator = False + pos += 1 + elif field_value[pos] in " \t": + # Allow whitespace even between forwarded-pairs, though + # RFC 7239 doesn't. This simplifies code and is in line + # with Postel's law. + pos += 1 + else: + # bad syntax here, skip to next comma + pos = field_value.find(",", pos) + return tuple(elems) + + @reify + def scheme(self) -> str: + """A string representing the scheme of the request. + + Hostname is resolved in this order: + + - overridden value by .clone(scheme=new_scheme) call. + - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise. + + 'http' or 'https'. + """ + if self._transport_sslcontext: + return "https" + else: + return "http" + + @reify + def method(self) -> str: + """Read only property for getting HTTP method. + + The value is upper-cased str like 'GET', 'POST', 'PUT' etc. + """ + return self._method + + @reify + def version(self) -> HttpVersion: + """Read only property for getting HTTP version of request. + + Returns aiohttp.protocol.HttpVersion instance. + """ + return self._version + + @reify + def host(self) -> str: + """Hostname of the request. + + Hostname is resolved in this order: + + - overridden value by .clone(host=new_host) call. + - HOST HTTP header + - socket.getfqdn() value + """ + host = self._message.headers.get(hdrs.HOST) + if host is not None: + return host + return socket.getfqdn() + + @reify + def remote(self) -> Optional[str]: + """Remote IP of client initiated HTTP request. + + The IP is resolved in this order: + + - overridden value by .clone(remote=new_remote) call. + - peername of opened socket + """ + if self._transport_peername is None: + return None + if isinstance(self._transport_peername, (list, tuple)): + return str(self._transport_peername[0]) + return str(self._transport_peername) + + @reify + def url(self) -> URL: + url = URL.build(scheme=self.scheme, host=self.host) + return url.join(self._rel_url) + + @reify + def path(self) -> str: + """The URL including *PATH INFO* without the host or scheme. + + E.g., ``/app/blog`` + """ + return self._rel_url.path + + @reify + def path_qs(self) -> str: + """The URL including PATH_INFO and the query string. + + E.g, /app/blog?id=10 + """ + return str(self._rel_url) + + @reify + def raw_path(self) -> str: + """The URL including raw *PATH INFO* without the host or scheme. + + Warning, the path is unquoted and may contains non valid URL characters + + E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters`` + """ + return self._message.path + + @reify + def query(self) -> MultiDictProxy[str]: + """A multidict with all the variables in the query string.""" + return MultiDictProxy(self._rel_url.query) + + @reify + def query_string(self) -> str: + """The query string in the URL. + + E.g., id=10 + """ + return self._rel_url.query_string + + @reify + def headers(self) -> "CIMultiDictProxy[str]": + """A case-insensitive multidict proxy with all headers.""" + return self._headers + + @reify + def raw_headers(self) -> RawHeaders: + """A sequence of pairs for all headers.""" + return self._message.raw_headers + + @reify + def if_modified_since(self) -> Optional[datetime.datetime]: + """The value of If-Modified-Since HTTP header, or None. + + This header is represented as a `datetime` object. + """ + return parse_http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE)) + + @reify + def if_unmodified_since(self) -> Optional[datetime.datetime]: + """The value of If-Unmodified-Since HTTP header, or None. + + This header is represented as a `datetime` object. + """ + return parse_http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE)) + + @staticmethod + def _etag_values(etag_header: str) -> Iterator[ETag]: + """Extract `ETag` objects from raw header.""" + if etag_header == ETAG_ANY: + yield ETag( + is_weak=False, + value=ETAG_ANY, + ) + else: + for match in LIST_QUOTED_ETAG_RE.finditer(etag_header): + is_weak, value, garbage = match.group(2, 3, 4) + # Any symbol captured by 4th group means + # that the following sequence is invalid. + if garbage: + break + + yield ETag( + is_weak=bool(is_weak), + value=value, + ) + + @classmethod + def _if_match_or_none_impl( + cls, header_value: Optional[str] + ) -> Optional[Tuple[ETag, ...]]: + if not header_value: + return None + + return tuple(cls._etag_values(header_value)) + + @reify + def if_match(self) -> Optional[Tuple[ETag, ...]]: + """The value of If-Match HTTP header, or None. + + This header is represented as a `tuple` of `ETag` objects. + """ + return self._if_match_or_none_impl(self.headers.get(hdrs.IF_MATCH)) + + @reify + def if_none_match(self) -> Optional[Tuple[ETag, ...]]: + """The value of If-None-Match HTTP header, or None. + + This header is represented as a `tuple` of `ETag` objects. + """ + return self._if_match_or_none_impl(self.headers.get(hdrs.IF_NONE_MATCH)) + + @reify + def if_range(self) -> Optional[datetime.datetime]: + """The value of If-Range HTTP header, or None. + + This header is represented as a `datetime` object. + """ + return parse_http_date(self.headers.get(hdrs.IF_RANGE)) + + @reify + def keep_alive(self) -> bool: + """Is keepalive enabled by client?""" + return not self._message.should_close + + @reify + def cookies(self) -> Mapping[str, str]: + """Return request cookies. + + A read-only dictionary-like object. + """ + raw = self.headers.get(hdrs.COOKIE, "") + parsed: SimpleCookie[str] = SimpleCookie(raw) + return MappingProxyType({key: val.value for key, val in parsed.items()}) + + @reify + def http_range(self) -> slice: + """The content of Range HTTP header. + + Return a slice instance. + + """ + rng = self._headers.get(hdrs.RANGE) + start, end = None, None + if rng is not None: + try: + pattern = r"^bytes=(\d*)-(\d*)$" + start, end = re.findall(pattern, rng)[0] + except IndexError: # pattern was not found in header + raise ValueError("range not in acceptable format") + + end = int(end) if end else None + start = int(start) if start else None + + if start is None and end is not None: + # end with no start is to return tail of content + start = -end + end = None + + if start is not None and end is not None: + # end is inclusive in range header, exclusive for slice + end += 1 + + if start >= end: + raise ValueError("start cannot be after end") + + if start is end is None: # No valid range supplied + raise ValueError("No start or end of range specified") + + return slice(start, end, 1) + + @reify + def content(self) -> StreamReader: + """Return raw payload stream.""" + return self._payload + + @property + def can_read_body(self) -> bool: + """Return True if request's HTTP BODY can be read, False otherwise.""" + return not self._payload.at_eof() + + @reify + def body_exists(self) -> bool: + """Return True if request has HTTP BODY, False otherwise.""" + return type(self._payload) is not EmptyStreamReader + + async def release(self) -> None: + """Release request. + + Eat unread part of HTTP BODY if present. + """ + while not self._payload.at_eof(): + await self._payload.readany() + + async def read(self) -> bytes: + """Read request body if present. + + Returns bytes object with full request content. + """ + if self._read_bytes is None: + body = bytearray() + while True: + chunk = await self._payload.readany() + body.extend(chunk) + if self._client_max_size: + body_size = len(body) + if body_size > self._client_max_size: + raise HTTPRequestEntityTooLarge( + max_size=self._client_max_size, actual_size=body_size + ) + if not chunk: + break + self._read_bytes = bytes(body) + return self._read_bytes + + async def text(self) -> str: + """Return BODY as text using encoding from .charset.""" + bytes_body = await self.read() + encoding = self.charset or "utf-8" + try: + return bytes_body.decode(encoding) + except LookupError: + raise HTTPUnsupportedMediaType() + + async def json( + self, + *, + loads: JSONDecoder = DEFAULT_JSON_DECODER, + content_type: Optional[str] = "application/json", + ) -> Any: + """Return BODY as JSON.""" + body = await self.text() + if content_type: + if not is_expected_content_type(self.content_type, content_type): + raise HTTPBadRequest( + text=( + "Attempt to decode JSON with " + "unexpected mimetype: %s" % self.content_type + ) + ) + + return loads(body) + + async def multipart(self) -> MultipartReader: + """Return async iterator to process BODY as multipart.""" + return MultipartReader(self._headers, self._payload) + + async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]": + """Return POST parameters.""" + if self._post is not None: + return self._post + if self._method not in self.POST_METHODS: + self._post = MultiDictProxy(MultiDict()) + return self._post + + content_type = self.content_type + if content_type not in ( + "", + "application/x-www-form-urlencoded", + "multipart/form-data", + ): + self._post = MultiDictProxy(MultiDict()) + return self._post + + out: MultiDict[Union[str, bytes, FileField]] = MultiDict() + + if content_type == "multipart/form-data": + multipart = await self.multipart() + max_size = self._client_max_size + + field = await multipart.next() + while field is not None: + size = 0 + field_ct = field.headers.get(hdrs.CONTENT_TYPE) + + if isinstance(field, BodyPartReader): + assert field.name is not None + + # Note that according to RFC 7578, the Content-Type header + # is optional, even for files, so we can't assume it's + # present. + # https://tools.ietf.org/html/rfc7578#section-4.4 + if field.filename: + # store file in temp file + tmp = tempfile.TemporaryFile() + chunk = await field.read_chunk(size=2**16) + while chunk: + chunk = field.decode(chunk) + tmp.write(chunk) + size += len(chunk) + if 0 < max_size < size: + tmp.close() + raise HTTPRequestEntityTooLarge( + max_size=max_size, actual_size=size + ) + chunk = await field.read_chunk(size=2**16) + tmp.seek(0) + + if field_ct is None: + field_ct = "application/octet-stream" + + ff = FileField( + field.name, + field.filename, + cast(io.BufferedReader, tmp), + field_ct, + field.headers, + ) + out.add(field.name, ff) + else: + # deal with ordinary data + value = await field.read(decode=True) + if field_ct is None or field_ct.startswith("text/"): + charset = field.get_charset(default="utf-8") + out.add(field.name, value.decode(charset)) + else: + out.add(field.name, value) + size += len(value) + if 0 < max_size < size: + raise HTTPRequestEntityTooLarge( + max_size=max_size, actual_size=size + ) + else: + raise ValueError( + "To decode nested multipart you need " "to use custom reader", + ) + + field = await multipart.next() + else: + data = await self.read() + if data: + charset = self.charset or "utf-8" + bytes_query = data.rstrip() + try: + query = bytes_query.decode(charset) + except LookupError: + raise HTTPUnsupportedMediaType() + out.extend( + parse_qsl(qs=query, keep_blank_values=True, encoding=charset) + ) + + self._post = MultiDictProxy(out) + return self._post + + def get_extra_info(self, name: str, default: Any = None) -> Any: + """Extra info from protocol transport""" + protocol = self._protocol + if protocol is None: + return default + + transport = protocol.transport + if transport is None: + return default + + return transport.get_extra_info(name, default) + + def __repr__(self) -> str: + ascii_encodable_path = self.path.encode("ascii", "backslashreplace").decode( + "ascii" + ) + return "<{} {} {} >".format( + self.__class__.__name__, self._method, ascii_encodable_path + ) + + def __eq__(self, other: object) -> bool: + return id(self) == id(other) + + def __bool__(self) -> bool: + return True + + async def _prepare_hook(self, response: StreamResponse) -> None: + return + + def _cancel(self, exc: BaseException) -> None: + self._payload.set_exception(exc) + for fut in self._disconnection_waiters: + set_result(fut, None) + + def _finish(self) -> None: + for fut in self._disconnection_waiters: + fut.cancel() + + if self._post is None or self.content_type != "multipart/form-data": + return + + # NOTE: Release file descriptors for the + # NOTE: `tempfile.Temporaryfile`-created `_io.BufferedRandom` + # NOTE: instances of files sent within multipart request body + # NOTE: via HTTP POST request. + for file_name, file_field_object in self._post.items(): + if not isinstance(file_field_object, FileField): + continue + + file_field_object.file.close() + + async def wait_for_disconnection(self) -> None: + loop = asyncio.get_event_loop() + fut: asyncio.Future[None] = loop.create_future() + self._disconnection_waiters.add(fut) + try: + await fut + finally: + self._disconnection_waiters.remove(fut) + + +class Request(BaseRequest): + __slots__ = ("_match_info",) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + # matchdict, route_name, handler + # or information about traversal lookup + + # initialized after route resolving + self._match_info: Optional[UrlMappingMatchInfo] = None + + def clone( + self, + *, + method: Union[str, _SENTINEL] = sentinel, + rel_url: Union[StrOrURL, _SENTINEL] = sentinel, + headers: Union[LooseHeaders, _SENTINEL] = sentinel, + scheme: Union[str, _SENTINEL] = sentinel, + host: Union[str, _SENTINEL] = sentinel, + remote: Union[str, _SENTINEL] = sentinel, + client_max_size: Union[int, _SENTINEL] = sentinel, + ) -> "Request": + ret = super().clone( + method=method, + rel_url=rel_url, + headers=headers, + scheme=scheme, + host=host, + remote=remote, + client_max_size=client_max_size, + ) + new_ret = cast(Request, ret) + new_ret._match_info = self._match_info + return new_ret + + @reify + def match_info(self) -> "UrlMappingMatchInfo": + """Result of route resolving.""" + match_info = self._match_info + assert match_info is not None + return match_info + + @property + def app(self) -> "Application": + """Application instance.""" + match_info = self._match_info + assert match_info is not None + return match_info.current_app + + @property + def config_dict(self) -> ChainMapProxy: + match_info = self._match_info + assert match_info is not None + lst = match_info.apps + app = self.app + idx = lst.index(app) + sublist = list(reversed(lst[: idx + 1])) + return ChainMapProxy(sublist) + + async def _prepare_hook(self, response: StreamResponse) -> None: + match_info = self._match_info + if match_info is None: + return + for app in match_info._apps: + await app.on_response_prepare.send(self, response) diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py new file mode 100644 index 0000000..1f2d623 --- /dev/null +++ b/aiohttp/web_response.py @@ -0,0 +1,728 @@ +import asyncio +import collections.abc +import datetime +import enum +import json +import math +import time +import warnings +from concurrent.futures import Executor +from http import HTTPStatus +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Iterator, + MutableMapping, + Optional, + Union, + cast, +) + +from multidict import CIMultiDict, istr + +from . import hdrs, payload +from .abc import AbstractStreamWriter +from .compression_utils import ZLibCompressor +from .helpers import ( + ETAG_ANY, + QUOTED_ETAG_RE, + CookieMixin, + ETag, + HeadersMixin, + parse_http_date, + populate_with_cookies, + rfc822_formatted_time, + sentinel, + validate_etag_value, +) +from .http import SERVER_SOFTWARE, HttpVersion10, HttpVersion11 +from .payload import Payload +from .typedefs import JSONEncoder, LooseHeaders + +__all__ = ("ContentCoding", "StreamResponse", "Response", "json_response") + + +if TYPE_CHECKING: # pragma: no cover + from .web_request import BaseRequest + + BaseClass = MutableMapping[str, Any] +else: + BaseClass = collections.abc.MutableMapping + + +class ContentCoding(enum.Enum): + # The content codings that we have support for. + # + # Additional registered codings are listed at: + # https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding + deflate = "deflate" + gzip = "gzip" + identity = "identity" + + +############################################################ +# HTTP Response classes +############################################################ + + +class StreamResponse(BaseClass, HeadersMixin, CookieMixin): + __slots__ = ( + "_length_check", + "_body", + "_keep_alive", + "_chunked", + "_compression", + "_compression_force", + "_req", + "_payload_writer", + "_eof_sent", + "_body_length", + "_state", + "_headers", + "_status", + "_reason", + "_cookies", + "__weakref__", + ) + + def __init__( + self, + *, + status: int = 200, + reason: Optional[str] = None, + headers: Optional[LooseHeaders] = None, + ) -> None: + super().__init__() + self._length_check = True + self._body = None + self._keep_alive: Optional[bool] = None + self._chunked = False + self._compression = False + self._compression_force: Optional[ContentCoding] = None + + self._req: Optional[BaseRequest] = None + self._payload_writer: Optional[AbstractStreamWriter] = None + self._eof_sent = False + self._body_length = 0 + self._state: Dict[str, Any] = {} + + if headers is not None: + self._headers: CIMultiDict[str] = CIMultiDict(headers) + else: + self._headers = CIMultiDict() + + self.set_status(status, reason) + + @property + def prepared(self) -> bool: + return self._payload_writer is not None + + @property + def task(self) -> "Optional[asyncio.Task[None]]": + if self._req: + return self._req.task + else: + return None + + @property + def status(self) -> int: + return self._status + + @property + def chunked(self) -> bool: + return self._chunked + + @property + def compression(self) -> bool: + return self._compression + + @property + def reason(self) -> str: + return self._reason + + def set_status( + self, + status: int, + reason: Optional[str] = None, + ) -> None: + assert not self.prepared, ( + "Cannot change the response status code after " "the headers have been sent" + ) + self._status = int(status) + if reason is None: + try: + reason = HTTPStatus(self._status).phrase + except ValueError: + reason = "" + self._reason = reason + + @property + def keep_alive(self) -> Optional[bool]: + return self._keep_alive + + def force_close(self) -> None: + self._keep_alive = False + + @property + def body_length(self) -> int: + return self._body_length + + def enable_chunked_encoding(self) -> None: + """Enables automatic chunked transfer encoding.""" + self._chunked = True + + if hdrs.CONTENT_LENGTH in self._headers: + raise RuntimeError( + "You can't enable chunked encoding when " "a content length is set" + ) + + def enable_compression(self, force: Optional[ContentCoding] = None) -> None: + """Enables response compression encoding.""" + # Backwards compatibility for when force was a bool <0.17. + self._compression = True + self._compression_force = force + + @property + def headers(self) -> "CIMultiDict[str]": + return self._headers + + @property + def content_length(self) -> Optional[int]: + # Just a placeholder for adding setter + return super().content_length + + @content_length.setter + def content_length(self, value: Optional[int]) -> None: + if value is not None: + value = int(value) + if self._chunked: + raise RuntimeError( + "You can't set content length when " "chunked encoding is enable" + ) + self._headers[hdrs.CONTENT_LENGTH] = str(value) + else: + self._headers.pop(hdrs.CONTENT_LENGTH, None) + + @property + def content_type(self) -> str: + # Just a placeholder for adding setter + return super().content_type + + @content_type.setter + def content_type(self, value: str) -> None: + self.content_type # read header values if needed + self._content_type = str(value) + self._generate_content_type_header() + + @property + def charset(self) -> Optional[str]: + # Just a placeholder for adding setter + return super().charset + + @charset.setter + def charset(self, value: Optional[str]) -> None: + ctype = self.content_type # read header values if needed + if ctype == "application/octet-stream": + raise RuntimeError( + "Setting charset for application/octet-stream " + "doesn't make sense, setup content_type first" + ) + assert self._content_dict is not None + if value is None: + self._content_dict.pop("charset", None) + else: + self._content_dict["charset"] = str(value).lower() + self._generate_content_type_header() + + @property + def last_modified(self) -> Optional[datetime.datetime]: + """The value of Last-Modified HTTP header, or None. + + This header is represented as a `datetime` object. + """ + return parse_http_date(self._headers.get(hdrs.LAST_MODIFIED)) + + @last_modified.setter + def last_modified( + self, value: Optional[Union[int, float, datetime.datetime, str]] + ) -> None: + if value is None: + self._headers.pop(hdrs.LAST_MODIFIED, None) + elif isinstance(value, (int, float)): + self._headers[hdrs.LAST_MODIFIED] = time.strftime( + "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value)) + ) + elif isinstance(value, datetime.datetime): + self._headers[hdrs.LAST_MODIFIED] = time.strftime( + "%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple() + ) + elif isinstance(value, str): + self._headers[hdrs.LAST_MODIFIED] = value + + @property + def etag(self) -> Optional[ETag]: + quoted_value = self._headers.get(hdrs.ETAG) + if not quoted_value: + return None + elif quoted_value == ETAG_ANY: + return ETag(value=ETAG_ANY) + match = QUOTED_ETAG_RE.fullmatch(quoted_value) + if not match: + return None + is_weak, value = match.group(1, 2) + return ETag( + is_weak=bool(is_weak), + value=value, + ) + + @etag.setter + def etag(self, value: Optional[Union[ETag, str]]) -> None: + if value is None: + self._headers.pop(hdrs.ETAG, None) + elif (isinstance(value, str) and value == ETAG_ANY) or ( + isinstance(value, ETag) and value.value == ETAG_ANY + ): + self._headers[hdrs.ETAG] = ETAG_ANY + elif isinstance(value, str): + validate_etag_value(value) + self._headers[hdrs.ETAG] = f'"{value}"' + elif isinstance(value, ETag) and isinstance(value.value, str): # type: ignore[redundant-expr] + validate_etag_value(value.value) + hdr_value = f'W/"{value.value}"' if value.is_weak else f'"{value.value}"' + self._headers[hdrs.ETAG] = hdr_value + else: + raise ValueError( + f"Unsupported etag type: {type(value)}. " + f"etag must be str, ETag or None" + ) + + def _generate_content_type_header( + self, CONTENT_TYPE: istr = hdrs.CONTENT_TYPE + ) -> None: + assert self._content_dict is not None + assert self._content_type is not None + params = "; ".join(f"{k}={v}" for k, v in self._content_dict.items()) + if params: + ctype = self._content_type + "; " + params + else: + ctype = self._content_type + self._headers[CONTENT_TYPE] = ctype + + async def _do_start_compression(self, coding: ContentCoding) -> None: + if coding != ContentCoding.identity: + assert self._payload_writer is not None + self._headers[hdrs.CONTENT_ENCODING] = coding.value + self._payload_writer.enable_compression(coding.value) + # Compressed payload may have different content length, + # remove the header + self._headers.popall(hdrs.CONTENT_LENGTH, None) + + async def _start_compression(self, request: "BaseRequest") -> None: + if self._compression_force: + await self._do_start_compression(self._compression_force) + else: + accept_encoding = request.headers.get(hdrs.ACCEPT_ENCODING, "").lower() + for coding in ContentCoding: + if coding.value in accept_encoding: + await self._do_start_compression(coding) + return + + async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter]: + if self._eof_sent: + return None + if self._payload_writer is not None: + return self._payload_writer + + return await self._start(request) + + async def _start(self, request: "BaseRequest") -> AbstractStreamWriter: + self._req = request + writer = self._payload_writer = request._payload_writer + + await self._prepare_headers() + await request._prepare_hook(self) + await self._write_headers() + + return writer + + async def _prepare_headers(self) -> None: + request = self._req + assert request is not None + writer = self._payload_writer + assert writer is not None + keep_alive = self._keep_alive + if keep_alive is None: + keep_alive = request.keep_alive + self._keep_alive = keep_alive + + version = request.version + + headers = self._headers + populate_with_cookies(headers, self.cookies) + + if self._compression: + await self._start_compression(request) + + if self._chunked: + if version != HttpVersion11: + raise RuntimeError( + "Using chunked encoding is forbidden " + "for HTTP/{0.major}.{0.minor}".format(request.version) + ) + writer.enable_chunking() + headers[hdrs.TRANSFER_ENCODING] = "chunked" + if hdrs.CONTENT_LENGTH in headers: + del headers[hdrs.CONTENT_LENGTH] + elif self._length_check: + writer.length = self.content_length + if writer.length is None: + if version >= HttpVersion11 and self.status != 204: + writer.enable_chunking() + headers[hdrs.TRANSFER_ENCODING] = "chunked" + if hdrs.CONTENT_LENGTH in headers: + del headers[hdrs.CONTENT_LENGTH] + else: + keep_alive = False + # HTTP 1.1: https://tools.ietf.org/html/rfc7230#section-3.3.2 + # HTTP 1.0: https://tools.ietf.org/html/rfc1945#section-10.4 + elif version >= HttpVersion11 and self.status in (100, 101, 102, 103, 204): + del headers[hdrs.CONTENT_LENGTH] + + if self.status not in (204, 304): + headers.setdefault(hdrs.CONTENT_TYPE, "application/octet-stream") + headers.setdefault(hdrs.DATE, rfc822_formatted_time()) + headers.setdefault(hdrs.SERVER, SERVER_SOFTWARE) + + # connection header + if hdrs.CONNECTION not in headers: + if keep_alive: + if version == HttpVersion10: + headers[hdrs.CONNECTION] = "keep-alive" + else: + if version == HttpVersion11: + headers[hdrs.CONNECTION] = "close" + + async def _write_headers(self) -> None: + request = self._req + assert request is not None + writer = self._payload_writer + assert writer is not None + # status line + version = request.version + status_line = "HTTP/{}.{} {} {}".format( + version[0], version[1], self._status, self._reason + ) + await writer.write_headers(status_line, self._headers) + + async def write(self, data: bytes) -> None: + assert isinstance( + data, (bytes, bytearray, memoryview) + ), "data argument must be byte-ish (%r)" % type(data) + + if self._eof_sent: + raise RuntimeError("Cannot call write() after write_eof()") + if self._payload_writer is None: + raise RuntimeError("Cannot call write() before prepare()") + + await self._payload_writer.write(data) + + async def drain(self) -> None: + assert not self._eof_sent, "EOF has already been sent" + assert self._payload_writer is not None, "Response has not been started" + warnings.warn( + "drain method is deprecated, use await resp.write()", + DeprecationWarning, + stacklevel=2, + ) + await self._payload_writer.drain() + + async def write_eof(self, data: bytes = b"") -> None: + assert isinstance( + data, (bytes, bytearray, memoryview) + ), "data argument must be byte-ish (%r)" % type(data) + + if self._eof_sent: + return + + assert self._payload_writer is not None, "Response has not been started" + + await self._payload_writer.write_eof(data) + self._eof_sent = True + self._req = None + self._body_length = self._payload_writer.output_size + self._payload_writer = None + + def __repr__(self) -> str: + if self._eof_sent: + info = "eof" + elif self.prepared: + assert self._req is not None + info = f"{self._req.method} {self._req.path} " + else: + info = "not prepared" + return f"<{self.__class__.__name__} {self.reason} {info}>" + + def __getitem__(self, key: str) -> Any: + return self._state[key] + + def __setitem__(self, key: str, value: Any) -> None: + self._state[key] = value + + def __delitem__(self, key: str) -> None: + del self._state[key] + + def __len__(self) -> int: + return len(self._state) + + def __iter__(self) -> Iterator[str]: + return iter(self._state) + + def __hash__(self) -> int: + return hash(id(self)) + + def __eq__(self, other: object) -> bool: + return self is other + + +class Response(StreamResponse): + __slots__ = ( + "_body_payload", + "_compressed_body", + "_zlib_executor_size", + "_zlib_executor", + ) + + def __init__( + self, + *, + body: Any = None, + status: int = 200, + reason: Optional[str] = None, + text: Optional[str] = None, + headers: Optional[LooseHeaders] = None, + content_type: Optional[str] = None, + charset: Optional[str] = None, + zlib_executor_size: Optional[int] = None, + zlib_executor: Optional[Executor] = None, + ) -> None: + if body is not None and text is not None: + raise ValueError("body and text are not allowed together") + + if headers is None: + real_headers: CIMultiDict[str] = CIMultiDict() + elif not isinstance(headers, CIMultiDict): + real_headers = CIMultiDict(headers) + else: + real_headers = headers # = cast('CIMultiDict[str]', headers) + + if content_type is not None and "charset" in content_type: + raise ValueError("charset must not be in content_type " "argument") + + if text is not None: + if hdrs.CONTENT_TYPE in real_headers: + if content_type or charset: + raise ValueError( + "passing both Content-Type header and " + "content_type or charset params " + "is forbidden" + ) + else: + # fast path for filling headers + if not isinstance(text, str): + raise TypeError("text argument must be str (%r)" % type(text)) + if content_type is None: + content_type = "text/plain" + if charset is None: + charset = "utf-8" + real_headers[hdrs.CONTENT_TYPE] = content_type + "; charset=" + charset + body = text.encode(charset) + text = None + else: + if hdrs.CONTENT_TYPE in real_headers: + if content_type is not None or charset is not None: + raise ValueError( + "passing both Content-Type header and " + "content_type or charset params " + "is forbidden" + ) + else: + if content_type is not None: + if charset is not None: + content_type += "; charset=" + charset + real_headers[hdrs.CONTENT_TYPE] = content_type + + super().__init__(status=status, reason=reason, headers=real_headers) + + if text is not None: + self.text = text + else: + self.body = body + + self._compressed_body: Optional[bytes] = None + self._zlib_executor_size = zlib_executor_size + self._zlib_executor = zlib_executor + + @property + def body(self) -> Optional[Union[bytes, Payload]]: + return self._body + + @body.setter + def body(self, body: bytes) -> None: + if body is None: + self._body: Optional[bytes] = None + self._body_payload: bool = False + elif isinstance(body, (bytes, bytearray)): + self._body = body + self._body_payload = False + else: + try: + self._body = body = payload.PAYLOAD_REGISTRY.get(body) + except payload.LookupError: + raise ValueError("Unsupported body type %r" % type(body)) + + self._body_payload = True + + headers = self._headers + + # set content-type + if hdrs.CONTENT_TYPE not in headers: + headers[hdrs.CONTENT_TYPE] = body.content_type + + # copy payload headers + if body.headers: + for key, value in body.headers.items(): + if key not in headers: + headers[key] = value + + self._compressed_body = None + + @property + def text(self) -> Optional[str]: + if self._body is None: + return None + return self._body.decode(self.charset or "utf-8") + + @text.setter + def text(self, text: str) -> None: + assert isinstance(text, str), "text argument must be str (%r)" % type(text) + + if self.content_type == "application/octet-stream": + self.content_type = "text/plain" + if self.charset is None: + self.charset = "utf-8" + + self._body = text.encode(self.charset) + self._body_payload = False + self._compressed_body = None + + @property + def content_length(self) -> Optional[int]: + if self._chunked: + return None + + if hdrs.CONTENT_LENGTH in self._headers: + return super().content_length + + if self._compressed_body is not None: + # Return length of the compressed body + return len(self._compressed_body) + elif self._body_payload: + # A payload without content length, or a compressed payload + return None + elif self._body is not None: + return len(self._body) + else: + return 0 + + @content_length.setter + def content_length(self, value: Optional[int]) -> None: + raise RuntimeError("Content length is set automatically") + + async def write_eof(self, data: bytes = b"") -> None: + if self._eof_sent: + return + if self._compressed_body is None: + body: Optional[Union[bytes, Payload]] = self._body + else: + body = self._compressed_body + assert not data, f"data arg is not supported, got {data!r}" + assert self._req is not None + assert self._payload_writer is not None + if body is not None: + if self._req._method == hdrs.METH_HEAD or self._status in [204, 304]: + await super().write_eof() + elif self._body_payload: + payload = cast(Payload, body) + await payload.write(self._payload_writer) + await super().write_eof() + else: + await super().write_eof(cast(bytes, body)) + else: + await super().write_eof() + + async def _start(self, request: "BaseRequest") -> AbstractStreamWriter: + if not self._chunked and hdrs.CONTENT_LENGTH not in self._headers: + if self._body_payload: + size = cast(Payload, self._body).size + if size is not None: + self._headers[hdrs.CONTENT_LENGTH] = str(size) + else: + body_len = len(self._body) if self._body else "0" + self._headers[hdrs.CONTENT_LENGTH] = str(body_len) + + return await super()._start(request) + + async def _do_start_compression(self, coding: ContentCoding) -> None: + if self._body_payload or self._chunked: + return await super()._do_start_compression(coding) + + if coding != ContentCoding.identity: + # Instead of using _payload_writer.enable_compression, + # compress the whole body + compressor = ZLibCompressor( + encoding=str(coding.value), + max_sync_chunk_size=self._zlib_executor_size, + executor=self._zlib_executor, + ) + assert self._body is not None + if self._zlib_executor_size is None and len(self._body) > 1024 * 1024: + warnings.warn( + "Synchronous compression of large response bodies " + f"({len(self._body)} bytes) might block the async event loop. " + "Consider providing a custom value to zlib_executor_size/" + "zlib_executor response properties or disabling compression on it." + ) + self._compressed_body = ( + await compressor.compress(self._body) + compressor.flush() + ) + assert self._compressed_body is not None + + self._headers[hdrs.CONTENT_ENCODING] = coding.value + self._headers[hdrs.CONTENT_LENGTH] = str(len(self._compressed_body)) + + +def json_response( + data: Any = sentinel, + *, + text: Optional[str] = None, + body: Optional[bytes] = None, + status: int = 200, + reason: Optional[str] = None, + headers: Optional[LooseHeaders] = None, + content_type: str = "application/json", + dumps: JSONEncoder = json.dumps, +) -> Response: + if data is not sentinel: + if text or body: + raise ValueError("only one of data, text, or body should be specified") + else: + text = dumps(data) + return Response( + text=text, + body=body, + status=status, + reason=reason, + headers=headers, + content_type=content_type, + ) diff --git a/aiohttp/web_routedef.py b/aiohttp/web_routedef.py new file mode 100644 index 0000000..e1ea3c1 --- /dev/null +++ b/aiohttp/web_routedef.py @@ -0,0 +1,215 @@ +import abc +import dataclasses +import os # noqa +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterator, + List, + Optional, + Sequence, + Type, + Union, + overload, +) + +from . import hdrs +from .abc import AbstractView +from .typedefs import Handler, PathLike + +if TYPE_CHECKING: # pragma: no cover + from .web_request import Request + from .web_response import StreamResponse + from .web_urldispatcher import AbstractRoute, UrlDispatcher +else: + Request = StreamResponse = UrlDispatcher = AbstractRoute = None + + +__all__ = ( + "AbstractRouteDef", + "RouteDef", + "StaticDef", + "RouteTableDef", + "head", + "options", + "get", + "post", + "patch", + "put", + "delete", + "route", + "view", + "static", +) + + +class AbstractRouteDef(abc.ABC): + @abc.abstractmethod + def register(self, router: UrlDispatcher) -> List[AbstractRoute]: + pass # pragma: no cover + + +_HandlerType = Union[Type[AbstractView], Handler] + + +@dataclasses.dataclass(frozen=True, repr=False) +class RouteDef(AbstractRouteDef): + method: str + path: str + handler: _HandlerType + kwargs: Dict[str, Any] + + def __repr__(self) -> str: + info = [] + for name, value in sorted(self.kwargs.items()): + info.append(f", {name}={value!r}") + return " {handler.__name__!r}" "{info}>".format( + method=self.method, path=self.path, handler=self.handler, info="".join(info) + ) + + def register(self, router: UrlDispatcher) -> List[AbstractRoute]: + if self.method in hdrs.METH_ALL: + reg = getattr(router, "add_" + self.method.lower()) + return [reg(self.path, self.handler, **self.kwargs)] + else: + return [ + router.add_route(self.method, self.path, self.handler, **self.kwargs) + ] + + +@dataclasses.dataclass(frozen=True, repr=False) +class StaticDef(AbstractRouteDef): + prefix: str + path: PathLike + kwargs: Dict[str, Any] + + def __repr__(self) -> str: + info = [] + for name, value in sorted(self.kwargs.items()): + info.append(f", {name}={value!r}") + return " {path}" "{info}>".format( + prefix=self.prefix, path=self.path, info="".join(info) + ) + + def register(self, router: UrlDispatcher) -> List[AbstractRoute]: + resource = router.add_static(self.prefix, self.path, **self.kwargs) + routes = resource.get_info().get("routes", {}) + return list(routes.values()) + + +def route(method: str, path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return RouteDef(method, path, handler, kwargs) + + +def head(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_HEAD, path, handler, **kwargs) + + +def options(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_OPTIONS, path, handler, **kwargs) + + +def get( + path: str, + handler: _HandlerType, + *, + name: Optional[str] = None, + allow_head: bool = True, + **kwargs: Any, +) -> RouteDef: + return route( + hdrs.METH_GET, path, handler, name=name, allow_head=allow_head, **kwargs + ) + + +def post(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_POST, path, handler, **kwargs) + + +def put(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_PUT, path, handler, **kwargs) + + +def patch(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_PATCH, path, handler, **kwargs) + + +def delete(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_DELETE, path, handler, **kwargs) + + +def view(path: str, handler: Type[AbstractView], **kwargs: Any) -> RouteDef: + return route(hdrs.METH_ANY, path, handler, **kwargs) + + +def static(prefix: str, path: PathLike, **kwargs: Any) -> StaticDef: + return StaticDef(prefix, path, kwargs) + + +_Deco = Callable[[_HandlerType], _HandlerType] + + +class RouteTableDef(Sequence[AbstractRouteDef]): + """Route definition table""" + + def __init__(self) -> None: + self._items: List[AbstractRouteDef] = [] + + def __repr__(self) -> str: + return f"" + + @overload + def __getitem__(self, index: int) -> AbstractRouteDef: + ... + + @overload + def __getitem__(self, index: slice) -> List[AbstractRouteDef]: + ... + + def __getitem__(self, index): # type: ignore[no-untyped-def] + return self._items[index] + + def __iter__(self) -> Iterator[AbstractRouteDef]: + return iter(self._items) + + def __len__(self) -> int: + return len(self._items) + + def __contains__(self, item: object) -> bool: + return item in self._items + + def route(self, method: str, path: str, **kwargs: Any) -> _Deco: + def inner(handler: _HandlerType) -> _HandlerType: + self._items.append(RouteDef(method, path, handler, kwargs)) + return handler + + return inner + + def head(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_HEAD, path, **kwargs) + + def get(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_GET, path, **kwargs) + + def post(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_POST, path, **kwargs) + + def put(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_PUT, path, **kwargs) + + def patch(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_PATCH, path, **kwargs) + + def delete(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_DELETE, path, **kwargs) + + def options(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_OPTIONS, path, **kwargs) + + def view(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_ANY, path, **kwargs) + + def static(self, prefix: str, path: PathLike, **kwargs: Any) -> None: + self._items.append(StaticDef(prefix, path, kwargs)) diff --git a/aiohttp/web_runner.py b/aiohttp/web_runner.py new file mode 100644 index 0000000..3063dce --- /dev/null +++ b/aiohttp/web_runner.py @@ -0,0 +1,450 @@ +import asyncio +import signal +import socket +from abc import ABC, abstractmethod +from contextlib import suppress +from typing import Any, List, Optional, Set, Type + +from yarl import URL + +from .abc import AbstractAccessLogger, AbstractStreamWriter +from .http_parser import RawRequestMessage +from .streams import StreamReader +from .typedefs import PathLike +from .web_app import Application +from .web_log import AccessLogger +from .web_protocol import RequestHandler +from .web_request import Request +from .web_server import Server + +try: + from ssl import SSLContext +except ImportError: + SSLContext = object # type: ignore[misc,assignment] + + +__all__ = ( + "BaseSite", + "TCPSite", + "UnixSite", + "NamedPipeSite", + "SockSite", + "BaseRunner", + "AppRunner", + "ServerRunner", + "GracefulExit", +) + + +class GracefulExit(SystemExit): + code = 1 + + +def _raise_graceful_exit() -> None: + raise GracefulExit() + + +class BaseSite(ABC): + __slots__ = ("_runner", "_shutdown_timeout", "_ssl_context", "_backlog", "_server") + + def __init__( + self, + runner: "BaseRunner", + *, + shutdown_timeout: float = 60.0, + ssl_context: Optional[SSLContext] = None, + backlog: int = 128, + ) -> None: + if runner.server is None: + raise RuntimeError("Call runner.setup() before making a site") + self._runner = runner + self._shutdown_timeout = shutdown_timeout + self._ssl_context = ssl_context + self._backlog = backlog + self._server: Optional[asyncio.AbstractServer] = None + + @property + @abstractmethod + def name(self) -> str: + pass # pragma: no cover + + @abstractmethod + async def start(self) -> None: + self._runner._reg_site(self) + + async def stop(self) -> None: + self._runner._check_site(self) + if self._server is None: + self._runner._unreg_site(self) + return # not started yet + self._server.close() + # named pipes do not have wait_closed property + if hasattr(self._server, "wait_closed"): + await self._server.wait_closed() + + # Wait for pending tasks for a given time limit. + with suppress(asyncio.TimeoutError): + await asyncio.wait_for( + self._wait(asyncio.current_task()), timeout=self._shutdown_timeout + ) + + await self._runner.shutdown() + assert self._runner.server + await self._runner.server.shutdown(self._shutdown_timeout) + self._runner._unreg_site(self) + + async def _wait(self, parent_task: Optional["asyncio.Task[object]"]) -> None: + exclude = self._runner.starting_tasks | {asyncio.current_task(), parent_task} + while tasks := asyncio.all_tasks() - exclude: + await asyncio.wait(tasks) + + +class TCPSite(BaseSite): + __slots__ = ("_host", "_port", "_reuse_address", "_reuse_port") + + def __init__( + self, + runner: "BaseRunner", + host: Optional[str] = None, + port: Optional[int] = None, + *, + shutdown_timeout: float = 60.0, + ssl_context: Optional[SSLContext] = None, + backlog: int = 128, + reuse_address: Optional[bool] = None, + reuse_port: Optional[bool] = None, + ) -> None: + super().__init__( + runner, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + ) + self._host = host + if port is None: + port = 8443 if self._ssl_context else 8080 + self._port = port + self._reuse_address = reuse_address + self._reuse_port = reuse_port + + @property + def name(self) -> str: + scheme = "https" if self._ssl_context else "http" + host = "0.0.0.0" if self._host is None else self._host + return str(URL.build(scheme=scheme, host=host, port=self._port)) + + async def start(self) -> None: + await super().start() + loop = asyncio.get_event_loop() + server = self._runner.server + assert server is not None + self._server = await loop.create_server( + server, + self._host, + self._port, + ssl=self._ssl_context, + backlog=self._backlog, + reuse_address=self._reuse_address, + reuse_port=self._reuse_port, + ) + + +class UnixSite(BaseSite): + __slots__ = ("_path",) + + def __init__( + self, + runner: "BaseRunner", + path: PathLike, + *, + shutdown_timeout: float = 60.0, + ssl_context: Optional[SSLContext] = None, + backlog: int = 128, + ) -> None: + super().__init__( + runner, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + ) + self._path = path + + @property + def name(self) -> str: + scheme = "https" if self._ssl_context else "http" + return f"{scheme}://unix:{self._path}:" + + async def start(self) -> None: + await super().start() + loop = asyncio.get_event_loop() + server = self._runner.server + assert server is not None + self._server = await loop.create_unix_server( + server, + self._path, + ssl=self._ssl_context, + backlog=self._backlog, + ) + + +class NamedPipeSite(BaseSite): + __slots__ = ("_path",) + + def __init__( + self, runner: "BaseRunner", path: str, *, shutdown_timeout: float = 60.0 + ) -> None: + loop = asyncio.get_event_loop() + if not isinstance( + loop, asyncio.ProactorEventLoop # type: ignore[attr-defined] + ): + raise RuntimeError( + "Named Pipes only available in proactor" "loop under windows" + ) + super().__init__(runner, shutdown_timeout=shutdown_timeout) + self._path = path + + @property + def name(self) -> str: + return self._path + + async def start(self) -> None: + await super().start() + loop = asyncio.get_event_loop() + server = self._runner.server + assert server is not None + _server = await loop.start_serving_pipe( # type: ignore[attr-defined] + server, self._path + ) + self._server = _server[0] + + +class SockSite(BaseSite): + __slots__ = ("_sock", "_name") + + def __init__( + self, + runner: "BaseRunner", + sock: socket.socket, + *, + shutdown_timeout: float = 60.0, + ssl_context: Optional[SSLContext] = None, + backlog: int = 128, + ) -> None: + super().__init__( + runner, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + ) + self._sock = sock + scheme = "https" if self._ssl_context else "http" + if hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX: + name = f"{scheme}://unix:{sock.getsockname()}:" + else: + host, port = sock.getsockname()[:2] + name = str(URL.build(scheme=scheme, host=host, port=port)) + self._name = name + + @property + def name(self) -> str: + return self._name + + async def start(self) -> None: + await super().start() + loop = asyncio.get_event_loop() + server = self._runner.server + assert server is not None + self._server = await loop.create_server( + server, sock=self._sock, ssl=self._ssl_context, backlog=self._backlog + ) + + +class BaseRunner(ABC): + __slots__ = ("starting_tasks", "_handle_signals", "_kwargs", "_server", "_sites") + + def __init__(self, *, handle_signals: bool = False, **kwargs: Any) -> None: + self._handle_signals = handle_signals + self._kwargs = kwargs + self._server: Optional[Server] = None + self._sites: List[BaseSite] = [] + + @property + def server(self) -> Optional[Server]: + return self._server + + @property + def addresses(self) -> List[Any]: + ret: List[Any] = [] + for site in self._sites: + server = site._server + if server is not None: + sockets = server.sockets # type: ignore[attr-defined] + if sockets is not None: + for sock in sockets: + ret.append(sock.getsockname()) + return ret + + @property + def sites(self) -> Set[BaseSite]: + return set(self._sites) + + async def setup(self) -> None: + loop = asyncio.get_event_loop() + + if self._handle_signals: + try: + loop.add_signal_handler(signal.SIGINT, _raise_graceful_exit) + loop.add_signal_handler(signal.SIGTERM, _raise_graceful_exit) + except NotImplementedError: # pragma: no cover + # add_signal_handler is not implemented on Windows + pass + + self._server = await self._make_server() + # On shutdown we want to avoid waiting on tasks which run forever. + # It's very likely that all tasks which run forever will have been created by + # the time we have completed the application startup (in self._make_server()), + # so we just record all running tasks here and exclude them later. + self.starting_tasks = asyncio.all_tasks() + + @abstractmethod + async def shutdown(self) -> None: + pass # pragma: no cover + + async def cleanup(self) -> None: + loop = asyncio.get_event_loop() + + # The loop over sites is intentional, an exception on gather() + # leaves self._sites in unpredictable state. + # The loop guarantees that a site is either deleted on success or + # still present on failure + for site in list(self._sites): + await site.stop() + await self._cleanup_server() + self._server = None + if self._handle_signals: + try: + loop.remove_signal_handler(signal.SIGINT) + loop.remove_signal_handler(signal.SIGTERM) + except NotImplementedError: # pragma: no cover + # remove_signal_handler is not implemented on Windows + pass + + @abstractmethod + async def _make_server(self) -> Server: + pass # pragma: no cover + + @abstractmethod + async def _cleanup_server(self) -> None: + pass # pragma: no cover + + def _reg_site(self, site: BaseSite) -> None: + if site in self._sites: + raise RuntimeError(f"Site {site} is already registered in runner {self}") + self._sites.append(site) + + def _check_site(self, site: BaseSite) -> None: + if site not in self._sites: + raise RuntimeError(f"Site {site} is not registered in runner {self}") + + def _unreg_site(self, site: BaseSite) -> None: + if site not in self._sites: + raise RuntimeError(f"Site {site} is not registered in runner {self}") + self._sites.remove(site) + + +class ServerRunner(BaseRunner): + """Low-level web server runner""" + + __slots__ = ("_web_server",) + + def __init__( + self, web_server: Server, *, handle_signals: bool = False, **kwargs: Any + ) -> None: + super().__init__(handle_signals=handle_signals, **kwargs) + self._web_server = web_server + + async def shutdown(self) -> None: + pass + + async def _make_server(self) -> Server: + return self._web_server + + async def _cleanup_server(self) -> None: + pass + + +class AppRunner(BaseRunner): + """Web Application runner""" + + __slots__ = ("_app",) + + def __init__( + self, + app: Application, + *, + handle_signals: bool = False, + access_log_class: Type[AbstractAccessLogger] = AccessLogger, + **kwargs: Any, + ) -> None: + if not isinstance(app, Application): + raise TypeError( + "The first argument should be web.Application " + "instance, got {!r}".format(app) + ) + kwargs["access_log_class"] = access_log_class + + if app._handler_args: + for k, v in app._handler_args.items(): + kwargs[k] = v + + if not issubclass(kwargs["access_log_class"], AbstractAccessLogger): + raise TypeError( + "access_log_class must be subclass of " + "aiohttp.abc.AbstractAccessLogger, got {}".format( + kwargs["access_log_class"] + ) + ) + + super().__init__(handle_signals=handle_signals, **kwargs) + self._app = app + + @property + def app(self) -> Application: + return self._app + + async def shutdown(self) -> None: + await self._app.shutdown() + + async def _make_server(self) -> Server: + self._app.on_startup.freeze() + await self._app.startup() + self._app.freeze() + + return Server( + self._app._handle, # type: ignore[arg-type] + request_factory=self._make_request, + **self._kwargs, + ) + + def _make_request( + self, + message: RawRequestMessage, + payload: StreamReader, + protocol: RequestHandler, + writer: AbstractStreamWriter, + task: "asyncio.Task[None]", + _cls: Type[Request] = Request, + ) -> Request: + loop = asyncio.get_running_loop() + return _cls( + message, + payload, + protocol, + writer, + task, + loop, + client_max_size=self.app._client_max_size, + ) + + async def _cleanup_server(self) -> None: + await self._app.cleanup() diff --git a/aiohttp/web_server.py b/aiohttp/web_server.py new file mode 100644 index 0000000..a3d658a --- /dev/null +++ b/aiohttp/web_server.py @@ -0,0 +1,79 @@ +"""Low level HTTP server.""" +import asyncio +import warnings +from typing import Any, Awaitable, Callable, Dict, List, Optional # noqa + +from .abc import AbstractStreamWriter +from .http_parser import RawRequestMessage +from .streams import StreamReader +from .web_protocol import RequestHandler, _RequestFactory, _RequestHandler +from .web_request import BaseRequest + +__all__ = ("Server",) + + +class Server: + def __init__( + self, + handler: _RequestHandler, + *, + request_factory: Optional[_RequestFactory] = None, + debug: Optional[bool] = None, + handler_cancellation: bool = False, + **kwargs: Any, + ) -> None: + if debug is not None: + warnings.warn( + "debug argument is no-op since 4.0 " "and scheduled for removal in 5.0", + DeprecationWarning, + stacklevel=2, + ) + self._loop = asyncio.get_running_loop() + self._connections: Dict[RequestHandler, asyncio.Transport] = {} + self._kwargs = kwargs + self.requests_count = 0 + self.request_handler = handler + self.request_factory = request_factory or self._make_request + self.handler_cancellation = handler_cancellation + + @property + def connections(self) -> List[RequestHandler]: + return list(self._connections.keys()) + + def connection_made( + self, handler: RequestHandler, transport: asyncio.Transport + ) -> None: + self._connections[handler] = transport + + def connection_lost( + self, handler: RequestHandler, exc: Optional[BaseException] = None + ) -> None: + if handler in self._connections: + del self._connections[handler] + + def _make_request( + self, + message: RawRequestMessage, + payload: StreamReader, + protocol: RequestHandler, + writer: AbstractStreamWriter, + task: "asyncio.Task[None]", + ) -> BaseRequest: + return BaseRequest(message, payload, protocol, writer, task, self._loop) + + async def shutdown(self, timeout: Optional[float] = None) -> None: + coros = [conn.shutdown(timeout) for conn in self._connections] + await asyncio.gather(*coros) + self._connections.clear() + + def __call__(self) -> RequestHandler: + try: + return RequestHandler(self, loop=self._loop, **self._kwargs) + except TypeError: + # Failsafe creation: remove all custom handler_args + kwargs = { + k: v + for k, v in self._kwargs.items() + if k in ["debug", "access_log_class"] + } + return RequestHandler(self, loop=self._loop, **kwargs) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py new file mode 100644 index 0000000..5c80323 --- /dev/null +++ b/aiohttp/web_urldispatcher.py @@ -0,0 +1,1198 @@ +import abc +import asyncio +import base64 +import hashlib +import keyword +import os +import re +from contextlib import contextmanager +from pathlib import Path +from types import MappingProxyType +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Container, + Dict, + Final, + Generator, + Iterable, + Iterator, + List, + Mapping, + NoReturn, + Optional, + Pattern, + Set, + Sized, + Tuple, + Type, + TypedDict, + Union, + cast, +) + +from yarl import URL, __version__ as yarl_version # type: ignore[attr-defined] + +from . import hdrs +from .abc import AbstractMatchInfo, AbstractRouter, AbstractView +from .helpers import DEBUG +from .http import HttpVersion11 +from .typedefs import Handler, PathLike +from .web_exceptions import ( + HTTPException, + HTTPExpectationFailed, + HTTPForbidden, + HTTPMethodNotAllowed, + HTTPNotFound, +) +from .web_fileresponse import FileResponse +from .web_request import Request +from .web_response import Response, StreamResponse +from .web_routedef import AbstractRouteDef + +__all__ = ( + "UrlDispatcher", + "UrlMappingMatchInfo", + "AbstractResource", + "Resource", + "PlainResource", + "DynamicResource", + "AbstractRoute", + "ResourceRoute", + "StaticResource", + "View", +) + + +if TYPE_CHECKING: # pragma: no cover + from .web_app import Application + + BaseDict = Dict[str, str] +else: + BaseDict = dict + +YARL_VERSION: Final[Tuple[int, ...]] = tuple(map(int, yarl_version.split(".")[:2])) + +HTTP_METHOD_RE: Final[Pattern[str]] = re.compile( + r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$" +) +ROUTE_RE: Final[Pattern[str]] = re.compile( + r"(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})" +) +PATH_SEP: Final[str] = re.escape("/") + + +_ExpectHandler = Callable[[Request], Awaitable[Optional[StreamResponse]]] +_Resolve = Tuple[Optional["UrlMappingMatchInfo"], Set[str]] + + +class _InfoDict(TypedDict, total=False): + path: str + + formatter: str + pattern: Pattern[str] + + directory: Path + prefix: str + routes: Mapping[str, "AbstractRoute"] + + app: "Application" + + domain: str + + rule: "AbstractRuleMatching" + + http_exception: HTTPException + + +class AbstractResource(Sized, Iterable["AbstractRoute"]): + def __init__(self, *, name: Optional[str] = None) -> None: + self._name = name + + @property + def name(self) -> Optional[str]: + return self._name + + @property + @abc.abstractmethod + def canonical(self) -> str: + """Exposes the resource's canonical path. + + For example '/foo/bar/{name}' + + """ + + @abc.abstractmethod # pragma: no branch + def url_for(self, **kwargs: str) -> URL: + """Construct url for resource with additional params.""" + + @abc.abstractmethod # pragma: no branch + async def resolve(self, request: Request) -> _Resolve: + """Resolve resource. + + Return (UrlMappingMatchInfo, allowed_methods) pair. + """ + + @abc.abstractmethod + def add_prefix(self, prefix: str) -> None: + """Add a prefix to processed URLs. + + Required for subapplications support. + """ + + @abc.abstractmethod + def get_info(self) -> _InfoDict: + """Return a dict with additional info useful for introspection""" + + def freeze(self) -> None: + pass + + @abc.abstractmethod + def raw_match(self, path: str) -> bool: + """Perform a raw match against path""" + + +class AbstractRoute(abc.ABC): + def __init__( + self, + method: str, + handler: Union[Handler, Type[AbstractView]], + *, + expect_handler: Optional[_ExpectHandler] = None, + resource: Optional[AbstractResource] = None, + ) -> None: + if expect_handler is None: + expect_handler = _default_expect_handler + + assert asyncio.iscoroutinefunction( + expect_handler + ), f"Coroutine is expected, got {expect_handler!r}" + + method = method.upper() + if not HTTP_METHOD_RE.match(method): + raise ValueError(f"{method} is not allowed HTTP method") + + if asyncio.iscoroutinefunction(handler): + pass + elif isinstance(handler, type) and issubclass(handler, AbstractView): + pass + else: + raise TypeError( + "Only async functions are allowed as web-handlers " + ", got {!r}".format(handler) + ) + + self._method = method + self._handler = handler + self._expect_handler = expect_handler + self._resource = resource + + @property + def method(self) -> str: + return self._method + + @property + def handler(self) -> Handler: + return self._handler + + @property + @abc.abstractmethod + def name(self) -> Optional[str]: + """Optional route's name, always equals to resource's name.""" + + @property + def resource(self) -> Optional[AbstractResource]: + return self._resource + + @abc.abstractmethod + def get_info(self) -> _InfoDict: + """Return a dict with additional info useful for introspection""" + + @abc.abstractmethod # pragma: no branch + def url_for(self, *args: str, **kwargs: str) -> URL: + """Construct url for route with additional params.""" + + async def handle_expect_header(self, request: Request) -> Optional[StreamResponse]: + return await self._expect_handler(request) + + +class UrlMappingMatchInfo(BaseDict, AbstractMatchInfo): + def __init__(self, match_dict: Dict[str, str], route: AbstractRoute): + super().__init__(match_dict) + self._route = route + self._apps: List[Application] = [] + self._current_app: Optional[Application] = None + self._frozen = False + + @property + def handler(self) -> Handler: + return self._route.handler + + @property + def route(self) -> AbstractRoute: + return self._route + + @property + def expect_handler(self) -> _ExpectHandler: + return self._route.handle_expect_header + + @property + def http_exception(self) -> Optional[HTTPException]: + return None + + def get_info(self) -> _InfoDict: # type: ignore[override] + return self._route.get_info() + + @property + def apps(self) -> Tuple["Application", ...]: + return tuple(self._apps) + + def add_app(self, app: "Application") -> None: + if self._frozen: + raise RuntimeError("Cannot change apps stack after .freeze() call") + if self._current_app is None: + self._current_app = app + self._apps.insert(0, app) + + @property + def current_app(self) -> "Application": + app = self._current_app + assert app is not None + return app + + @contextmanager + def set_current_app(self, app: "Application") -> Generator[None, None, None]: + if DEBUG: # pragma: no cover + if app not in self._apps: + raise RuntimeError( + "Expected one of the following apps {!r}, got {!r}".format( + self._apps, app + ) + ) + prev = self._current_app + self._current_app = app + try: + yield + finally: + self._current_app = prev + + def freeze(self) -> None: + self._frozen = True + + def __repr__(self) -> str: + return f"" + + +class MatchInfoError(UrlMappingMatchInfo): + def __init__(self, http_exception: HTTPException) -> None: + self._exception = http_exception + super().__init__({}, SystemRoute(self._exception)) + + @property + def http_exception(self) -> HTTPException: + return self._exception + + def __repr__(self) -> str: + return "".format( + self._exception.status, self._exception.reason + ) + + +async def _default_expect_handler(request: Request) -> None: + """Default handler for Expect header. + + Just send "100 Continue" to client. + raise HTTPExpectationFailed if value of header is not "100-continue" + """ + expect = request.headers.get(hdrs.EXPECT, "") + if request.version == HttpVersion11: + if expect.lower() == "100-continue": + await request.writer.write(b"HTTP/1.1 100 Continue\r\n\r\n") + else: + raise HTTPExpectationFailed(text="Unknown Expect: %s" % expect) + + +class Resource(AbstractResource): + def __init__(self, *, name: Optional[str] = None) -> None: + super().__init__(name=name) + self._routes: List[ResourceRoute] = [] + + def add_route( + self, + method: str, + handler: Union[Type[AbstractView], Handler], + *, + expect_handler: Optional[_ExpectHandler] = None, + ) -> "ResourceRoute": + for route_obj in self._routes: + if route_obj.method == method or route_obj.method == hdrs.METH_ANY: + raise RuntimeError( + "Added route will never be executed, " + "method {route.method} is already " + "registered".format(route=route_obj) + ) + + route_obj = ResourceRoute(method, handler, self, expect_handler=expect_handler) + self.register_route(route_obj) + return route_obj + + def register_route(self, route: "ResourceRoute") -> None: + assert isinstance( + route, ResourceRoute + ), f"Instance of Route class is required, got {route!r}" + self._routes.append(route) + + async def resolve(self, request: Request) -> _Resolve: + allowed_methods: Set[str] = set() + + match_dict = self._match(request.rel_url.raw_path) + if match_dict is None: + return None, allowed_methods + + for route_obj in self._routes: + route_method = route_obj.method + allowed_methods.add(route_method) + + if route_method == request.method or route_method == hdrs.METH_ANY: + return (UrlMappingMatchInfo(match_dict, route_obj), allowed_methods) + else: + return None, allowed_methods + + @abc.abstractmethod + def _match(self, path: str) -> Optional[Dict[str, str]]: + pass # pragma: no cover + + def __len__(self) -> int: + return len(self._routes) + + def __iter__(self) -> Iterator["ResourceRoute"]: + return iter(self._routes) + + # TODO: implement all abstract methods + + +class PlainResource(Resource): + def __init__(self, path: str, *, name: Optional[str] = None) -> None: + super().__init__(name=name) + assert not path or path.startswith("/") + self._path = path + + @property + def canonical(self) -> str: + return self._path + + def freeze(self) -> None: + if not self._path: + self._path = "/" + + def add_prefix(self, prefix: str) -> None: + assert prefix.startswith("/") + assert not prefix.endswith("/") + assert len(prefix) > 1 + self._path = prefix + self._path + + def _match(self, path: str) -> Optional[Dict[str, str]]: + # string comparison is about 10 times faster than regexp matching + if self._path == path: + return {} + else: + return None + + def raw_match(self, path: str) -> bool: + return self._path == path + + def get_info(self) -> _InfoDict: + return {"path": self._path} + + def url_for(self) -> URL: # type: ignore[override] + return URL.build(path=self._path, encoded=True) + + def __repr__(self) -> str: + name = "'" + self.name + "' " if self.name is not None else "" + return f"" + + +class DynamicResource(Resource): + DYN = re.compile(r"\{(?P[_a-zA-Z][_a-zA-Z0-9]*)\}") + DYN_WITH_RE = re.compile(r"\{(?P[_a-zA-Z][_a-zA-Z0-9]*):(?P.+)\}") + GOOD = r"[^{}/]+" + + def __init__(self, path: str, *, name: Optional[str] = None) -> None: + super().__init__(name=name) + pattern = "" + formatter = "" + for part in ROUTE_RE.split(path): + match = self.DYN.fullmatch(part) + if match: + pattern += "(?P<{}>{})".format(match.group("var"), self.GOOD) + formatter += "{" + match.group("var") + "}" + continue + + match = self.DYN_WITH_RE.fullmatch(part) + if match: + pattern += "(?P<{var}>{re})".format(**match.groupdict()) + formatter += "{" + match.group("var") + "}" + continue + + if "{" in part or "}" in part: + raise ValueError(f"Invalid path '{path}'['{part}']") + + part = _requote_path(part) + formatter += part + pattern += re.escape(part) + + try: + compiled = re.compile(pattern) + except re.error as exc: + raise ValueError(f"Bad pattern '{pattern}': {exc}") from None + assert compiled.pattern.startswith(PATH_SEP) + assert formatter.startswith("/") + self._pattern = compiled + self._formatter = formatter + + @property + def canonical(self) -> str: + return self._formatter + + def add_prefix(self, prefix: str) -> None: + assert prefix.startswith("/") + assert not prefix.endswith("/") + assert len(prefix) > 1 + self._pattern = re.compile(re.escape(prefix) + self._pattern.pattern) + self._formatter = prefix + self._formatter + + def _match(self, path: str) -> Optional[Dict[str, str]]: + match = self._pattern.fullmatch(path) + if match is None: + return None + else: + return { + key: _unquote_path(value) for key, value in match.groupdict().items() + } + + def raw_match(self, path: str) -> bool: + return self._formatter == path + + def get_info(self) -> _InfoDict: + return {"formatter": self._formatter, "pattern": self._pattern} + + def url_for(self, **parts: str) -> URL: + url = self._formatter.format_map({k: _quote_path(v) for k, v in parts.items()}) + return URL.build(path=url, encoded=True) + + def __repr__(self) -> str: + name = "'" + self.name + "' " if self.name is not None else "" + return "".format( + name=name, formatter=self._formatter + ) + + +class PrefixResource(AbstractResource): + def __init__(self, prefix: str, *, name: Optional[str] = None) -> None: + assert not prefix or prefix.startswith("/"), prefix + assert prefix in ("", "/") or not prefix.endswith("/"), prefix + super().__init__(name=name) + self._prefix = _requote_path(prefix) + self._prefix2 = self._prefix + "/" + + @property + def canonical(self) -> str: + return self._prefix + + def add_prefix(self, prefix: str) -> None: + assert prefix.startswith("/") + assert not prefix.endswith("/") + assert len(prefix) > 1 + self._prefix = prefix + self._prefix + self._prefix2 = self._prefix + "/" + + def raw_match(self, prefix: str) -> bool: + return False + + # TODO: impl missing abstract methods + + +class StaticResource(PrefixResource): + VERSION_KEY = "v" + + def __init__( + self, + prefix: str, + directory: PathLike, + *, + name: Optional[str] = None, + expect_handler: Optional[_ExpectHandler] = None, + chunk_size: int = 256 * 1024, + show_index: bool = False, + follow_symlinks: bool = False, + append_version: bool = False, + ) -> None: + super().__init__(prefix, name=name) + try: + directory = Path(directory) + if str(directory).startswith("~"): + directory = Path(os.path.expanduser(str(directory))) + directory = directory.resolve() + if not directory.is_dir(): + raise ValueError("Not a directory") + except (FileNotFoundError, ValueError) as error: + raise ValueError(f"No directory exists at '{directory}'") from error + self._directory = directory + self._show_index = show_index + self._chunk_size = chunk_size + self._follow_symlinks = follow_symlinks + self._expect_handler = expect_handler + self._append_version = append_version + + self._routes = { + "GET": ResourceRoute( + "GET", self._handle, self, expect_handler=expect_handler + ), + "HEAD": ResourceRoute( + "HEAD", self._handle, self, expect_handler=expect_handler + ), + } + + def url_for( # type: ignore[override] + self, + *, + filename: PathLike, + append_version: Optional[bool] = None, + ) -> URL: + if append_version is None: + append_version = self._append_version + filename = str(filename).lstrip("/") + + url = URL.build(path=self._prefix, encoded=True) + # filename is not encoded + if YARL_VERSION < (1, 6): + url = url / filename.replace("%", "%25") + else: + url = url / filename + + if append_version: + try: + filepath = self._directory.joinpath(filename).resolve() + if not self._follow_symlinks: + filepath.relative_to(self._directory) + except (ValueError, FileNotFoundError): + # ValueError for case when path point to symlink + # with follow_symlinks is False + return url # relatively safe + if filepath.is_file(): + # TODO cache file content + # with file watcher for cache invalidation + with filepath.open("rb") as f: + file_bytes = f.read() + h = self._get_file_hash(file_bytes) + url = url.with_query({self.VERSION_KEY: h}) + return url + return url + + @staticmethod + def _get_file_hash(byte_array: bytes) -> str: + m = hashlib.sha256() # todo sha256 can be configurable param + m.update(byte_array) + b64 = base64.urlsafe_b64encode(m.digest()) + return b64.decode("ascii") + + def get_info(self) -> _InfoDict: + return { + "directory": self._directory, + "prefix": self._prefix, + "routes": self._routes, + } + + def set_options_route(self, handler: Handler) -> None: + if "OPTIONS" in self._routes: + raise RuntimeError("OPTIONS route was set already") + self._routes["OPTIONS"] = ResourceRoute( + "OPTIONS", handler, self, expect_handler=self._expect_handler + ) + + async def resolve(self, request: Request) -> _Resolve: + path = request.rel_url.raw_path + method = request.method + allowed_methods = set(self._routes) + if not path.startswith(self._prefix2) and path != self._prefix: + return None, set() + + if method not in allowed_methods: + return None, allowed_methods + + match_dict = {"filename": _unquote_path(path[len(self._prefix) + 1 :])} + return (UrlMappingMatchInfo(match_dict, self._routes[method]), allowed_methods) + + def __len__(self) -> int: + return len(self._routes) + + def __iter__(self) -> Iterator[AbstractRoute]: + return iter(self._routes.values()) + + async def _handle(self, request: Request) -> StreamResponse: + rel_url = request.match_info["filename"] + try: + filename = Path(rel_url) + if filename.anchor: + # rel_url is an absolute name like + # /static/\\machine_name\c$ or /static/D:\path + # where the static dir is totally different + raise HTTPForbidden() + filepath = self._directory.joinpath(filename).resolve() + if not self._follow_symlinks: + filepath.relative_to(self._directory) + except (ValueError, FileNotFoundError) as error: + # relatively safe + raise HTTPNotFound() from error + except HTTPForbidden: + raise + except Exception as error: + # perm error or other kind! + request.app.logger.exception(error) + raise HTTPNotFound() from error + + # on opening a dir, load its contents if allowed + if filepath.is_dir(): + if self._show_index: + try: + return Response( + text=self._directory_as_html(filepath), content_type="text/html" + ) + except PermissionError: + raise HTTPForbidden() + else: + raise HTTPForbidden() + elif filepath.is_file(): + return FileResponse(filepath, chunk_size=self._chunk_size) + else: + raise HTTPNotFound + + def _directory_as_html(self, filepath: Path) -> str: + # returns directory's index as html + + # sanity check + assert filepath.is_dir() + + relative_path_to_dir = filepath.relative_to(self._directory).as_posix() + index_of = f"Index of /{relative_path_to_dir}" + h1 = f"

{index_of}

" + + index_list = [] + dir_index = filepath.iterdir() + for _file in sorted(dir_index): + # show file url as relative to static path + rel_path = _file.relative_to(self._directory).as_posix() + file_url = self._prefix + "/" + rel_path + + # if file is a directory, add '/' to the end of the name + if _file.is_dir(): + file_name = f"{_file.name}/" + else: + file_name = _file.name + + index_list.append( + '
  • {name}
  • '.format( + url=file_url, name=file_name + ) + ) + ul = "
      \n{}\n
    ".format("\n".join(index_list)) + body = f"\n{h1}\n{ul}\n" + + head_str = f"\n{index_of}\n" + html = f"\n{head_str}\n{body}\n" + + return html + + def __repr__(self) -> str: + name = "'" + self.name + "'" if self.name is not None else "" + return " {directory!r}>".format( + name=name, path=self._prefix, directory=self._directory + ) + + +class PrefixedSubAppResource(PrefixResource): + def __init__(self, prefix: str, app: "Application") -> None: + super().__init__(prefix) + self._app = app + for resource in app.router.resources(): + resource.add_prefix(prefix) + + def add_prefix(self, prefix: str) -> None: + super().add_prefix(prefix) + for resource in self._app.router.resources(): + resource.add_prefix(prefix) + + def url_for(self, *args: str, **kwargs: str) -> URL: + raise RuntimeError(".url_for() is not supported " "by sub-application root") + + def get_info(self) -> _InfoDict: + return {"app": self._app, "prefix": self._prefix} + + async def resolve(self, request: Request) -> _Resolve: + if ( + not request.url.raw_path.startswith(self._prefix2) + and request.url.raw_path != self._prefix + ): + return None, set() + match_info = await self._app.router.resolve(request) + match_info.add_app(self._app) + if isinstance(match_info.http_exception, HTTPMethodNotAllowed): + methods = match_info.http_exception.allowed_methods + else: + methods = set() + return match_info, methods + + def __len__(self) -> int: + return len(self._app.router.routes()) + + def __iter__(self) -> Iterator[AbstractRoute]: + return iter(self._app.router.routes()) + + def __repr__(self) -> str: + return " {app!r}>".format( + prefix=self._prefix, app=self._app + ) + + +class AbstractRuleMatching(abc.ABC): + @abc.abstractmethod # pragma: no branch + async def match(self, request: Request) -> bool: + """Return bool if the request satisfies the criteria""" + + @abc.abstractmethod # pragma: no branch + def get_info(self) -> _InfoDict: + """Return a dict with additional info useful for introspection""" + + @property + @abc.abstractmethod # pragma: no branch + def canonical(self) -> str: + """Return a str""" + + +class Domain(AbstractRuleMatching): + re_part = re.compile(r"(?!-)[a-z\d-]{1,63}(? None: + super().__init__() + self._domain = self.validation(domain) + + @property + def canonical(self) -> str: + return self._domain + + def validation(self, domain: str) -> str: + if not isinstance(domain, str): + raise TypeError("Domain must be str") + domain = domain.rstrip(".").lower() + if not domain: + raise ValueError("Domain cannot be empty") + elif "://" in domain: + raise ValueError("Scheme not supported") + url = URL("http://" + domain) + assert url.raw_host is not None + if not all(self.re_part.fullmatch(x) for x in url.raw_host.split(".")): + raise ValueError("Domain not valid") + if url.port == 80: + return url.raw_host + return f"{url.raw_host}:{url.port}" + + async def match(self, request: Request) -> bool: + host = request.headers.get(hdrs.HOST) + if not host: + return False + return self.match_domain(host) + + def match_domain(self, host: str) -> bool: + return host.lower() == self._domain + + def get_info(self) -> _InfoDict: + return {"domain": self._domain} + + +class MaskDomain(Domain): + re_part = re.compile(r"(?!-)[a-z\d\*-]{1,63}(? None: + super().__init__(domain) + mask = self._domain.replace(".", r"\.").replace("*", ".*") + self._mask = re.compile(mask) + + @property + def canonical(self) -> str: + return self._mask.pattern + + def match_domain(self, host: str) -> bool: + return self._mask.fullmatch(host) is not None + + +class MatchedSubAppResource(PrefixedSubAppResource): + def __init__(self, rule: AbstractRuleMatching, app: "Application") -> None: + AbstractResource.__init__(self) + self._prefix = "" + self._app = app + self._rule = rule + + @property + def canonical(self) -> str: + return self._rule.canonical + + def get_info(self) -> _InfoDict: + return {"app": self._app, "rule": self._rule} + + async def resolve(self, request: Request) -> _Resolve: + if not await self._rule.match(request): + return None, set() + match_info = await self._app.router.resolve(request) + match_info.add_app(self._app) + if isinstance(match_info.http_exception, HTTPMethodNotAllowed): + methods = match_info.http_exception.allowed_methods + else: + methods = set() + return match_info, methods + + def __repr__(self) -> str: + return " {app!r}>" "".format(app=self._app) + + +class ResourceRoute(AbstractRoute): + """A route with resource""" + + def __init__( + self, + method: str, + handler: Union[Handler, Type[AbstractView]], + resource: AbstractResource, + *, + expect_handler: Optional[_ExpectHandler] = None, + ) -> None: + super().__init__( + method, handler, expect_handler=expect_handler, resource=resource + ) + + def __repr__(self) -> str: + return " {handler!r}".format( + method=self.method, resource=self._resource, handler=self.handler + ) + + @property + def name(self) -> Optional[str]: + if self._resource is None: + return None + return self._resource.name + + def url_for(self, *args: str, **kwargs: str) -> URL: + """Construct url for route with additional params.""" + assert self._resource is not None + return self._resource.url_for(*args, **kwargs) + + def get_info(self) -> _InfoDict: + assert self._resource is not None + return self._resource.get_info() + + +class SystemRoute(AbstractRoute): + def __init__(self, http_exception: HTTPException) -> None: + super().__init__(hdrs.METH_ANY, self._handle) + self._http_exception = http_exception + + def url_for(self, *args: str, **kwargs: str) -> URL: + raise RuntimeError(".url_for() is not allowed for SystemRoute") + + @property + def name(self) -> Optional[str]: + return None + + def get_info(self) -> _InfoDict: + return {"http_exception": self._http_exception} + + async def _handle(self, request: Request) -> StreamResponse: + raise self._http_exception + + @property + def status(self) -> int: + return self._http_exception.status + + @property + def reason(self) -> str: + return self._http_exception.reason + + def __repr__(self) -> str: + return "".format(self=self) + + +class View(AbstractView): + async def _iter(self) -> StreamResponse: + if self.request.method not in hdrs.METH_ALL: + self._raise_allowed_methods() + method: Optional[Callable[[], Awaitable[StreamResponse]]] = getattr( + self, self.request.method.lower(), None + ) + if method is None: + self._raise_allowed_methods() + return await method() + + def __await__(self) -> Generator[Any, None, StreamResponse]: + return self._iter().__await__() + + def _raise_allowed_methods(self) -> NoReturn: + allowed_methods = {m for m in hdrs.METH_ALL if hasattr(self, m.lower())} + raise HTTPMethodNotAllowed(self.request.method, allowed_methods) + + +class ResourcesView(Sized, Iterable[AbstractResource], Container[AbstractResource]): + def __init__(self, resources: List[AbstractResource]) -> None: + self._resources = resources + + def __len__(self) -> int: + return len(self._resources) + + def __iter__(self) -> Iterator[AbstractResource]: + yield from self._resources + + def __contains__(self, resource: object) -> bool: + return resource in self._resources + + +class RoutesView(Sized, Iterable[AbstractRoute], Container[AbstractRoute]): + def __init__(self, resources: List[AbstractResource]): + self._routes: List[AbstractRoute] = [] + for resource in resources: + for route in resource: + self._routes.append(route) + + def __len__(self) -> int: + return len(self._routes) + + def __iter__(self) -> Iterator[AbstractRoute]: + yield from self._routes + + def __contains__(self, route: object) -> bool: + return route in self._routes + + +class UrlDispatcher(AbstractRouter, Mapping[str, AbstractResource]): + NAME_SPLIT_RE = re.compile(r"[.:-]") + + def __init__(self) -> None: + super().__init__() + self._resources: List[AbstractResource] = [] + self._named_resources: Dict[str, AbstractResource] = {} + + async def resolve(self, request: Request) -> UrlMappingMatchInfo: + method = request.method + allowed_methods: Set[str] = set() + + for resource in self._resources: + match_dict, allowed = await resource.resolve(request) + if match_dict is not None: + return match_dict + else: + allowed_methods |= allowed + + if allowed_methods: + return MatchInfoError(HTTPMethodNotAllowed(method, allowed_methods)) + else: + return MatchInfoError(HTTPNotFound()) + + def __iter__(self) -> Iterator[str]: + return iter(self._named_resources) + + def __len__(self) -> int: + return len(self._named_resources) + + def __contains__(self, resource: object) -> bool: + return resource in self._named_resources + + def __getitem__(self, name: str) -> AbstractResource: + return self._named_resources[name] + + def resources(self) -> ResourcesView: + return ResourcesView(self._resources) + + def routes(self) -> RoutesView: + return RoutesView(self._resources) + + def named_resources(self) -> Mapping[str, AbstractResource]: + return MappingProxyType(self._named_resources) + + def register_resource(self, resource: AbstractResource) -> None: + assert isinstance( + resource, AbstractResource + ), f"Instance of AbstractResource class is required, got {resource!r}" + if self.frozen: + raise RuntimeError("Cannot register a resource into frozen router.") + + name = resource.name + + if name is not None: + parts = self.NAME_SPLIT_RE.split(name) + for part in parts: + if keyword.iskeyword(part): + raise ValueError( + f"Incorrect route name {name!r}, " + "python keywords cannot be used " + "for route name" + ) + if not part.isidentifier(): + raise ValueError( + "Incorrect route name {!r}, " + "the name should be a sequence of " + "python identifiers separated " + "by dash, dot or column".format(name) + ) + if name in self._named_resources: + raise ValueError( + "Duplicate {!r}, " + "already handled by {!r}".format(name, self._named_resources[name]) + ) + self._named_resources[name] = resource + self._resources.append(resource) + + def add_resource(self, path: str, *, name: Optional[str] = None) -> Resource: + if path and not path.startswith("/"): + raise ValueError("path should be started with / or be empty") + # Reuse last added resource if path and name are the same + if self._resources: + resource = self._resources[-1] + if resource.name == name and resource.raw_match(path): + return cast(Resource, resource) + if not ("{" in path or "}" in path or ROUTE_RE.search(path)): + resource = PlainResource(_requote_path(path), name=name) + self.register_resource(resource) + return resource + resource = DynamicResource(path, name=name) + self.register_resource(resource) + return resource + + def add_route( + self, + method: str, + path: str, + handler: Union[Handler, Type[AbstractView]], + *, + name: Optional[str] = None, + expect_handler: Optional[_ExpectHandler] = None, + ) -> AbstractRoute: + resource = self.add_resource(path, name=name) + return resource.add_route(method, handler, expect_handler=expect_handler) + + def add_static( + self, + prefix: str, + path: PathLike, + *, + name: Optional[str] = None, + expect_handler: Optional[_ExpectHandler] = None, + chunk_size: int = 256 * 1024, + show_index: bool = False, + follow_symlinks: bool = False, + append_version: bool = False, + ) -> AbstractResource: + """Add static files view. + + prefix - url prefix + path - folder with files + + """ + assert prefix.startswith("/") + if prefix.endswith("/"): + prefix = prefix[:-1] + resource = StaticResource( + prefix, + path, + name=name, + expect_handler=expect_handler, + chunk_size=chunk_size, + show_index=show_index, + follow_symlinks=follow_symlinks, + append_version=append_version, + ) + self.register_resource(resource) + return resource + + def add_head(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method HEAD.""" + return self.add_route(hdrs.METH_HEAD, path, handler, **kwargs) + + def add_options(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method OPTIONS.""" + return self.add_route(hdrs.METH_OPTIONS, path, handler, **kwargs) + + def add_get( + self, + path: str, + handler: Handler, + *, + name: Optional[str] = None, + allow_head: bool = True, + **kwargs: Any, + ) -> AbstractRoute: + """Shortcut for add_route with method GET. + + If allow_head is true, another + route is added allowing head requests to the same endpoint. + """ + resource = self.add_resource(path, name=name) + if allow_head: + resource.add_route(hdrs.METH_HEAD, handler, **kwargs) + return resource.add_route(hdrs.METH_GET, handler, **kwargs) + + def add_post(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method POST.""" + return self.add_route(hdrs.METH_POST, path, handler, **kwargs) + + def add_put(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method PUT.""" + return self.add_route(hdrs.METH_PUT, path, handler, **kwargs) + + def add_patch(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method PATCH.""" + return self.add_route(hdrs.METH_PATCH, path, handler, **kwargs) + + def add_delete(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method DELETE.""" + return self.add_route(hdrs.METH_DELETE, path, handler, **kwargs) + + def add_view( + self, path: str, handler: Type[AbstractView], **kwargs: Any + ) -> AbstractRoute: + """Shortcut for add_route with ANY methods for a class-based view.""" + return self.add_route(hdrs.METH_ANY, path, handler, **kwargs) + + def freeze(self) -> None: + super().freeze() + for resource in self._resources: + resource.freeze() + + def add_routes(self, routes: Iterable[AbstractRouteDef]) -> List[AbstractRoute]: + """Append routes to route table. + + Parameter should be a sequence of RouteDef objects. + + Returns a list of registered AbstractRoute instances. + """ + registered_routes = [] + for route_def in routes: + registered_routes.extend(route_def.register(self)) + return registered_routes + + +def _quote_path(value: str) -> str: + if YARL_VERSION < (1, 6): + value = value.replace("%", "%25") + return URL.build(path=value, encoded=False).raw_path + + +def _unquote_path(value: str) -> str: + return URL.build(path=value, encoded=True).path + + +def _requote_path(value: str) -> str: + # Quote non-ascii characters and other characters which must be quoted, + # but preserve existing %-sequences. + result = _quote_path(value) + if "%" in value: + result = result.replace("%25", "%") + return result diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py new file mode 100644 index 0000000..9fcdc4b --- /dev/null +++ b/aiohttp/web_ws.py @@ -0,0 +1,525 @@ +import asyncio +import base64 +import binascii +import dataclasses +import hashlib +import json +import sys +from typing import Any, Final, Iterable, Optional, Tuple, cast + +from multidict import CIMultiDict + +from . import hdrs +from .abc import AbstractStreamWriter +from .helpers import call_later, set_result +from .http import ( + WS_CLOSED_MESSAGE, + WS_CLOSING_MESSAGE, + WS_KEY, + WebSocketError, + WebSocketReader, + WebSocketWriter, + WSCloseCode, + WSMessage, + WSMsgType as WSMsgType, + ws_ext_gen, + ws_ext_parse, +) +from .log import ws_logger +from .streams import EofStream, FlowControlDataQueue +from .typedefs import JSONDecoder, JSONEncoder +from .web_exceptions import HTTPBadRequest, HTTPException +from .web_request import BaseRequest +from .web_response import StreamResponse + +if sys.version_info >= (3, 11): + import asyncio as async_timeout +else: + import async_timeout + +__all__ = ( + "WebSocketResponse", + "WebSocketReady", + "WSMsgType", +) + +THRESHOLD_CONNLOST_ACCESS: Final[int] = 5 + + +@dataclasses.dataclass(frozen=True) +class WebSocketReady: + ok: bool + protocol: Optional[str] + + def __bool__(self) -> bool: + return self.ok + + +class WebSocketResponse(StreamResponse): + __slots__ = ( + "_protocols", + "_ws_protocol", + "_writer", + "_reader", + "_closed", + "_closing", + "_conn_lost", + "_close_code", + "_loop", + "_waiting", + "_exception", + "_timeout", + "_receive_timeout", + "_autoclose", + "_autoping", + "_heartbeat", + "_heartbeat_cb", + "_pong_heartbeat", + "_pong_response_cb", + "_compress", + "_max_msg_size", + ) + + def __init__( + self, + *, + timeout: float = 10.0, + receive_timeout: Optional[float] = None, + autoclose: bool = True, + autoping: bool = True, + heartbeat: Optional[float] = None, + protocols: Iterable[str] = (), + compress: bool = True, + max_msg_size: int = 4 * 1024 * 1024, + ) -> None: + super().__init__(status=101) + self._length_check = False + self._protocols = protocols + self._ws_protocol: Optional[str] = None + self._writer: Optional[WebSocketWriter] = None + self._reader: Optional[FlowControlDataQueue[WSMessage]] = None + self._closed = False + self._closing = False + self._conn_lost = 0 + self._close_code: Optional[int] = None + self._loop: Optional[asyncio.AbstractEventLoop] = None + self._waiting: Optional[asyncio.Future[bool]] = None + self._exception: Optional[BaseException] = None + self._timeout = timeout + self._receive_timeout = receive_timeout + self._autoclose = autoclose + self._autoping = autoping + self._heartbeat = heartbeat + self._heartbeat_cb: Optional[asyncio.TimerHandle] = None + if heartbeat is not None: + self._pong_heartbeat = heartbeat / 2.0 + self._pong_response_cb: Optional[asyncio.TimerHandle] = None + self._compress = compress + self._max_msg_size = max_msg_size + + def _cancel_heartbeat(self) -> None: + if self._pong_response_cb is not None: + self._pong_response_cb.cancel() + self._pong_response_cb = None + + if self._heartbeat_cb is not None: + self._heartbeat_cb.cancel() + self._heartbeat_cb = None + + def _reset_heartbeat(self) -> None: + self._cancel_heartbeat() + + if self._heartbeat is not None: + assert self._loop is not None + self._heartbeat_cb = call_later( + self._send_heartbeat, + self._heartbeat, + self._loop, + timeout_ceil_threshold=self._req._protocol._timeout_ceil_threshold + if self._req is not None + else 5, + ) + + def _send_heartbeat(self) -> None: + if self._heartbeat is not None and not self._closed: + assert self._loop is not None and self._writer is not None + # fire-and-forget a task is not perfect but maybe ok for + # sending ping. Otherwise we need a long-living heartbeat + # task in the class. + self._loop.create_task(self._writer.ping()) # type: ignore[unused-awaitable] + + if self._pong_response_cb is not None: + self._pong_response_cb.cancel() + self._pong_response_cb = call_later( + self._pong_not_received, + self._pong_heartbeat, + self._loop, + timeout_ceil_threshold=self._req._protocol._timeout_ceil_threshold + if self._req is not None + else 5, + ) + + def _pong_not_received(self) -> None: + if self._req is not None and self._req.transport is not None: + self._closed = True + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._exception = asyncio.TimeoutError() + self._req.transport.close() + + async def prepare(self, request: BaseRequest) -> AbstractStreamWriter: + # make pre-check to don't hide it by do_handshake() exceptions + if self._payload_writer is not None: + return self._payload_writer + + protocol, writer = self._pre_start(request) + payload_writer = await super().prepare(request) + assert payload_writer is not None + self._post_start(request, protocol, writer) + await payload_writer.drain() + return payload_writer + + def _handshake( + self, request: BaseRequest + ) -> Tuple["CIMultiDict[str]", str, bool, bool]: + headers = request.headers + if "websocket" != headers.get(hdrs.UPGRADE, "").lower().strip(): + raise HTTPBadRequest( + text=( + "No WebSocket UPGRADE hdr: {}\n Can " + '"Upgrade" only to "WebSocket".' + ).format(headers.get(hdrs.UPGRADE)) + ) + + if "upgrade" not in headers.get(hdrs.CONNECTION, "").lower(): + raise HTTPBadRequest( + text="No CONNECTION upgrade hdr: {}".format( + headers.get(hdrs.CONNECTION) + ) + ) + + # find common sub-protocol between client and server + protocol = None + if hdrs.SEC_WEBSOCKET_PROTOCOL in headers: + req_protocols = [ + str(proto.strip()) + for proto in headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",") + ] + + for proto in req_protocols: + if proto in self._protocols: + protocol = proto + break + else: + # No overlap found: Return no protocol as per spec + ws_logger.warning( + "Client protocols %r don’t overlap server-known ones %r", + req_protocols, + self._protocols, + ) + + # check supported version + version = headers.get(hdrs.SEC_WEBSOCKET_VERSION, "") + if version not in ("13", "8", "7"): + raise HTTPBadRequest(text=f"Unsupported version: {version}") + + # check client handshake for validity + key = headers.get(hdrs.SEC_WEBSOCKET_KEY) + try: + if not key or len(base64.b64decode(key)) != 16: + raise HTTPBadRequest(text=f"Handshake error: {key!r}") + except binascii.Error: + raise HTTPBadRequest(text=f"Handshake error: {key!r}") from None + + accept_val = base64.b64encode( + hashlib.sha1(key.encode() + WS_KEY).digest() + ).decode() + response_headers = CIMultiDict( + { + hdrs.UPGRADE: "websocket", + hdrs.CONNECTION: "upgrade", + hdrs.SEC_WEBSOCKET_ACCEPT: accept_val, + } + ) + + notakeover = False + compress = 0 + if self._compress: + extensions = headers.get(hdrs.SEC_WEBSOCKET_EXTENSIONS) + # Server side always get return with no exception. + # If something happened, just drop compress extension + compress, notakeover = ws_ext_parse(extensions, isserver=True) + if compress: + enabledext = ws_ext_gen( + compress=compress, isserver=True, server_notakeover=notakeover + ) + response_headers[hdrs.SEC_WEBSOCKET_EXTENSIONS] = enabledext + + if protocol: + response_headers[hdrs.SEC_WEBSOCKET_PROTOCOL] = protocol + return ( + response_headers, + protocol, + compress, + notakeover, + ) # type: ignore[return-value] + + def _pre_start(self, request: BaseRequest) -> Tuple[str, WebSocketWriter]: + self._loop = request._loop + + headers, protocol, compress, notakeover = self._handshake(request) + + self.set_status(101) + self.headers.update(headers) + self.force_close() + self._compress = compress + transport = request._protocol.transport + assert transport is not None + writer = WebSocketWriter( + request._protocol, transport, compress=compress, notakeover=notakeover + ) + + return protocol, writer + + def _post_start( + self, request: BaseRequest, protocol: str, writer: WebSocketWriter + ) -> None: + self._ws_protocol = protocol + self._writer = writer + + self._reset_heartbeat() + + loop = self._loop + assert loop is not None + self._reader = FlowControlDataQueue(request._protocol, 2**16, loop=loop) + request.protocol.set_parser( + WebSocketReader(self._reader, self._max_msg_size, compress=self._compress) + ) + # disable HTTP keepalive for WebSocket + request.protocol.keep_alive(False) + + def can_prepare(self, request: BaseRequest) -> WebSocketReady: + if self._writer is not None: + raise RuntimeError("Already started") + try: + _, protocol, _, _ = self._handshake(request) + except HTTPException: + return WebSocketReady(False, None) + else: + return WebSocketReady(True, protocol) + + @property + def closed(self) -> bool: + return self._closed + + @property + def close_code(self) -> Optional[int]: + return self._close_code + + @property + def ws_protocol(self) -> Optional[str]: + return self._ws_protocol + + @property + def compress(self) -> bool: + return self._compress + + def exception(self) -> Optional[BaseException]: + return self._exception + + async def ping(self, message: bytes = b"") -> None: + if self._writer is None: + raise RuntimeError("Call .prepare() first") + await self._writer.ping(message) + + async def pong(self, message: bytes = b"") -> None: + # unsolicited pong + if self._writer is None: + raise RuntimeError("Call .prepare() first") + await self._writer.pong(message) + + async def send_str(self, data: str, compress: Optional[bool] = None) -> None: + if self._writer is None: + raise RuntimeError("Call .prepare() first") + if not isinstance(data, str): + raise TypeError("data argument must be str (%r)" % type(data)) + await self._writer.send(data, binary=False, compress=compress) + + async def send_bytes(self, data: bytes, compress: Optional[bool] = None) -> None: + if self._writer is None: + raise RuntimeError("Call .prepare() first") + if not isinstance(data, (bytes, bytearray, memoryview)): + raise TypeError("data argument must be byte-ish (%r)" % type(data)) + await self._writer.send(data, binary=True, compress=compress) + + async def send_json( + self, + data: Any, + compress: Optional[bool] = None, + *, + dumps: JSONEncoder = json.dumps, + ) -> None: + await self.send_str(dumps(data), compress=compress) + + async def write_eof(self) -> None: # type: ignore[override] + if self._eof_sent: + return + if self._payload_writer is None: + raise RuntimeError("Response has not been started") + + await self.close() + self._eof_sent = True + + async def close(self, *, code: int = WSCloseCode.OK, message: bytes = b"") -> bool: + if self._writer is None: + raise RuntimeError("Call .prepare() first") + + self._cancel_heartbeat() + reader = self._reader + assert reader is not None + + # we need to break `receive()` cycle first, + # `close()` may be called from different task + if self._waiting is not None and not self._closed: + reader.feed_data(WS_CLOSING_MESSAGE, 0) + await self._waiting + + if not self._closed: + self._closed = True + try: + await self._writer.close(code, message) + writer = self._payload_writer + assert writer is not None + await writer.drain() + except (asyncio.CancelledError, asyncio.TimeoutError): + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + raise + except Exception as exc: + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._exception = exc + return True + + if self._closing: + return True + + reader = self._reader + assert reader is not None + try: + async with async_timeout.timeout(self._timeout): + msg = await reader.read() + except asyncio.CancelledError: + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + raise + except Exception as exc: + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._exception = exc + return True + + if msg.type == WSMsgType.CLOSE: + self._close_code = msg.data + return True + + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._exception = asyncio.TimeoutError() + return True + else: + return False + + async def receive(self, timeout: Optional[float] = None) -> WSMessage: + if self._reader is None: + raise RuntimeError("Call .prepare() first") + + loop = self._loop + assert loop is not None + while True: + if self._waiting is not None: + raise RuntimeError("Concurrent call to receive() is not allowed") + + if self._closed: + self._conn_lost += 1 + if self._conn_lost >= THRESHOLD_CONNLOST_ACCESS: + raise RuntimeError("WebSocket connection is closed.") + return WS_CLOSED_MESSAGE + elif self._closing: + return WS_CLOSING_MESSAGE + + try: + self._waiting = loop.create_future() + try: + async with async_timeout.timeout(timeout or self._receive_timeout): + msg = await self._reader.read() + self._reset_heartbeat() + finally: + waiter = self._waiting + set_result(waiter, True) + self._waiting = None + except (asyncio.CancelledError, asyncio.TimeoutError): + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + raise + except EofStream: + self._close_code = WSCloseCode.OK + await self.close() + return WSMessage(WSMsgType.CLOSED, None, None) + except WebSocketError as exc: + self._close_code = exc.code + await self.close(code=exc.code) + return WSMessage(WSMsgType.ERROR, exc, None) + except Exception as exc: + self._exception = exc + self._closing = True + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + await self.close() + return WSMessage(WSMsgType.ERROR, exc, None) + + if msg.type == WSMsgType.CLOSE: + self._closing = True + self._close_code = msg.data + # Could be closed while awaiting reader. + if not self._closed and self._autoclose: # type: ignore[redundant-expr] + await self.close() + elif msg.type == WSMsgType.CLOSING: + self._closing = True + elif msg.type == WSMsgType.PING and self._autoping: + await self.pong(msg.data) + continue + elif msg.type == WSMsgType.PONG and self._autoping: + continue + + return msg + + async def receive_str(self, *, timeout: Optional[float] = None) -> str: + msg = await self.receive(timeout) + if msg.type != WSMsgType.TEXT: + raise TypeError( + "Received message {}:{!r} is not WSMsgType.TEXT".format( + msg.type, msg.data + ) + ) + return cast(str, msg.data) + + async def receive_bytes(self, *, timeout: Optional[float] = None) -> bytes: + msg = await self.receive(timeout) + if msg.type != WSMsgType.BINARY: + raise TypeError(f"Received message {msg.type}:{msg.data!r} is not bytes") + return cast(bytes, msg.data) + + async def receive_json( + self, *, loads: JSONDecoder = json.loads, timeout: Optional[float] = None + ) -> Any: + data = await self.receive_str(timeout=timeout) + return loads(data) + + async def write(self, data: bytes) -> None: + raise RuntimeError("Cannot call .write() for websocket") + + def __aiter__(self) -> "WebSocketResponse": + return self + + async def __anext__(self) -> WSMessage: + msg = await self.receive() + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED): + raise StopAsyncIteration + return msg + + def _cancel(self, exc: BaseException) -> None: + if self._reader is not None: + self._reader.set_exception(exc) diff --git a/aiohttp/worker.py b/aiohttp/worker.py new file mode 100644 index 0000000..c1c45f1 --- /dev/null +++ b/aiohttp/worker.py @@ -0,0 +1,239 @@ +"""Async gunicorn worker for aiohttp.web""" + +import asyncio +import os +import re +import signal +import sys +from types import FrameType +from typing import Any, Awaitable, Callable, Optional, Union # noqa + +from gunicorn.config import AccessLogFormat as GunicornAccessLogFormat +from gunicorn.workers import base + +from aiohttp import web + +from .helpers import set_result +from .web_app import Application +from .web_log import AccessLogger + +try: + import ssl + + SSLContext = ssl.SSLContext +except ImportError: # pragma: no cover + ssl = None # type: ignore[assignment] + SSLContext = object # type: ignore[misc,assignment] + + +__all__ = ("GunicornWebWorker", "GunicornUVLoopWebWorker") + + +class GunicornWebWorker(base.Worker): # type: ignore[misc,no-any-unimported] + DEFAULT_AIOHTTP_LOG_FORMAT = AccessLogger.LOG_FORMAT + DEFAULT_GUNICORN_LOG_FORMAT = GunicornAccessLogFormat.default + + def __init__(self, *args: Any, **kw: Any) -> None: # pragma: no cover + super().__init__(*args, **kw) + + self._task: Optional[asyncio.Task[None]] = None + self.exit_code = 0 + self._notify_waiter: Optional[asyncio.Future[bool]] = None + + def init_process(self) -> None: + # create new event_loop after fork + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + super().init_process() + + def run(self) -> None: + self._task = self.loop.create_task(self._run()) + + try: # ignore all finalization problems + self.loop.run_until_complete(self._task) + except Exception: + self.log.exception("Exception in gunicorn worker") + self.loop.run_until_complete(self.loop.shutdown_asyncgens()) + self.loop.close() + + sys.exit(self.exit_code) + + async def _run(self) -> None: + runner = None + if isinstance(self.wsgi, Application): + app = self.wsgi + elif asyncio.iscoroutinefunction(self.wsgi): + wsgi = await self.wsgi() + if isinstance(wsgi, web.AppRunner): + runner = wsgi + app = runner.app + else: + app = wsgi + else: + raise RuntimeError( + "wsgi app should be either Application or " + "async function returning Application, got {}".format(self.wsgi) + ) + + if runner is None: + access_log = self.log.access_log if self.cfg.accesslog else None + runner = web.AppRunner( + app, + logger=self.log, + keepalive_timeout=self.cfg.keepalive, + access_log=access_log, + access_log_format=self._get_valid_log_format( + self.cfg.access_log_format + ), + ) + await runner.setup() + + ctx = self._create_ssl_context(self.cfg) if self.cfg.is_ssl else None + + assert runner is not None + server = runner.server + assert server is not None + for sock in self.sockets: + site = web.SockSite( + runner, + sock, + ssl_context=ctx, + shutdown_timeout=self.cfg.graceful_timeout / 100 * 95, + ) + await site.start() + + # If our parent changed then we shut down. + pid = os.getpid() + try: + while self.alive: # type: ignore[has-type] + self.notify() + + cnt = server.requests_count + if self.max_requests and cnt > self.max_requests: + self.alive = False + self.log.info("Max requests, shutting down: %s", self) + + elif pid == os.getpid() and self.ppid != os.getppid(): + self.alive = False + self.log.info("Parent changed, shutting down: %s", self) + else: + await self._wait_next_notify() + except BaseException: + pass + + await runner.cleanup() + + def _wait_next_notify(self) -> "asyncio.Future[bool]": + self._notify_waiter_done() + + loop = self.loop + assert loop is not None + self._notify_waiter = waiter = loop.create_future() + self.loop.call_later(1.0, self._notify_waiter_done, waiter) + + return waiter + + def _notify_waiter_done( + self, waiter: Optional["asyncio.Future[bool]"] = None + ) -> None: + if waiter is None: + waiter = self._notify_waiter + if waiter is not None: + set_result(waiter, True) + + if waiter is self._notify_waiter: + self._notify_waiter = None + + def init_signals(self) -> None: + # Set up signals through the event loop API. + + self.loop.add_signal_handler( + signal.SIGQUIT, self.handle_quit, signal.SIGQUIT, None + ) + + self.loop.add_signal_handler( + signal.SIGTERM, self.handle_exit, signal.SIGTERM, None + ) + + self.loop.add_signal_handler( + signal.SIGINT, self.handle_quit, signal.SIGINT, None + ) + + self.loop.add_signal_handler( + signal.SIGWINCH, self.handle_winch, signal.SIGWINCH, None + ) + + self.loop.add_signal_handler( + signal.SIGUSR1, self.handle_usr1, signal.SIGUSR1, None + ) + + self.loop.add_signal_handler( + signal.SIGABRT, self.handle_abort, signal.SIGABRT, None + ) + + # Don't let SIGTERM and SIGUSR1 disturb active requests + # by interrupting system calls + signal.siginterrupt(signal.SIGTERM, False) + signal.siginterrupt(signal.SIGUSR1, False) + # Reset signals so Gunicorn doesn't swallow subprocess return codes + # See: https://github.com/aio-libs/aiohttp/issues/6130 + + def handle_quit(self, sig: int, frame: Optional[FrameType]) -> None: + self.alive = False + + # worker_int callback + self.cfg.worker_int(self) + + # wakeup closing process + self._notify_waiter_done() + + def handle_abort(self, sig: int, frame: Optional[FrameType]) -> None: + self.alive = False + self.exit_code = 1 + self.cfg.worker_abort(self) + sys.exit(1) + + @staticmethod + def _create_ssl_context(cfg: Any) -> "SSLContext": + """Creates SSLContext instance for usage in asyncio.create_server. + + See ssl.SSLSocket.__init__ for more details. + """ + if ssl is None: # pragma: no cover + raise RuntimeError("SSL is not supported.") + + ctx = ssl.SSLContext(cfg.ssl_version) + ctx.load_cert_chain(cfg.certfile, cfg.keyfile) + ctx.verify_mode = cfg.cert_reqs + if cfg.ca_certs: + ctx.load_verify_locations(cfg.ca_certs) + if cfg.ciphers: + ctx.set_ciphers(cfg.ciphers) + return ctx + + def _get_valid_log_format(self, source_format: str) -> str: + if source_format == self.DEFAULT_GUNICORN_LOG_FORMAT: + return self.DEFAULT_AIOHTTP_LOG_FORMAT + elif re.search(r"%\([^\)]+\)", source_format): + raise ValueError( + "Gunicorn's style options in form of `%(name)s` are not " + "supported for the log formatting. Please use aiohttp's " + "format specification to configure access log formatting: " + "http://docs.aiohttp.org/en/stable/logging.html" + "#format-specification" + ) + else: + return source_format + + +class GunicornUVLoopWebWorker(GunicornWebWorker): + def init_process(self) -> None: + import uvloop + + # Setup uvloop policy, so that every + # asyncio.get_event_loop() will create an instance + # of uvloop event loop. + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + + super().init_process() diff --git a/aiosignal/__init__.py b/aiosignal/__init__.py new file mode 100644 index 0000000..3d288e6 --- /dev/null +++ b/aiosignal/__init__.py @@ -0,0 +1,36 @@ +from frozenlist import FrozenList + +__version__ = "1.3.1" + +__all__ = ("Signal",) + + +class Signal(FrozenList): + """Coroutine-based signal implementation. + + To connect a callback to a signal, use any list method. + + Signals are fired using the send() coroutine, which takes named + arguments. + """ + + __slots__ = ("_owner",) + + def __init__(self, owner): + super().__init__() + self._owner = owner + + def __repr__(self): + return "".format( + self._owner, self.frozen, list(self) + ) + + async def send(self, *args, **kwargs): + """ + Sends data to all registered receivers. + """ + if not self.frozen: + raise RuntimeError("Cannot send non-frozen signal.") + + for receiver in self: + await receiver(*args, **kwargs) # type: ignore diff --git a/aiosignal/__init__.pyi b/aiosignal/__init__.pyi new file mode 100644 index 0000000..d4e3416 --- /dev/null +++ b/aiosignal/__init__.pyi @@ -0,0 +1,12 @@ +from typing import Any, Generic, TypeVar + +from frozenlist import FrozenList + +__all__ = ("Signal",) + +_T = TypeVar("_T") + +class Signal(FrozenList[_T], Generic[_T]): + def __init__(self, owner: Any) -> None: ... + def __repr__(self) -> str: ... + async def send(self, *args: Any, **kwargs: Any) -> None: ... diff --git a/aiosignal/py.typed b/aiosignal/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/async_timeout/__init__.py b/async_timeout/__init__.py new file mode 100644 index 0000000..1ffb069 --- /dev/null +++ b/async_timeout/__init__.py @@ -0,0 +1,239 @@ +import asyncio +import enum +import sys +import warnings +from types import TracebackType +from typing import Optional, Type + + +if sys.version_info >= (3, 8): + from typing import final +else: + from typing_extensions import final + + +if sys.version_info >= (3, 11): + + def _uncancel_task(task: "asyncio.Task[object]") -> None: + task.uncancel() + +else: + + def _uncancel_task(task: "asyncio.Task[object]") -> None: + pass + + +__version__ = "4.0.3" + + +__all__ = ("timeout", "timeout_at", "Timeout") + + +def timeout(delay: Optional[float]) -> "Timeout": + """timeout context manager. + + Useful in cases when you want to apply timeout logic around block + of code or in cases when asyncio.wait_for is not suitable. For example: + + >>> async with timeout(0.001): + ... async with aiohttp.get('https://github.com') as r: + ... await r.text() + + + delay - value in seconds or None to disable timeout logic + """ + loop = asyncio.get_running_loop() + if delay is not None: + deadline = loop.time() + delay # type: Optional[float] + else: + deadline = None + return Timeout(deadline, loop) + + +def timeout_at(deadline: Optional[float]) -> "Timeout": + """Schedule the timeout at absolute time. + + deadline argument points on the time in the same clock system + as loop.time(). + + Please note: it is not POSIX time but a time with + undefined starting base, e.g. the time of the system power on. + + >>> async with timeout_at(loop.time() + 10): + ... async with aiohttp.get('https://github.com') as r: + ... await r.text() + + + """ + loop = asyncio.get_running_loop() + return Timeout(deadline, loop) + + +class _State(enum.Enum): + INIT = "INIT" + ENTER = "ENTER" + TIMEOUT = "TIMEOUT" + EXIT = "EXIT" + + +@final +class Timeout: + # Internal class, please don't instantiate it directly + # Use timeout() and timeout_at() public factories instead. + # + # Implementation note: `async with timeout()` is preferred + # over `with timeout()`. + # While technically the Timeout class implementation + # doesn't need to be async at all, + # the `async with` statement explicitly points that + # the context manager should be used from async function context. + # + # This design allows to avoid many silly misusages. + # + # TimeoutError is raised immediately when scheduled + # if the deadline is passed. + # The purpose is to time out as soon as possible + # without waiting for the next await expression. + + __slots__ = ("_deadline", "_loop", "_state", "_timeout_handler", "_task") + + def __init__( + self, deadline: Optional[float], loop: asyncio.AbstractEventLoop + ) -> None: + self._loop = loop + self._state = _State.INIT + + self._task: Optional["asyncio.Task[object]"] = None + self._timeout_handler = None # type: Optional[asyncio.Handle] + if deadline is None: + self._deadline = None # type: Optional[float] + else: + self.update(deadline) + + def __enter__(self) -> "Timeout": + warnings.warn( + "with timeout() is deprecated, use async with timeout() instead", + DeprecationWarning, + stacklevel=2, + ) + self._do_enter() + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Optional[bool]: + self._do_exit(exc_type) + return None + + async def __aenter__(self) -> "Timeout": + self._do_enter() + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Optional[bool]: + self._do_exit(exc_type) + return None + + @property + def expired(self) -> bool: + """Is timeout expired during execution?""" + return self._state == _State.TIMEOUT + + @property + def deadline(self) -> Optional[float]: + return self._deadline + + def reject(self) -> None: + """Reject scheduled timeout if any.""" + # cancel is maybe better name but + # task.cancel() raises CancelledError in asyncio world. + if self._state not in (_State.INIT, _State.ENTER): + raise RuntimeError(f"invalid state {self._state.value}") + self._reject() + + def _reject(self) -> None: + self._task = None + if self._timeout_handler is not None: + self._timeout_handler.cancel() + self._timeout_handler = None + + def shift(self, delay: float) -> None: + """Advance timeout on delay seconds. + + The delay can be negative. + + Raise RuntimeError if shift is called when deadline is not scheduled + """ + deadline = self._deadline + if deadline is None: + raise RuntimeError("cannot shift timeout if deadline is not scheduled") + self.update(deadline + delay) + + def update(self, deadline: float) -> None: + """Set deadline to absolute value. + + deadline argument points on the time in the same clock system + as loop.time(). + + If new deadline is in the past the timeout is raised immediately. + + Please note: it is not POSIX time but a time with + undefined starting base, e.g. the time of the system power on. + """ + if self._state == _State.EXIT: + raise RuntimeError("cannot reschedule after exit from context manager") + if self._state == _State.TIMEOUT: + raise RuntimeError("cannot reschedule expired timeout") + if self._timeout_handler is not None: + self._timeout_handler.cancel() + self._deadline = deadline + if self._state != _State.INIT: + self._reschedule() + + def _reschedule(self) -> None: + assert self._state == _State.ENTER + deadline = self._deadline + if deadline is None: + return + + now = self._loop.time() + if self._timeout_handler is not None: + self._timeout_handler.cancel() + + self._task = asyncio.current_task() + if deadline <= now: + self._timeout_handler = self._loop.call_soon(self._on_timeout) + else: + self._timeout_handler = self._loop.call_at(deadline, self._on_timeout) + + def _do_enter(self) -> None: + if self._state != _State.INIT: + raise RuntimeError(f"invalid state {self._state.value}") + self._state = _State.ENTER + self._reschedule() + + def _do_exit(self, exc_type: Optional[Type[BaseException]]) -> None: + if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT: + assert self._task is not None + _uncancel_task(self._task) + self._timeout_handler = None + self._task = None + raise asyncio.TimeoutError + # timeout has not expired + self._state = _State.EXIT + self._reject() + return None + + def _on_timeout(self) -> None: + assert self._task is not None + self._task.cancel() + self._state = _State.TIMEOUT + # drop the reference early + self._timeout_handler = None diff --git a/async_timeout/py.typed b/async_timeout/py.typed new file mode 100644 index 0000000..3b94f91 --- /dev/null +++ b/async_timeout/py.typed @@ -0,0 +1 @@ +Placeholder diff --git a/autoActions/README.md b/autoActions/README.md new file mode 100644 index 0000000..9e819e0 --- /dev/null +++ b/autoActions/README.md @@ -0,0 +1,272 @@ +# AutoActions + +Automatiken, die im Web-UI unter „Automatismen" angelegt werden und hier +ausgeführt werden: *wenn Bedingung, dann Kommando*. + +``` +Browser Datenbank homeMesh Runner +────────────────────── ───────────────────────── ──────────────────── +Karte „Automatismen" ──▶ automations ──▶ autoaction_runner.py +ajax/AutoAction.php automation_conditions MQTT / HTTP / Tahoma +restricted/automations.php automation_actions ──▶ Geräte +js/solar/autoActionFuncs.js automation_action_params + automation_log + calendar_days ◀── fetch_calendar.py +``` + +## Wie eine Automatik aufgebaut ist + +Eine Automatik hat **Auslöser**, **Rahmenbedingungen** und **Aktionen**. + +Ein Auslöser vergleicht einen Messwert (`actor_states`) mit einer Schwelle. +Mehrere Auslöser werden über `group_no` verknüpft: gleiche Nummer heißt UND, +verschiedene Nummern heißen ODER — ausgewertet wird `any(all(gruppe))`. Im +Editor ist eine Gruppe ein gerahmter Block mit eigenem „+ Bedingung", zwischen +den Blöcken steht ein ODER. Die Klammerung ist damit gezeichnet und nicht bloß +vereinbart, und darunter steht derselbe Ausdruck noch einmal als Satz. + +Eine Aktion ist ein Kommando (`actor_commands`) mit einem Wert je Parameter +(`command_parameters`) — nicht vier feste Spalten „Wert 1" bis „Wert 4", +sondern so viele Zeilen, wie das Gerät Parameter hat. + +Die Rahmenbedingungen (Wochentage, Zeitfenster, Ferien, Feiertage) sagen, wann +die Automatik überhaupt hinsehen darf. + +## Warum ein Dauerläufer und kein Cronjob + +Zwei Gründe: + +* Schwellwert-Auslöser sollen greifen, wenn die MQTT-Nachricht hereinkommt, + nicht erst im nächsten Minutenraster. +* `actor_states.current_value` wird sonst von niemandem fortgeschrieben — es + wird beim Geräte-Discovery einmal gesetzt und danach nie wieder. Ein + zustandsloser Cronjob hätte gar nichts, womit er vergleichen könnte. Der + Runner pflegt den Wert nebenbei mit — nur wenn er sich geändert hat und + höchstens einmal je Minute —, wovon auch der Editor profitiert: er zeigt + neben jedem Messwert den aktuellen Stand. + +## Was worauf wartet + +Die Hauptschleife wartet auf nichts. Alles, was ein Netz braucht, läuft +daneben: + +| | wo | Takt | +|---|---|---| +| MQTT-Nachrichten | kommen von selbst, Zwischenspeicher im Transport | sofort | +| Uhrzeit, Datum | im Runner gerechnet | jeder Takt | +| Sonnenauf-/-untergang | `solarLog.daylight` | einmal je Tag | +| HTTP- und WLED-Geräte | `Sammler`, eigener Faden | `poll_http`, `poll_wled` | +| Tahoma | `Sammler`, eigener Faden | `poll_tahoma`, Vorgabe 5 Minuten | +| Kommandos senden | `Versand`, ein Faden je Gerät | wenn etwas ansteht | + +Das war nicht immer so. Vorher wurden die Geräte mitten in der Schleife +abgefragt — neunzehn Tahoma-Geräte nacheinander, jedes mit bis zu zehn +Sekunden Zeitlimit. Eine Runde dauerte dadurch rund fünfundvierzig statt +dreißig Sekunden, gepollt wurde erst jede zweite Runde, also alle neunzig +Sekunden. Und weil die Uhr an derselben Abfrage hing, kam jede dritte Minute +nie vor: `um 18:26` wurde nie wahr. + +Die Fäden fassen die Datenbank nicht an. Sie legen ihre Ergebnisse in eine +Queue, geschrieben wird im Hauptfaden — eine pymysql-Verbindung gehört einem +Faden. + +## Nur die steigende Flanke + +`automations.cond_met` hält fest, ob die Bedingung beim letzten Durchlauf schon +erfüllt war. Ohne das würde „Temperatur über 22 Grad" bei jedem Takt erneut +feuern. Verlässt die Automatik ihr Zeitfenster, wird die Flanke +zurückgesetzt, damit sie im nächsten Fenster wieder steigen kann. + +Zeit-Auslöser gibt es in drei Formen: + +| | wahr, wenn | +|---|---| +| `um 16:30` | ab dieser Minute, noch `catchup_minutes` lang | +| `ab 16:30` | von da an bis Mitternacht | +| `vor 16:30` | bis dahin | + +Ausgelöst wird in allen drei Fällen nur einmal, eben wegen der Flanke. + +Das Nachholfenster bei `um` ist der Ersatz für die früher verlangte +Punktgenauigkeit. Solange `um 16:30` nur in genau dieser Minute wahr war, +kostete jeder Aussetzer die Automatik für den ganzen Tag — und Aussetzer gab +es reichlich, weil die Uhr am Geräte-Poll hing und jede dritte Minute +übersprang. Jetzt gilt die Bedingung fünf Minuten lang (einstellbar), die +Flanke sorgt weiterhin für genau einen Lauf, und ein Neustart mitten im +Fenster holt den Lauf nach. Die breiten Zeitfenster in `auto_watering.py` +folgen derselben Überlegung. + +Beim Sonnenauf- und -untergang ist der Wert ein Versatz, und der kann davor +oder danach liegen — deshalb dieselben drei Fälle mal zwei: `+ 00:30` eine +halbe Stunde nach Sonnenaufgang, `ab - 00:30` ab einer halben Stunde davor, +`vor + 00:30` bis eine halbe Stunde danach. + +Über den Tagesrand wird gerechnet, nicht abgeschnitten: „sechs Stunden vor +Sonnenaufgang" landet am Vorabend, und das ist so gewollt — abgeschnitten +wären solche Angaben gar nicht mehr formulierbar. Verglichen wird die Uhrzeit +innerhalb des Tages; ein Ziel jenseits von Mitternacht gilt als diese Uhrzeit +am selben Tag. Bei `+` und `-` ist das genau der gemeinte Zeitpunkt, bei `ab` +und `vor` verschiebt sich der wahre Bereich entsprechend mit. + +`force_once` („am Ende des Zeitraums auf jeden Fall ausführen") greift, wenn +das Fenster zugeht und in diesem Fenster noch nichts passiert ist. + +## Sperrzeit + +Die Flanke allein schützt nicht gegen einen Messwert, der um die Schwelle +**pendelt**: „Temperatur > 22" bei 22,1 / 21,9 / 22,1 °C ist jedes Mal eine +echte steigende Flanke, und über MQTT können die Werte im Sekundentakt +hereinkommen. `automations.lockout_secs` sagt, wie lange nach einer Auslösung +nicht wieder geschaltet wird. Der Editor bietet drei Stufen an: + +| | | gedacht für | +|---|---|---| +| Ohne | 0 s | volle Geschwindigkeit, jede Flanke schaltet | +| Kurz | 60 s | Licht, Farbe, Dimmwert | +| Lang | 900 s | Rollläden, Ventile, alles mit Motor | + +Gespeichert werden Sekunden, angeboten werden nur die drei Stufen — eine +vierte ist damit eine Zeile in `lockoutChoices()` und keine Wanderung durch +die Datenbank. + +Eine Flanke innerhalb der Sperrzeit wird **verworfen, nicht aufgehoben**. Ein +Rollladen, der eine Viertelstunde später doch noch losfährt, weil vor langer +Zeit einmal eine Schwelle gestreift wurde, wäre unangenehmer als einer, der +gar nicht fährt — und der nächste echte Anlass nach Ablauf der Sperre kommt +ohnehin durch. Verworfene Flanken stehen im Log auf `DEBUG`, nicht in +`automation_log`; bei einem zappelnden Sensor wäre die Tabelle sonst voll +davon. + +`force_once` ist von der Sperre nicht betroffen: es greift nur, wenn im +Fenster gar nichts gelaufen ist — dann ist auch keine Sperre aktiv. + +## Transporte + +Welcher Weg zum Gerät führt, entscheidet die URL des Aktors in `actors`: + +| URL | Messwert (`actor_states.url`) | Kommando (`actor_commands.command_url`) | +|---|---|---| +| `mqtt://…` | vollständiges Topic, abonniert; bei mehreren Messwerten je Topic zusätzlich `value_path` | Nutzlast auf das Parameter-Topic | +| `http://…` | Feldname in der JSON-Antwort, gepollt | Abfrageargumente an die Geräte-URL (`turn=on`) | +| `wled://…` | Pfad in `/json/state` (`seg[0].col[0]`), gepollt | JSON-Vorlage mit Platzhaltern, als Ganzes gesendet | +| Tahoma | Statusname (`core:ClosureState`), gepollt | `exec/apply` an die Box | +| `Logic` | gerechnet: Uhrzeit, Datum, Sonne | – | + +Alle fünf stehen in `transports.py`. Eine sechste Geräteart kommt als weitere +Klasse dazu; sie braucht `passt()`, `zustaende_lesen()` und `senden()`. + +Tahoma ist der einzige, der nicht am URL-Schema erkannt wird, sondern an der +**Box-Kennung** in der URL. Das Schema beschreibt dort die Funkart, und +dieselbe Box liefert `io://` für die Jalousien, `rts://` für die Dachfenster +und `internal://` für die Alarmanlage. Ohne `pin` in der `config.ini` ist +niemand zuständig — dann meldet der Runner beim Auslösen „kein Transport", +statt still nichts zu tun. + +Mehrere Messwerte teilen sich oft **ein Topic**: der go-eCharger schickt +sechzehn Zahlen als JSON-Feld auf `…/nrg`, und erst das `value_template` der +Home-Assistant-Discovery sagt, dass „Strom L1" das fünfte Element ist. Diese +Angabe steht in `actor_states.value_path` — in derselben Schreibweise, die +auch WLED benutzt: `[4]`, `ssid`, `seg[0].col[0]`. Ohne Pfad gilt die ganze +Nutzlast. + +Gelesen wird nur der einfache Fall aus dem Template: ein Zugriff auf +`value_json` und was danach an Punkten und Klammern folgt. + +### Werttabellen + +Manche Geräte schicken eine Zahl und meinen einen Zustand: + +``` +{{ ['Unknown','Idle','Charging','WaitCar','Complete','Error'][value_json|int] }} +{{ ['Default','Eco','NextTrip'][value_json|int-3] }} +``` + +Das ist kein Pfad, sondern eine Übersetzung von Zahl nach Text. Sie landet in +`possible_values` — in der Schreibweise, die WLED für seine Effektliste schon +benutzt: eine Liste aus `{Wert: Bezeichnung}`. Ein Versatz im Ausdruck wandert +dabei in die Schlüssel, aus `[value_json|int-3]` wird also `{"3":"Default"}`. + +Der Runner übersetzt beim Lesen: aus der gesendeten `2` wird `Charging`. Eine +Bedingung vergleicht damit genau den Klartext, den der Editor zur Auswahl +stellt. Steht die Zahl nicht in der Tabelle, bleibt sie stehen — ein +erfundener Name wäre schlimmer als ein roher Wert. + +Auf beiden Seiten des Editors steckt dieselbe Tabelle, aber der gespeicherte +Wert ist ein anderer: + +| | angezeigt | gespeichert | +|---|---|---| +| **Messwert** (Bedingung) | `Charging` | `Charging` — der Runner hat schon übersetzt | +| **Parameter** (Aktion) | `Blink` | `1` — das Gerät will die Zahl | + +Bei WLED trägt die Kommando-Vorlage alles: `{"seg":[{"col":[[%red%,%green%,%blue%]]}]}` +wird mit den Parameterwerten gefüllt und am Stück geschickt. Deshalb haben die +Parameter dort keine eigene URL — ihr Name *ist* der Platzhalter. + +## Voraussetzungen + +* Python 3 mit `pymysql`, `requests`, `paho-mqtt` +* `homeMesh_automations.sql` einmal eingespielt +* Im Web-Verzeichnis muss in `restricted/deviceDiscovery/config.ini` + **`clear_tables = false`** stehen. + Discovery schreibt mit `ON DUPLICATE KEY UPDATE` auf den URLs, das Leeren ist + unnötig — ein `TRUNCATE` würde dagegen die Geräte-IDs neu vergeben, und die + Automatiken zeigen per Fremdschlüssel genau auf diese IDs. + +## Einrichten + +```bash +cp config.ini.example config.ini # ausfüllen: Datenbank, MQTT, Tahoma +python3 fetch_calendar.py # Feiertage und Ferien holen +python3 autoaction_runner.py --once --dry-run --verbose # Probelauf +``` + +`--dry-run` schaltet nichts, protokolliert aber jedes Kommando, das geschickt +würde. `--once` macht einen einzigen Durchlauf. + +## Wo das läuft + +Der Runner ist ein Hintergrundprozess und wohnt deshalb beim SolarManager, +nicht im Web-Verzeichnis: + +``` +/volume1/homes/wagner/SolarManager/ +├── solarManager.py +├── startSolarServer.sh startet beide, siehe unten +└── autoActions/ + ├── autoaction_runner.py + ├── transports.py + ├── fetch_calendar.py + └── config.ini Zugangsdaten, nicht im Git +``` + +Das Web-UI kennt diesen Pfad nicht — Browser und Runner reden ausschließlich +über die Datenbank `homeMesh` miteinander. Der Runner lädt das Regelwerk nach, +sobald im Browser etwas gespeichert wurde; ein Neustart nach jeder Änderung ist +nicht nötig. + +`startSolarServer.sh` startet `solarManager.py` und den Runner gemeinsam und +beendet vorher, was schon läuft. Aufgerufen wird es beim Booten (auf der +Synology über den Aufgabenplaner, Ereignis „Hochfahren", als root); dasselbe +Skript von Hand aufzurufen ist der normale Weg, den Runner neu zu starten. +Zwei Instanzen dürfen nie gleichzeitig laufen — sie würden jedes Kommando +doppelt schicken und sich gegenseitig vom MQTT-Broker werfen, weil beide +dieselbe Client-Kennung benutzen. Genau davor schützt das Beenden am Anfang. + +Ausgabe landet in `autoActions.log` neben `solarOutput.log`. `SIGTERM` fängt +der Runner ab und fährt geordnet herunter. + +`fetch_calendar.py` gehört einmal jährlich in den Cron: + +``` +0 4 1 1 * /usr/bin/python3 /volume1/homes/wagner/SolarManager/autoActions/fetch_calendar.py +``` + +Ein zusätzlicher Lauf im Herbst schadet nicht — die Ferientermine des +übernächsten Schuljahres stehen erst später fest. + +## Nachsehen, was passiert ist + +`automation_log` hält je Auslösung fest, ob sie durchlief (`fired`), wegen +`force_once` nachgeholt wurde (`forced`) oder scheiterte (`error`, mit Grund in +`detail`). Einträge älter als 30 Tage räumt der Runner selbst weg. diff --git a/autoActions/autoaction_runner.py b/autoActions/autoaction_runner.py new file mode 100644 index 0000000..3533884 --- /dev/null +++ b/autoActions/autoaction_runner.py @@ -0,0 +1,1022 @@ +#!/usr/bin/env python3 +""" +AutoAction-Runner - fuehrt die im Web-UI angelegten Automatiken aus. + +Laeuft als Dauerprozess, nicht als Cronjob. Zwei Gruende: + + * Schwellwert-Ausloeser ("Temperatur ueber 22 Grad") sollen sofort greifen, + wenn die Nachricht hereinkommt, und nicht bis zum naechsten Minutenraster + warten. + * `actor_states.current_value` wird sonst von niemandem fortgeschrieben - + beim Geraete-Discovery einmal gesetzt und danach nie wieder. Ein + zustandsloser Cronjob haette also gar nichts, womit er vergleichen + koennte. Der Runner pflegt den Wert nebenbei mit, wodurch auch der Editor + im Browser aktuelle Zahlen anzeigt. + +Ablauf: + + Start Regelwerk und Geraetemodell laden, MQTT-Topics abonnieren, + Sammlerfaeden fuer die gepollten Geraete starten + Ereignis MQTT-Nachricht -> Wert merken -> im naechsten Takt auswerten + Hintergrund je Transport ein Faden: HTTP und WLED im Minutentakt, Tahoma + alle fuenf Minuten. Die Werte landen in einer Queue. + Takt alle tick_seconds: Queue leeren, auswerten, Regelwerk auf + Aenderung pruefen. Nichts davon wartet auf ein Netz. + Pruefen aktiv? (enabled, Wochentag, Ferien/Feiertag, Zeitfenster) + -> Bedingungen auswerten -> steigende Flanke -> Aktionen + +Die Uhr gehoert ausdruecklich nicht zu den abgefragten Geraeten. Sie stand +frueher mit im Geraete-Poll, und weil neunzehn Tahoma-Geraete nacheinander +laenger als eine Minute brauchten, kam jede dritte Minute nie vor: ein +Ausloeser "um 18:26" wurde nie wahr. Verglichen wird jetzt direkt gegen +datetime.now() - siehe uhrzeit_erfuellt(). + +Nur die steigende Flanke loest aus: `automations.cond_met` haelt fest, ob die +Bedingung beim letzten Durchlauf schon erfuellt war. Ohne das wuerde +"Temperatur ueber 22 Grad" bei jedem Takt erneut feuern. + +Zeit-Ausloeser gibt es in drei Formen: "um 16:30" gilt ab dieser Minute noch +catchup_minutes lang, "ab 16:30" von da an bis Mitternacht, "vor 16:30" bis +dahin. Ausgeloest wird in allen drei Faellen nur einmal, eben wegen der +Flanke. Das Nachholfenster bei "um" ist der Ersatz fuer die frueher +verlangte Punktgenauigkeit: ein Neustart, ein haengendes Geraet oder ein +langsamer Durchlauf kosten die Automatik nicht mehr den ganzen Tag, und weil +nur die Flanke zaehlt, laeuft sie trotzdem hoechstens einmal. Dieselbe +Ueberlegung steht hinter den breiten Zeitfenstern in auto_watering.py. + +Beim Sonnenauf- und -untergang traegt der Operator zusaetzlich das +Vorzeichen des Versatzes: "+ 00:30" eine halbe Stunde danach, ">=- 00:30" +ab einer halben Stunde davor, "<+ 00:30" bis eine halbe Stunde danach. + +Tabellen siehe homeMesh_automations.sql, Konfiguration siehe config.ini.example. +""" + +import argparse +import configparser +import json +import logging +import os +import queue +import signal +import sys +import threading +import time +from datetime import date, datetime, timedelta + +import pymysql +import requests +import paho.mqtt.client as mqtt + +from transports import (HTTPTransport, LogicTransport, MQTTTransport, + TahomaTransport, WLEDTransport) + +logger = logging.getLogger("autoaction") + +# Wie lange ein Messwert in der Datenbank stehen bleiben darf, bevor er +# aufgefrischt wird. Der Wert dient nur der Anzeige im Editor; jede Nachricht +# sofort zu schreiben waere bei einem gespraechigen Sensor sinnlose Last. +SCHREIB_ABSTAND = timedelta(seconds=60) + +# Aelteres im Protokoll interessiert niemanden mehr. +LOG_AUFBEWAHRUNG_TAGE = 30 + + +# =========================================================================== +# Konfiguration +# =========================================================================== + +class Config: + def __init__(self, dateiname="config.ini"): + pfad = os.path.join(os.path.dirname(os.path.abspath(__file__)), dateiname) + if not os.path.exists(pfad): + raise FileNotFoundError( + "%s fehlt - config.ini.example kopieren und ausfuellen." % pfad) + self.cfg = configparser.ConfigParser() + self.cfg.read(pfad, encoding="utf-8") + + def text(self, sektion, schluessel, vorgabe=""): + return self.cfg.get(sektion, schluessel, fallback=vorgabe).strip() + + def zahl(self, sektion, schluessel, vorgabe=0): + try: + return self.cfg.getint(sektion, schluessel, fallback=vorgabe) + except ValueError: + return vorgabe + + def ja(self, sektion, schluessel, vorgabe=False): + return self.text(sektion, schluessel, str(vorgabe)).lower() in ("true", "1", "yes", "on") + + +# =========================================================================== +# Datenbank +# =========================================================================== + +def verbinden(config, sektion="database"): + return pymysql.connect( + host=config.text("database", "host", "localhost"), + port=config.zahl("database", "port", 3306), + user=config.text(sektion, "user") or config.text("database", "user"), + password=config.text(sektion, "password") or config.text("database", "password"), + database=config.text(sektion, "database"), + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + autocommit=True) + + +class Regelwerk: + """ + Das geladene Abbild der Datenbank: Automatiken mit ihren Bedingungen und + Aktionen, dazu die Messwerte und Kommandos, die sie benutzen. + """ + + def __init__(self, automatiken, states, kommandos, signatur): + self.automatiken = automatiken + self.states = states # state_id -> Beschreibung + self.kommandos = kommandos # command_id -> Beschreibung + self.signatur = signatur + + @staticmethod + def signatur_lesen(db): + """ + Woran der Runner merkt, dass er neu laden muss. `changed` allein + genuegt nicht: eine geloeschte Automatik veraendert den groessten + Zeitstempel nicht. Deshalb zaehlen die Zeilen mit - auch die der + Geraetetabellen, damit ein Discovery-Lauf ebenfalls durchschlaegt. + """ + with db.cursor() as c: + c.execute("""SELECT (SELECT COUNT(*) FROM automations) AS a, + (SELECT UNIX_TIMESTAMP(MAX(changed)) FROM automations) AS t, + (SELECT COUNT(*) FROM automation_conditions) AS b, + (SELECT COUNT(*) FROM automation_actions) AS c, + (SELECT COUNT(*) FROM actor_states) AS d, + (SELECT COUNT(*) FROM actor_commands) AS e""") + return tuple(sorted(c.fetchone().items())) + + @classmethod + def laden(cls, db): + signatur = cls.signatur_lesen(db) + + states = {} + with db.cursor() as c: + c.execute("""SELECT s.id, s.state_name, s.url AS state_url, s.value_path, + s.current_value, s.possible_values, + a.url AS actor_url, a.name AS actor_name, t.type + FROM actor_states s + JOIN actors a ON a.id = s.actor_id + LEFT JOIN state_types t ON s.state_type = t.id""") + for row in c.fetchall(): + row["type"] = row["type"] or "string" + # Werttabelle als flaches {gesendeter Wert: Bezeichnung}. In + # der Datenbank steht sie als Liste aus Ein-Schluessel- + # Objekten, weil der Editor sie so schon versteht. + row["wertetabelle"] = {} + try: + for eintrag in json.loads(row["possible_values"] or "[]"): + if isinstance(eintrag, dict): + for wert, name in eintrag.items(): + row["wertetabelle"][str(wert)] = name + except ValueError: + pass + states[row["id"]] = row + + kommandos = {} + with db.cursor() as c: + c.execute("""SELECT k.id, k.command_name, k.command_url, + a.url AS actor_url, a.name AS actor_name + FROM actor_commands k JOIN actors a ON a.id = k.actor_id""") + for row in c.fetchall(): + row["params"] = [] + kommandos[row["id"]] = row + with db.cursor() as c: + c.execute("""SELECT id, command_id, parameter_name, url FROM command_parameters + ORDER BY command_id, id""") + for row in c.fetchall(): + if row["command_id"] in kommandos: + kommandos[row["command_id"]]["params"].append(row) + + automatiken = {} + with db.cursor() as c: + c.execute("SELECT * FROM automations WHERE enabled = 1") + for row in c.fetchall(): + row["gruppen"] = {} + row["aktionen"] = [] + automatiken[row["id"]] = row + with db.cursor() as c: + c.execute("""SELECT * FROM automation_conditions + ORDER BY automation_id, group_no, position, id""") + for row in c.fetchall(): + auto = automatiken.get(row["automation_id"]) + if auto is None: + continue + if row["state_id"] not in states: + logger.warning("Automatik %s: Messwert %s gibt es nicht mehr", + auto["name"], row["state_id"]) + continue + auto["gruppen"].setdefault(row["group_no"], []).append(row) + with db.cursor() as c: + c.execute("""SELECT a.id, a.automation_id, a.command_id, p.parameter_id, p.value + FROM automation_actions a + LEFT JOIN automation_action_params p ON p.action_id = a.id + ORDER BY a.automation_id, a.position, a.id""") + gesammelt = {} + for row in c.fetchall(): + auto = automatiken.get(row["automation_id"]) + if auto is None: + continue + aktion = gesammelt.get(row["id"]) + if aktion is None: + aktion = {"command_id": row["command_id"], "werte": {}} + gesammelt[row["id"]] = aktion + auto["aktionen"].append(aktion) + if row["parameter_id"] is not None: + aktion["werte"][row["parameter_id"]] = row["value"] + + logger.info("Regelwerk geladen: %d Automatiken, %d Messwerte, %d Kommandos", + len(automatiken), len(states), len(kommandos)) + return cls(automatiken, states, kommandos, signatur) + + +# =========================================================================== +# Auswertung +# =========================================================================== + +def minuten(text): + """"16:30" oder "16:30:00" als Minuten seit Mitternacht.""" + teile = str(text).strip().split(":") + return int(teile[0]) * 60 + int(teile[1]) + + +def als_datum(text): + for form in ("%d.%m.%Y", "%Y-%m-%d", "%d.%m.%y"): + try: + return datetime.strptime(str(text).strip(), form).date() + except ValueError: + continue + return None + + +def als_zeitpunkt(text): + """Datum mit Uhrzeit. Das Web-Feld liefert "2026-08-30T16:30".""" + roh = str(text).strip().replace("T", " ") + for form in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", + "%d.%m.%Y %H:%M:%S", "%d.%m.%Y %H:%M"): + try: + return datetime.strptime(roh, form) + except ValueError: + continue + tag = als_datum(roh.split(" ")[0]) + return datetime.combine(tag, datetime.min.time()) if tag else None + + +WAHR = {"true", "1", "on", "ja", "yes", "an"} + +# Wie lange ein punktgenauer Ausloeser ("um 16:30", "Sonnenaufgang + 00:30") +# nachtraeglich noch gilt, in Minuten. Ohne dieses Fenster muesste die +# Auswertung genau in dieser einen Minute stattfinden; ein langsamer +# Durchlauf, ein Neustart oder ein haengendes Geraet haetten die Automatik +# fuer den Tag gekostet. Ausgeloest wird trotzdem nur einmal, weil nur die +# steigende Flanke zaehlt (automations.cond_met). +NACHHOLFENSTER = 5 + + +def im_nachholfenster(jetzt_m, ziel_m, fenster): + """ + Liegt die Zielminute hoechstens `fenster` Minuten zurueck? + + Gerechnet wird modulo 24 Stunden, damit ein Ziel kurz vor Mitternacht auch + nach Mitternacht noch zieht: 23:58 ist um 00:01 drei Minuten her. + """ + return (jetzt_m - ziel_m) % 1440 < max(1, fenster) + + +def uhrzeit_erfuellt(typ, op, soll, jetzt, fenster): + """ + Uhrzeit und Datum gegen die echte Uhr, nicht gegen einen Abtastwert. + + Frueher stand hier der zuletzt abgetastete Wert des Messwerts "Uhrzeit", + den der Runner im Geraete-Poll mitfuehrte. Der Poll brauchte aber laenger + als eine Minute, sodass jede dritte Minute nie vorkam - und "um 18:26" + schlicht nie wahr wurde. `jetzt` liegt hier ohnehin vor. + """ + if typ == "date": + soll_d = als_datum(soll) + if soll_d is None: + return False + ist_d = jetzt.date() + if op == "=": return ist_d == soll_d + if op == "<": return ist_d < soll_d + return ist_d >= soll_d + + jetzt_m, soll_m = jetzt.hour * 60 + jetzt.minute, minuten(soll) + if op == "=": return im_nachholfenster(jetzt_m, soll_m, fenster) + if op == "<": return jetzt_m < soll_m + return jetzt_m >= soll_m + + +def bedingung_erfuellt(bedingung, state, wert, jetzt, fenster=NACHHOLFENSTER): + """ + Ein einzelner Vergleich. `wert` ist der aktuelle Messwert als Text, so wie + er vom Geraet kam; `bedingung["value"]` die eingestellte Schwelle. + Unbekannter Wert heisst nicht erfuellt - lieber nicht schalten als auf + Verdacht schalten. + + Uhrzeit und Datum sind die Ausnahme: die werden nicht gemessen, sondern + abgelesen. Sie kommen deshalb direkt aus `jetzt` und nicht aus `wert` - + siehe uhrzeit_erfuellt(). Fuer die Anzeige im Editor fuehrt der Runner sie + zwar auch als Messwert mit, aber ein Vergleich darf nicht davon abhaengen, + wie frisch dieser Abtastwert gerade ist. + + `fenster` ist die Nachholzeit in Minuten fuer punktgenaue Ausloeser. + """ + typ = state["type"] + op = bedingung["operator"] + soll = bedingung["value"] + + if state.get("actor_url") == "Logic" and typ in ("time", "date"): + return uhrzeit_erfuellt(typ, op, soll, jetzt, fenster) + if wert is None or wert == "": + return False + + try: + if typ in ("integer", "float"): + ist_z, soll_z = float(str(wert).replace(",", ".")), float(str(soll).replace(",", ".")) + if op == "=": return ist_z == soll_z + if op == "!=": return ist_z != soll_z + if op == ">": return ist_z > soll_z + if op == "<": return ist_z < soll_z + if op == ">=": return ist_z >= soll_z + if op == "<=": return ist_z <= soll_z + return False + + if typ == "time": + # Zeit-Messwerte, die tatsaechlich von einem Geraet kommen. Die + # Uhr selbst laeuft ueber uhrzeit_erfuellt(). + ist_m, soll_m = minuten(wert), minuten(soll) + if op == "=": return ist_m == soll_m + if op == "<": return ist_m < soll_m + return ist_m >= soll_m + + if typ == "deltatime": + # Der Messwert ist der Sonnenauf- bzw. -untergang, die Schwelle + # ein Versatz. Das Vorzeichen steckt im Operator, der Vergleich + # davor: ">=-" heisst "ab einer halben Stunde davor". + versatz = minuten(soll) + ziel = minuten(wert) + (-versatz if op.endswith("-") else versatz) + # Ueber den Tagesrand wird gerechnet, nicht abgeschnitten: "sechs + # Stunden vor Sonnenaufgang" ist eine gewollte Angabe und landet + # dann eben am Vorabend. Abgeschnitten waeren solche Faelle gar + # nicht mehr formulierbar. + # + # Verglichen wird die Uhrzeit innerhalb des Tages. Ein Ziel + # jenseits von Mitternacht gilt also als diese Uhrzeit am selben + # Tag - bei "um" ist das genau der gemeinte Zeitpunkt, bei "ab" + # und "vor" verschiebt sich der wahre Bereich entsprechend. + ziel %= 1440 + jetzt_m = jetzt.hour * 60 + jetzt.minute + if op.startswith(">="): return jetzt_m >= ziel + if op.startswith("<"): return jetzt_m < ziel + return im_nachholfenster(jetzt_m, ziel, fenster) + + if typ in ("date", "datetime"): + wandeln = als_datum if typ == "date" else als_zeitpunkt + ist_d, soll_d = wandeln(wert), wandeln(soll) + if ist_d is None or soll_d is None: + return False + if op == "=": return ist_d == soll_d + if op == "<": return ist_d < soll_d + return ist_d >= soll_d + + if typ == "bool": + ist_b = str(wert).strip().lower() in WAHR + soll_b = str(soll).strip().lower() in WAHR + return ist_b == soll_b if op == "=" else ist_b != soll_b + + # string und alles Uebrige + if op == "=": + return str(wert).strip() == str(soll).strip() + return str(wert).strip() != str(soll).strip() + + except (ValueError, IndexError) as fehler: + logger.debug("Vergleich %s %s %s nicht moeglich: %s", wert, op, soll, fehler) + return False + + +def gruppen_erfuellt(automatik, regelwerk, werte, jetzt, fenster=NACHHOLFENSTER): + """ + Gleiche group_no = UND, verschiedene = ODER. Eine Automatik ohne + Bedingungen loest nie aus - sonst wuerde sie nach einem Discovery-Lauf, + der ihren Messwert entfernt hat, ploetzlich dauernd feuern. + """ + if not automatik["gruppen"]: + return False + for bedingungen in automatik["gruppen"].values(): + if all(bedingung_erfuellt(b, regelwerk.states[b["state_id"]], + werte.get(b["state_id"]), jetzt, fenster) + for b in bedingungen): + return True + return False + + +def im_zeitfenster(jetzt, von, bis): + """von > bis heisst: das Fenster reicht ueber Mitternacht.""" + m = jetzt.hour * 60 + jetzt.minute + a, b = minuten(von), minuten(bis) + return a <= m <= b if a <= b else (m >= a or m <= b) + + +def tag_passt(automatik, jetzt, kalender): + if not automatik["weekdays"] & (1 << jetzt.weekday()): + return False + if kalender["feiertag"] and not automatik["on_holiday"]: + return False + if kalender["ferien"] and not automatik["on_vacation"]: + return False + return True + + +# =========================================================================== +# Versand +# =========================================================================== + +class Versand: + """ + Schickt Kommandos im Hintergrund - je Geraet der Reihe nach, ueber + Geraete hinweg nebeneinander. + + Ohne das blockiert ein einziges Kommando die ganze Auswertung: eine + Jalousie zuzufahren dauert ueber eine Minute, weil zwischen den beiden + Schwenkbefehlen auf das Ende der Fahrt gewartet werden muss (siehe + TahomaTransport). So lange kaeme kein Zeit-Ausloeser mehr durch, kein + Messwert wuerde zurueckgeschrieben, und mehrere Rollladen in einer + Automatik wuerden sich aufaddieren. + + Je Geraet eine Warteschlange mit einem eigenen Faden: zwei Kommandos an + dieselbe Jalousie duerfen sich nicht ueberholen - "Neigung 0" und + "Neigung 100" sind sonst wirkungslos oder vertauscht -, zwei Kommandos an + verschiedene Jalousien duerfen ruhig gleichzeitig laufen. + + Die Datenbank bleibt aussen vor: die Faeden melden ihr Ergebnis nur + zurueck, geschrieben wird im Hauptfaden. Eine pymysql-Verbindung ist + nicht fuer mehrere Faeden gedacht. + """ + + def __init__(self): + self.warteschlangen = {} # actor_url -> Queue + self.ergebnisse = queue.Queue() + self.laeuft = True + + def einreihen(self, automation_id, beschreibung, transport, auftrag): + schlange = self.warteschlangen.get(auftrag["actor_url"]) + if schlange is None: + schlange = queue.Queue() + self.warteschlangen[auftrag["actor_url"]] = schlange + faden = threading.Thread(target=self._arbeiten, args=(schlange,), + name="versand", daemon=True) + faden.start() + schlange.put((automation_id, beschreibung, transport, auftrag)) + + def _arbeiten(self, schlange): + while self.laeuft: + posten = schlange.get() + if posten is None: + return + automation_id, beschreibung, transport, auftrag = posten + try: + transport.senden(auftrag) + self.ergebnisse.put((automation_id, beschreibung, None)) + except Exception as fehler: + self.ergebnisse.put((automation_id, beschreibung, str(fehler))) + finally: + schlange.task_done() + + def abholen(self): + """Alles, was seit dem letzten Mal fertig geworden ist.""" + fertig = [] + while True: + try: + fertig.append(self.ergebnisse.get_nowait()) + except queue.Empty: + return fertig + + def offen(self): + return sum(s.unfinished_tasks for s in self.warteschlangen.values()) + + def beenden(self): + self.laeuft = False + for schlange in self.warteschlangen.values(): + schlange.put(None) + + +class Sammler: + """ + Fragt die Geraete ab, die sich nicht von selbst melden - im Hintergrund. + + Frueher geschah das mitten in der Hauptschleife: neunzehn Tahoma-Geraete + nacheinander, jedes mit bis zu zehn Sekunden Zeitlimit, dazu die HTTP- und + WLED-Geraete. Eine Runde dauerte dadurch rund fuenfundvierzig statt + dreissig Sekunden. Gepollt wurde, sobald seit dem letzten Poll sechzig + Sekunden vergangen waren - also erst jede zweite Runde, in Wahrheit alle + neunzig Sekunden. Weil die Uhr an derselben Abfrage hing, uebersprang der + Runner jede dritte Minute, und ein Ausloeser "um 18:26" wurde nie wahr. + + Jetzt hat jeder Transport seinen eigenen Faden und seinen eigenen Abstand: + die Rollaeden duerfen gemuetlich alle fuenf Minuten, waehrend die + Hauptschleife im Sekundentakt weiterlaeuft. + + Die Faeden fassen die Datenbank nicht an - sie legen ihre Werte in eine + Queue, geschrieben wird im Hauptfaden. Dieselbe Regel wie beim Versand: + eine pymysql-Verbindung gehoert einem Faden. + """ + + def __init__(self): + self.ergebnisse = queue.Queue() + self.laeuft = True + + def aufnehmen(self, transport, abstand): + faden = threading.Thread(target=self._arbeiten, args=(transport, abstand), + name="sammler-" + transport.schema, daemon=True) + faden.start() + + def _arbeiten(self, transport, abstand): + while self.laeuft: + try: + werte = transport.zustaende_lesen() + if werte: + self.ergebnisse.put(werte) + except Exception as fehler: + logger.warning("%s nicht abfragbar: %s", transport.schema, fehler) + # In Sekundenschritten warten, damit das Beenden nicht bis zum + # naechsten Durchgang dauert - bei Tahoma waeren das fuenf Minuten. + for _ in range(max(1, int(abstand))): + if not self.laeuft: + return + time.sleep(1) + + def abholen(self): + """Alles, was die Faeden seit dem letzten Mal geliefert haben.""" + neu = {} + while True: + try: + neu.update(self.ergebnisse.get_nowait()) + except queue.Empty: + return neu + + def beenden(self): + self.laeuft = False + + +# =========================================================================== +# Der Runner +# =========================================================================== + +class Runner: + + def __init__(self, config, dry_run=False): + self.config = config + self.dry_run = dry_run or config.ja("runner", "dry_run") + self.db = verbinden(config) + self.solar = verbinden(config, "solar") + + self.werte = {} # state_id -> letzter bekannter Wert + self.geschrieben = {} # state_id -> zuletzt in die DB geschriebener Wert + self.geschrieben_um = {} # state_id -> wann das war + self.war_aktiv = {} # automation_id -> war im Zeitfenster + self.lief_im_fenster = {} # automation_id -> hat im Fenster ausgeloest + self._sonne = (None, "00:00", "00:00") # (datum, aufgang, untergang) + self._kalender = (None, {"feiertag": False, "ferien": False}) + self._letzte_saeuberung = None + + self.versand = Versand() + self.sammler = Sammler() + self.mqtt = self._mqtt_verbinden() + self.transporte = [ + MQTTTransport(self.mqtt, self.dry_run), + WLEDTransport(requests, dry_run=self.dry_run), + HTTPTransport(requests, dry_run=self.dry_run), + TahomaTransport(requests, config.text("tahoma", "pin"), + config.text("tahoma", "token"), + config.zahl("tahoma", "timeout", 10), self.dry_run), + LogicTransport(self.sonnenzeiten), + ] + self.regelwerk = None + + # --- Aufbau ---------------------------------------------------------- + + def _mqtt_verbinden(self): + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, + client_id=self.config.text("mqtt", "client_id", "autoaction_runner")) + benutzer = self.config.text("mqtt", "username") + if benutzer: + client.username_pw_set(benutzer, self.config.text("mqtt", "password")) + client.on_message = self._mqtt_nachricht + client.connect(self.config.text("mqtt", "broker", "localhost"), + self.config.zahl("mqtt", "port", 1883), 60) + client.loop_start() + return client + + def _mqtt_nachricht(self, client, userdata, nachricht): + # Der Client laeuft schon, waehrend __init__ noch die Transporte baut. + for transport in getattr(self, "transporte", []): + if isinstance(transport, MQTTTransport): + transport.nachricht(nachricht.topic, nachricht.payload) + + def transport_fuer(self, actor_url): + for transport in self.transporte: + if transport.passt(actor_url): + return transport + return None + + def regelwerk_laden(self): + self.regelwerk = Regelwerk.laden(self.db) + + # Welche Geraete Position und Neigung zusammen koennen. Nur die haben + # die Kugelschreiber-Mechanik, und nur bei ihnen wird ein "Zu" zum + # zweifachen Schwenken - siehe TahomaTransport. + kombi = {k["actor_url"] for k in self.regelwerk.kommandos.values() + if k["command_url"] == "setClosureAndOrientation"} + for transport in self.transporte: + if isinstance(transport, TahomaTransport): + transport.kombigeraete_setzen(kombi) + + # Jeder Transport bekommt die Messwerte, fuer die er zustaendig ist. + for transport in self.transporte: + passende = [{"id": s["id"], "actor_url": s["actor_url"], + "state_url": s["state_url"], "value_path": s["value_path"], + "wertetabelle": s["wertetabelle"]} + for s in self.regelwerk.states.values() + if transport.passt(s["actor_url"])] + transport.zustaende_anmelden(passende) + # Der zuletzt bekannte Wert aus der Datenbank ist besser als gar + # keiner: nach einem Neustart steht sonst jede Bedingung auf "unklar", + # bis das Geraet zufaellig etwas schickt. + for s in self.regelwerk.states.values(): + if s["id"] not in self.werte and s["current_value"] is not None: + self.werte[s["id"]] = s["current_value"] + # Was schon in der Tabelle steht, muss nach einem Neustart nicht + # noch einmal hineingeschrieben werden. + self.geschrieben.setdefault(s["id"], s["current_value"]) + + # --- Umgebung -------------------------------------------------------- + + def sonnenzeiten(self): + """Aus solarLog.daylight, einmal je Tag geholt.""" + heute = date.today() + if self._sonne[0] == heute: + return self._sonne[1], self._sonne[2] + auf, unter = "00:00", "00:00" + try: + with self.solar.cursor() as c: + c.execute("SELECT sunrise, sunset FROM daylight WHERE date = %s", (heute,)) + zeile = c.fetchone() + if zeile: + auf = self._als_uhrzeit(zeile["sunrise"]) + unter = self._als_uhrzeit(zeile["sunset"]) + else: + logger.warning("Kein Eintrag in daylight fuer %s", heute) + except Exception as fehler: + logger.warning("Sonnenzeiten nicht lesbar: %s", fehler) + self._sonne = (heute, auf, unter) + return auf, unter + + @staticmethod + def _als_uhrzeit(wert): + """TIME-Spalten liefert pymysql als timedelta, nicht als Text.""" + if isinstance(wert, timedelta): + minute = int(wert.total_seconds()) // 60 + return "%02d:%02d" % (minute // 60 % 24, minute % 60) + return str(wert)[:5] + + def kalender(self): + """Ferien und Feiertage von heute, einmal je Tag geholt.""" + heute = date.today() + if self._kalender[0] == heute: + return self._kalender[1] + stand = {"feiertag": False, "ferien": False} + try: + with self.db.cursor() as c: + c.execute("SELECT holiday, vacation FROM calendar_days WHERE date = %s", (heute,)) + zeile = c.fetchone() + if zeile: + stand = {"feiertag": bool(zeile["holiday"]), "ferien": bool(zeile["vacation"])} + except Exception as fehler: + logger.warning("Kalender nicht lesbar: %s", fehler) + self._kalender = (heute, stand) + return stand + + # --- Werte ----------------------------------------------------------- + + def sammler_starten(self): + """ + Je gepolltem Transport ein Faden. MQTT meldet sich von selbst und die + Uhr rechnet der Runner - die beiden stehen hier nicht. + """ + vorgaben = [(HTTPTransport, "poll_http", 60), + (WLEDTransport, "poll_wled", 60), + (TahomaTransport, "poll_tahoma", 300)] + for transport in self.transporte: + for klasse, schluessel, vorgabe in vorgaben: + if isinstance(transport, klasse): + abstand = self.config.zahl("runner", schluessel, vorgabe) + self.sammler.aufnehmen(transport, abstand) + logger.info("%s wird alle %d s abgefragt", klasse.__name__, abstand) + break + + def werte_einsammeln(self, auch_geraete=False): + """ + MQTT und Uhr kosten nichts und werden jeden Takt gelesen; die Geraete + liefert der Sammler aus dem Hintergrund. Nur bei --once, wo es keine + Faeden gibt, fragt die Schleife die Geraete selbst. + """ + neu = {} + for transport in self.transporte: + billig = isinstance(transport, (MQTTTransport, LogicTransport)) + if billig or auch_geraete: + neu.update(transport.zustaende_lesen()) + neu.update(self.sammler.abholen()) + if neu: + self.werte.update(neu) + self.werte_zurueckschreiben(neu) + return neu + + def werte_zurueckschreiben(self, neu): + """ + current_value nachfuehren, damit der Editor im Browser aktuelle Zahlen + zeigt ("Temperatur (= 21,4 °C)"). + + Geschrieben wird nur, was sich geaendert hat. MQTT liefert ohnehin nur + neu Hereingekommenes, die gepollten Transporte dagegen bei jedem + Durchgang ihren kompletten Bestand - ohne diesen Vergleich gingen rund + 180 unveraenderte Werte je Runde in die Tabelle. Zusaetzlich + gedrosselt, sonst schreibt ein gespraechiger Sensor im Sekundentakt. + """ + jetzt = datetime.now() + faellig = [(w, i) for i, w in neu.items() + if self.geschrieben.get(i) != w + and self.geschrieben_um.get(i, datetime.min) + SCHREIB_ABSTAND <= jetzt] + if not faellig: + return + try: + with self.db.cursor() as c: + c.executemany("UPDATE actor_states SET current_value = %s WHERE id = %s", faellig) + for wert, state_id in faellig: + self.geschrieben[state_id] = wert + self.geschrieben_um[state_id] = jetzt + except Exception as fehler: + logger.warning("current_value nicht schreibbar: %s", fehler) + + # --- Ausfuehren ------------------------------------------------------ + + def ausloesen(self, automatik, anlass): + """ + Die Aktionen einer Automatik in den Versand geben. + + Geschickt wird im Hintergrund - eine Jalousie zuzufahren dauert ueber + eine Minute, und so lange darf die Auswertung nicht stehen. Was dabei + schiefgeht, kommt spaeter ueber ergebnisse_verbuchen() ins Protokoll. + """ + fehlerText = [] + eingereiht = 0 + for aktion in automatik["aktionen"]: + kommando = self.regelwerk.kommandos.get(aktion["command_id"]) + if kommando is None: + fehlerText.append("Kommando %s gibt es nicht mehr" % aktion["command_id"]) + logger.error("%s: Kommando %s gibt es nicht mehr", + automatik["name"], aktion["command_id"]) + continue + transport = self.transport_fuer(kommando["actor_url"]) + if transport is None: + fehlerText.append("Kein Transport fuer %s" % kommando["actor_url"]) + logger.error("%s: fuer %s (%s) gibt es keinen Transport", + automatik["name"], kommando["actor_name"], kommando["actor_url"]) + continue + auftrag = { + "actor_url": kommando["actor_url"], + "command_url": kommando["command_url"], + "params": [{"url": p["url"], "name": p["parameter_name"], + "wert": aktion["werte"].get(p["id"], "")} + for p in kommando["params"]], + } + beschreibung = "%s: %s" % (kommando["actor_name"], kommando["command_name"]) + logger.info("%s: %s (unterwegs)", automatik["name"], beschreibung) + self.versand.einreihen(automatik["id"], beschreibung, transport, auftrag) + eingereiht += 1 + + ergebnis = "error" if fehlerText else anlass + detail = "; ".join(fehlerText)[:255] if fehlerText \ + else ("%d Kommando(s) unterwegs" % eingereiht) + try: + with self.db.cursor() as c: + c.execute("""UPDATE automations SET last_run = NOW(), changed = changed + WHERE id = %s""", (automatik["id"],)) + c.execute("""INSERT INTO automation_log (automation_id, result, detail) + VALUES (%s, %s, %s)""", (automatik["id"], ergebnis, detail)) + except Exception as fehler: + logger.warning("Protokoll nicht schreibbar: %s", fehler) + automatik["last_run"] = datetime.now() + self.lief_im_fenster[automatik["id"]] = True + + def ergebnisse_verbuchen(self): + """ + Was der Versand inzwischen erledigt hat ins Protokoll schreiben. + + Nur Fehlschlaege bekommen eine eigene Zeile - der Lauf selbst steht + schon drin, und ein Protokoll, das jedes gelungene Kommando einzeln + auffuehrt, findet niemand mehr etwas darin. + """ + for automation_id, beschreibung, fehler in self.versand.abholen(): + if fehler is None: + logger.debug("erledigt: %s", beschreibung) + continue + logger.error("%s konnte nicht geschickt werden: %s", beschreibung, fehler) + try: + with self.db.cursor() as c: + c.execute("""INSERT INTO automation_log (automation_id, result, detail) + VALUES (%s, 'error', %s)""", + (automation_id, ("%s: %s" % (beschreibung, fehler))[:255])) + except Exception as schreibfehler: + logger.warning("Protokoll nicht schreibbar: %s", schreibfehler) + + def flanke_merken(self, automatik, erfuellt): + if bool(automatik["cond_met"]) == bool(erfuellt): + return + automatik["cond_met"] = 1 if erfuellt else 0 + try: + with self.db.cursor() as c: + c.execute("""UPDATE automations SET cond_met = %s, changed = changed + WHERE id = %s""", (automatik["cond_met"], automatik["id"])) + except Exception as fehler: + logger.warning("cond_met nicht schreibbar: %s", fehler) + + def durchlauf(self, fenster=NACHHOLFENSTER): + jetzt = datetime.now() + kalender = self.kalender() + + for automatik in self.regelwerk.automatiken.values(): + aktiv = (tag_passt(automatik, jetzt, kalender) + and im_zeitfenster(jetzt, automatik["window_from"], automatik["window_to"])) + vorher_aktiv = self.war_aktiv.get(automatik["id"], aktiv) + + if aktiv: + if not vorher_aktiv: + self.lief_im_fenster[automatik["id"]] = False + erfuellt = gruppen_erfuellt(automatik, self.regelwerk, self.werte, + jetzt, fenster) + if erfuellt and not automatik["cond_met"]: + if self.gesperrt(automatik, jetzt): + logger.debug("%s: Flanke faellt in die Sperrzeit, uebersprungen", + automatik["name"]) + else: + self.ausloesen(automatik, "fired") + self.flanke_merken(automatik, erfuellt) + else: + # Das Fenster ist gerade zugegangen. Wer "auf jeden Fall" + # angehakt hat, bekommt jetzt seinen Lauf - aber nur, wenn in + # diesem Fenster noch keiner stattgefunden hat. Nach einem + # Neustart mitten im Fenster weiss der Runner das nicht mehr + # aus dem Speicher, deshalb zaehlt zusaetzlich last_run. + if vorher_aktiv and automatik["force_once"] and not self.lief_im_fenster.get(automatik["id"]): + if not self.lief_heute(automatik, jetzt): + logger.info("%s: Zeitfenster vorbei, wird trotzdem ausgefuehrt", + automatik["name"]) + self.ausloesen(automatik, "forced") + # Ausserhalb des Fensters die Flanke zuruecksetzen, sonst + # koennte sie im naechsten Fenster nicht mehr steigen. + self.flanke_merken(automatik, False) + self.lief_im_fenster[automatik["id"]] = False + + self.war_aktiv[automatik["id"]] = aktiv + + @staticmethod + def gesperrt(automatik, jetzt): + """ + Liegt die letzte Ausloesung noch innerhalb der Sperrzeit? + + Gegen Messwerte, die um die Schwelle pendeln: "Temperatur > 22" bei + 22,1 / 21,9 / 22,1 Grad ist jedes Mal eine echte steigende Flanke, und + ueber MQTT koennen die Werte im Sekundentakt hereinkommen. + + Die Flanke wird dabei verworfen und nicht aufgehoben. Ein Rollladen, + der eine Viertelstunde spaeter doch noch losfaehrt, weil vor langer + Zeit einmal eine Schwelle gestreift wurde, waere unangenehmer als + einer, der gar nicht faehrt. Der naechste echte Anlass nach Ablauf + der Sperre kommt ohnehin durch. + + force_once ist davon nicht betroffen: es greift nur, wenn im Fenster + gar nichts gelaufen ist - dann ist auch keine Sperre aktiv. + """ + sperre = int(automatik.get("lockout_secs") or 0) + letzter = automatik.get("last_run") + if not sperre or not letzter: + return False + return (jetzt - letzter).total_seconds() < sperre + + @staticmethod + def lief_heute(automatik, jetzt): + letzter = automatik.get("last_run") + return bool(letzter) and letzter.date() == jetzt.date() + + def protokoll_saeubern(self): + heute = date.today() + if self._letzte_saeuberung == heute: + return + self._letzte_saeuberung = heute + try: + with self.db.cursor() as c: + c.execute("DELETE FROM automation_log WHERE ts < NOW() - INTERVAL %s DAY", + (LOG_AUFBEWAHRUNG_TAGE,)) + except Exception as fehler: + logger.warning("Protokoll nicht aufraeumbar: %s", fehler) + + # --- Hauptschleife --------------------------------------------------- + + def laufen(self, nur_einmal=False): + """ + Die Schleife macht nur noch Billiges: Werte abholen, auswerten, + protokollieren. Alles, was auf ein Netz warten muss, laeuft daneben - + die Geraeteabfrage im Sammler, das Schalten im Versand. Deshalb darf + der Takt kurz sein. + """ + self.regelwerk_laden() + takt = self.config.zahl("runner", "tick_seconds", 10) + neuladen = self.config.zahl("runner", "reload_seconds", 30) + fenster = self.config.zahl("runner", "catchup_minutes", NACHHOLFENSTER) + letztes_pruefen = 0.0 + if not nur_einmal: + self.sammler_starten() + + while True: + jetzt = time.monotonic() + self.werte_einsammeln(auch_geraete=nur_einmal) + self.durchlauf(fenster) + self.ergebnisse_verbuchen() + self.protokoll_saeubern() + + if jetzt - letztes_pruefen >= neuladen: + letztes_pruefen = jetzt + try: + if Regelwerk.signatur_lesen(self.db) != self.regelwerk.signatur: + logger.info("Regelwerk hat sich geaendert, wird neu geladen") + self.regelwerk_laden() + except Exception as fehler: + logger.warning("Regelwerk nicht pruefbar: %s", fehler) + + if nur_einmal: + return + time.sleep(takt) + + def beenden(self): + self.sammler.beenden() + offen = self.versand.offen() + if offen: + logger.info("%d Kommando(s) noch unterwegs - wird nicht abgewartet", offen) + self.versand.beenden() + try: + self.mqtt.loop_stop() + self.mqtt.disconnect() + except Exception: + pass + + +# =========================================================================== + +def main(): + parser = argparse.ArgumentParser(description="Fuehrt die Automatiken aus dem Web-UI aus.") + parser.add_argument("--dry-run", action="store_true", + help="nichts wirklich schalten, nur protokollieren") + parser.add_argument("--once", action="store_true", + help="einen einzigen Durchlauf, dann beenden") + parser.add_argument("--verbose", action="store_true", help="DEBUG-Ausgaben") + parser.add_argument("--config", default="config.ini") + args = parser.parse_args() + + config = Config(args.config) + logging.basicConfig( + level=logging.DEBUG if args.verbose else getattr( + logging, config.text("runner", "log_level", "INFO").upper(), logging.INFO), + format="%(asctime)s - %(levelname)s - %(message)s") + + runner = Runner(config, args.dry_run) + if runner.dry_run: + logger.info("Probelauf: es wird nichts geschaltet.") + + # startSolarServer.sh beendet eine laufende Instanz mit SIGTERM, bevor es + # die neue startet. Ohne diesen Handler faellt der Prozess sofort um und + # beenden() kaeme nie dran: der Broker hielte die Verbindung noch eine + # Weile fuer lebendig, und ein gerade laufendes Kommando bliebe auf halbem + # Weg stehen. Als KeyboardInterrupt geht es denselben Weg wie Strg-C. + def abbrechen(signum, rahmen): + raise KeyboardInterrupt + + signal.signal(signal.SIGTERM, abbrechen) + + try: + runner.laufen(args.once) + except KeyboardInterrupt: + logger.info("Abbruch - wird beendet") + finally: + runner.beenden() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/autoActions/config.ini.example b/autoActions/config.ini.example new file mode 100644 index 0000000..7f8950e --- /dev/null +++ b/autoActions/config.ini.example @@ -0,0 +1,87 @@ +# ============================================================================ +# AutoAction-Runner - Konfiguration +# ============================================================================ +# Kopieren nach config.ini und ausfuellen. config.ini ist nicht versioniert +# (siehe .gitignore), weil hier Zugangsdaten stehen. + +# ============================================================================ +# DATENBANK - Geraete und Automatiken (homeMesh) +# ============================================================================ +[database] +host = nas.fritz.box +port = 3310 +database = homeMesh +user = homeMesh +password = + +# ============================================================================ +# DATENBANK - Messwerte (solarLog) +# ============================================================================ +# Nur fuer die Tabelle `daylight`, aus der Sonnenauf- und -untergang kommen. +# Leer lassen, wenn dieselben Zugangsdaten wie oben gelten. +[solar] +database = solarLog +user = +password = + +# ============================================================================ +# MQTT +# ============================================================================ +[mqtt] +broker = nas.fritz.box +port = 1883 +username = +password = +client_id = autoaction_runner + +# ============================================================================ +# TAHOMA +# ============================================================================ +# Fuer Aktoren, deren URL mit io:// beginnt. Ohne Token bleiben sie stumm - +# der Runner protokolliert das dann als Fehler, statt still nichts zu tun. +[tahoma] +pin = +token = +timeout = 10 + +# ============================================================================ +# KALENDER +# ============================================================================ +# Fuer fetch_calendar.py, das Feiertage und Schulferien in calendar_days +# eintraegt. Regionscodes siehe openholidaysapi.org, Bayern ist DE-BY. +[calendar] +country = DE +subdivision = DE-BY + +# ============================================================================ +# LAUFZEIT +# ============================================================================ +[runner] +# Abstand der Auswertung in Sekunden. Die Schleife wartet auf nichts mehr - +# Geraete fragt der Sammler in eigenen Faeden ab, geschaltet wird im Versand -, +# deshalb darf der Takt kurz sein. Er bestimmt nur noch, wie schnell eine +# hereingekommene MQTT-Nachricht ausgewertet wird. +tick_seconds = 10 + +# Wie oft Geraete abgefragt werden, die nichts von sich aus melden. Getrennt +# je Transport, weil sie unterschiedlich teuer sind: eine Shelly-Abfrage ist +# in Millisekunden zurueck, die Tahoma-Box braucht fuer ihre Geraete +# nacheinander gut zehn Sekunden. In Sekunden. +poll_http = 60 +poll_wled = 60 +poll_tahoma = 300 + +# Wie lange ein punktgenauer Ausloeser ("um 16:30", "Sonnenaufgang + 00:30") +# nachtraeglich noch gilt, in Minuten. Faellt die Auswertung in dieser Minute +# aus - Neustart, haengendes Geraet -, holt der naechste Takt sie nach. +# Ausgeloest wird trotzdem nur einmal, weil nur die steigende Flanke zaehlt. +catchup_minutes = 5 + +# Wie oft der Runner nachsieht, ob sich das Regelwerk geaendert hat. +reload_seconds = 30 + +# Nichts wirklich schalten, nur protokollieren. Zum Ausprobieren neuer Regeln. +dry_run = false + +# DEBUG, INFO, WARNING, ERROR +log_level = INFO diff --git a/autoActions/fetch_calendar.py b/autoActions/fetch_calendar.py new file mode 100644 index 0000000..c653c60 --- /dev/null +++ b/autoActions/fetch_calendar.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +""" +Fuellt die Tabelle `calendar_days` mit Feiertagen und Schulferien. + +Die Automatiken koennen mit "an Feiertagen ausfuehren" und "in den Ferien +ausfuehren" auf besondere Tage reagieren; woher das Wissen kommt, steht hier. +Nur besondere Tage werden eingetragen - ein fehlendes Datum ist ein +gewoehnlicher Tag. + +Quelle ist openholidaysapi.org: ein offenes Verzeichnis der EU-Kommission, das +gesetzliche Feiertage und Schulferien fuer alle deutschen Bundeslaender +liefert. Ein Aufruf je Jahr genuegt, deshalb ist das ein Cronjob und kein +Dauerlaeufer: + + 0 4 1 1 * /usr/bin/python3 /pfad/zu/fetch_calendar.py + +Ein zusaetzlicher Lauf im Herbst schadet nicht - die Ferientermine des +uebernaechsten Schuljahres stehen erst spaeter fest. + +Bereits eingetragene Tage werden ueberschrieben, nie doppelt angelegt. +""" + +import argparse +import logging +import sys +from datetime import date, timedelta + +import pymysql +import requests + +from autoaction_runner import Config, verbinden + +logger = logging.getLogger("kalender") + +BASIS = "https://openholidaysapi.org" + + +def zeitraum(api, land, region, von, bis): + """Eintraege einer Art (PublicHolidays oder SchoolHolidays) abholen.""" + antwort = requests.get( + BASIS + "/" + api, + params={"countryIsoCode": land, "subdivisionCode": region, + "languageIsoCode": "DE", "validFrom": von.isoformat(), + "validTo": bis.isoformat()}, + headers={"Accept": "application/json"}, timeout=20) + antwort.raise_for_status() + return antwort.json() + + +def name(eintrag): + for teil in eintrag.get("name", []): + if teil.get("text"): + return teil["text"][:80] + return "?" + + +def tage(eintrag): + """Ferien erstrecken sich ueber Wochen; hier wird daraus Tag fuer Tag.""" + start = date.fromisoformat(eintrag["startDate"]) + ende = date.fromisoformat(eintrag["endDate"]) + while start <= ende: + yield start + start += timedelta(days=1) + + +def main(): + parser = argparse.ArgumentParser(description="Feiertage und Ferien in calendar_days schreiben.") + parser.add_argument("--jahr", type=int, default=date.today().year) + parser.add_argument("--jahre", type=int, default=2, help="wie viele Jahre ab --jahr") + parser.add_argument("--config", default="config.ini") + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") + config = Config(args.config) + land = config.text("calendar", "country", "DE") + region = config.text("calendar", "subdivision", "DE-BY") + + von = date(args.jahr, 1, 1) + bis = date(args.jahr + args.jahre - 1, 12, 31) + logger.info("Hole %s bis %s fuer %s", von, bis, region) + + gesammelt = {} + for eintrag in zeitraum("PublicHolidays", land, region, von, bis): + for tag in tage(eintrag): + gesammelt.setdefault(tag, {})["holiday"] = name(eintrag) + for eintrag in zeitraum("SchoolHolidays", land, region, von, bis): + for tag in tage(eintrag): + gesammelt.setdefault(tag, {})["vacation"] = name(eintrag) + + db = verbinden(config) + with db.cursor() as c: + c.executemany( + """INSERT INTO calendar_days (date, holiday, vacation) VALUES (%s, %s, %s) + ON DUPLICATE KEY UPDATE holiday = VALUES(holiday), vacation = VALUES(vacation)""", + [(tag, eintrag.get("holiday"), eintrag.get("vacation")) + for tag, eintrag in sorted(gesammelt.items())]) + logger.info("%d Tage eingetragen", len(gesammelt)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/autoActions/transports.py b/autoActions/transports.py new file mode 100644 index 0000000..e1e75f1 --- /dev/null +++ b/autoActions/transports.py @@ -0,0 +1,626 @@ +#!/usr/bin/env python3 +""" +Transporte fuer den AutoAction-Runner. + +Ein Transport weiss, wie man mit einer Sorte Geraet redet - er liest deren +Messwerte und schickt deren Kommandos. Welcher zustaendig ist, entscheidet die +URL des Aktors in der Tabelle `actors`: + + mqtt://... MQTT-Geraete (Home-Assistant-Discovery) + http://... Shelly und andere HTTP-Geraete + wled://... WLED-Lampen + io://, rts://, internal://, ogp:// Tahoma - das Schema haengt an der + Funkart des Geraets, deshalb wird dort nicht danach + entschieden, sondern an der Box-Kennung in der URL + Logic das gerechnete Geraet "Zeitpunkt" (Uhrzeit, Datum, Sonne) + +Alle liegen in einer Datei statt in einem Paket wie bei deviceDiscovery: es +sind fuenf kurze Klassen, und wer eine sechste Geraeteart anschliesst, sieht +hier auf einen Blick, was dafuer zu tun ist. + +Jeder Transport hat zwei Haelften: + + zustaende_anmelden(states) einmalig beim Start + zustaende_lesen() liefert {state_id: wert} - nur was neu ist + senden(aktion) fuehrt ein Kommando aus + +`states` ist eine Liste von Dicts mit actor_url, state_url, value_path und +id, `aktion` ein Dict mit actor_url, command_url und params (Liste aus +{url, name, wert}). +""" + +import json +import logging +import re +import time +import urllib3 +from datetime import datetime +from urllib.parse import quote + +logger = logging.getLogger("autoaction.transport") + +# Die Tahoma-Box hat ein selbst ausgestelltes Zertifikat auf einen Namen, den +# nur das Heimnetz kennt. Die Pruefung ist dort bewusst aus (wie in +# ajax/tahoma.php); ohne diese Zeile warnt urllib3 bei jeder einzelnen +# Abfrage und uebertoent das eigentliche Protokoll. +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +def wert_aus_pfad(daten, pfad): + """ + Einen Teilwert aus einer Nutzlast holen: "[3]", "ssid", "seg[0].col[0]". + Ein leerer Pfad heisst: die Nutzlast selbst. + + Gebraucht wird das an zwei Stellen. MQTT-Geraete legen mehrere Messwerte + auf ein Topic - der go-eCharger schickt sechzehn Zahlen als JSON-Feld -, + und WLED liefert seinen gesamten Zustand als ein Dokument. + """ + if not pfad: + return daten + for teil in pfad.split("."): + treffer = re.match(r"^([^\[]*)((?:\[\d+\])*)$", teil) + if not treffer: + raise KeyError(pfad) + if treffer.group(1): + daten = daten[treffer.group(1)] + for index in re.findall(r"\[(\d+)\]", treffer.group(2)): + daten = daten[int(index)] + return daten + + +def uebersetze(wert, tabelle): + """ + Aus der gesendeten Zahl den Zustandsnamen machen: aus "2" wird "Charging". + + Manche Geraete schicken einen Zahlencode und meinen einen Zustand. Welche + Zahl welchen Namen hat, steht in possible_values - derselben Spalte, aus + der auch der Editor seine Auswahlliste baut. Eine Bedingung vergleicht + damit genau den Klartext, den man dort ausgewaehlt hat. + + Steht die Zahl nicht in der Tabelle, bleibt sie stehen: ein erfundener + Name waere schlimmer als ein roher Wert. + """ + if not tabelle: + return wert + if wert in tabelle: + return tabelle[wert] + try: # "2.0" und "2" meinen dieselbe Stufe + ganz = str(int(float(wert))) + except (TypeError, ValueError): + return wert + return tabelle.get(ganz, wert) + + +class Transport: + """Gemeinsame Form. Wer nichts zu lesen hat, erbt die leeren Methoden.""" + + schema = "" + + def passt(self, actor_url): + return actor_url.startswith(self.schema) + + def zustaende_anmelden(self, states): + pass + + def zustaende_lesen(self): + return {} + + def senden(self, aktion): + raise NotImplementedError + + +# --------------------------------------------------------------------------- +# MQTT +# --------------------------------------------------------------------------- + +class MQTTTransport(Transport): + """ + Die state_url ist hier das vollstaendige Topic, die parameter_url das + Kommando-Topic. Werte kommen von selbst herein und landen in einem + Zwischenspeicher, den der Runner im Takt abholt. + """ + + schema = "mqtt://" + + def __init__(self, client, dry_run=False): + self.client = client + self.dry_run = dry_run + self.topics = {} # topic -> [(state_id, value_path, wertetabelle), ...] + self.neu = {} # state_id -> wert + + def zustaende_anmelden(self, states): + self.topics = {} + for s in states: + if not s["state_url"]: + continue + self.topics.setdefault(s["state_url"], []).append( + (s["id"], s.get("value_path"), s.get("wertetabelle") or {})) + for topic in self.topics: + self.client.subscribe(topic) + mehrfach = sum(1 for e in self.topics.values() if len(e) > 1) + logger.info("MQTT: %d Topics abonniert, %d davon mit mehreren Messwerten", + len(self.topics), mehrfach) + + def nachricht(self, topic, payload): + """Wird vom Runner aus dem on_message-Rueckruf gerufen.""" + eintraege = self.topics.get(topic, []) + if not eintraege: + return + text = payload.decode("utf-8", "replace").strip() if isinstance(payload, bytes) else str(payload) + try: + daten = json.loads(text) + except ValueError: + daten = None # kein JSON - dann gilt der Rohtext + for state_id, pfad, tabelle in eintraege: + wert = self._wert(text, daten, pfad, tabelle) + if wert is not None: + self.neu[state_id] = wert + + @staticmethod + def _wert(text, daten, pfad, tabelle=None): + """ + Aus der Nutzlast den Wert eines einzelnen Messwerts machen. + + Mehrere Messwerte teilen sich oft ein Topic; welcher Teil gemeint ist, + steht in value_path - gelesen aus dem value_template der + Home-Assistant-Discovery. Ohne Pfad gilt die ganze Nutzlast, und + JSON-Skalare werden ausgepackt: manche Geraete schicken 21.4 mit + Anfuehrungszeichen, andere true statt ON. + + Steht eine Werttabelle dabei, wird aus der gesendeten Zahl der + Zustandsname: aus 2 wird "Charging". + """ + if daten is None: + return uebersetze(text, tabelle) + try: + wert = wert_aus_pfad(daten, pfad) + except (KeyError, IndexError, TypeError): + logger.debug("Pfad %s nicht in der Nutzlast: %s", pfad, text[:80]) + return None + if isinstance(wert, bool): + wert = "true" if wert else "false" + elif isinstance(wert, (int, float, str)): + wert = str(wert) + else: + return json.dumps(wert, ensure_ascii=False) + return uebersetze(wert, tabelle) + + def zustaende_lesen(self): + werte, self.neu = self.neu, {} + return werte + + def senden(self, aktion): + if not aktion["params"]: + # Kommando ohne Parameter: das Kommando selbst ist die Nutzlast. + self._publish(aktion["command_url"], "") + return + for p in aktion["params"]: + self._publish(p["url"] or aktion["command_url"], p["wert"]) + + def _publish(self, topic, nutzlast): + if not topic: + raise ValueError("Kommando ohne Topic") + if self.dry_run: + logger.info("[dry-run] MQTT %s <- %s", topic, nutzlast) + return + ergebnis = self.client.publish(topic, nutzlast, qos=1, retain=False) + ergebnis.wait_for_publish(timeout=5) + + +# --------------------------------------------------------------------------- +# HTTP (Shelly und Verwandte) +# --------------------------------------------------------------------------- + +class HTTPTransport(Transport): + """ + Die actor_url ist der Endpunkt, die state_url ein Feldname in dessen + JSON-Antwort ("tC", "a_voltage"). Solche Geraete melden sich nicht von + selbst, sie werden im Poll-Takt gefragt. + + Beim Senden wird die command_url als Abfrageargument an die actor_url + gehaengt ("turn=on") und die Parameter mit ihrem eigenen Namen dazu. + Frueher stand hier eine Uebersetzungstabelle, weil die Shelly-Kommandos + ohne URL in der Datenbank landeten - das ist im Discovery behoben, die + Zuordnung gehoert dorthin und nicht in den Runner. + """ + + schema = "http" + + def __init__(self, requests_modul, timeout=5, dry_run=False): + self.requests = requests_modul + self.timeout = timeout + self.dry_run = dry_run + self.states = [] + + def zustaende_anmelden(self, states): + self.states = [s for s in states if s["state_url"]] + logger.info("HTTP: %d Messwerte an %d Endpunkten", + len(self.states), len({s["actor_url"] for s in self.states})) + + def zustaende_lesen(self): + werte = {} + # Je Endpunkt eine Anfrage, auch wenn mehrere Messwerte daran haengen. + nach_url = {} + for s in self.states: + nach_url.setdefault(s["actor_url"], []).append(s) + for url, states in nach_url.items(): + try: + antwort = self.requests.get(url, timeout=self.timeout) + daten = antwort.json() + except Exception as fehler: + logger.debug("HTTP %s nicht erreichbar: %s", url, fehler) + continue + for s in states: + if isinstance(daten, dict) and s["state_url"] in daten: + werte[s["id"]] = str(daten[s["state_url"]]) + return werte + + def senden(self, aktion): + argumente = {} + for teil in (aktion["command_url"] or "").split("&"): + if "=" in teil: + schluessel, wert = teil.split("=", 1) + argumente[schluessel] = wert + for p in aktion["params"]: + if p["url"]: + argumente[p["url"]] = p["wert"] + if not argumente: + raise RuntimeError("Kommando ohne URL und ohne Parameter - im " + "Geraetemodell fehlt die Angabe, was zu schicken ist") + if self.dry_run: + logger.info("[dry-run] HTTP %s %s", aktion["actor_url"], argumente) + return + antwort = self.requests.get(aktion["actor_url"], params=argumente, timeout=self.timeout) + if antwort.status_code >= 400: + raise RuntimeError("HTTP %d von %s" % (antwort.status_code, aktion["actor_url"])) + + +# --------------------------------------------------------------------------- +# WLED +# --------------------------------------------------------------------------- + +class WLEDTransport(Transport): + """ + WLED-Lampen sprechen ueber eine einzige JSON-Schnittstelle: + GET http://IP/json/state liefert den Zustand, POST dorthin setzt ihn. + + Das Geraetemodell nutzt das elegant aus - die command_url ist eine + JSON-Vorlage mit Platzhaltern: + + {"bri":%brightness%} + {"seg":[{"col":[[%red%,%green%,%blue%]]}]} + + Gesendet wird also nicht Argument fuer Argument, sondern die ausgefuellte + Vorlage am Stueck. Deshalb haben die Parameter hier auch keine eigene URL: + ihr Name ist der Platzhalter. + + Die state_url ist ein Pfad in die Antwort ("on", "bri", + "seg[0].col[0]") - dieselbe Schreibweise, die auch in current_value steht. + """ + + schema = "wled://" + + def __init__(self, requests_modul, timeout=5, dry_run=False): + self.requests = requests_modul + self.timeout = timeout + self.dry_run = dry_run + self.states = [] + + @staticmethod + def _adresse(actor_url): + return "http://" + actor_url[len("wled://"):].rstrip("/") + + def zustaende_anmelden(self, states): + self.states = [s for s in states if s["state_url"]] + logger.info("WLED: %d Messwerte an %d Lampen", + len(self.states), len({s["actor_url"] for s in self.states})) + + def zustaende_lesen(self): + werte = {} + nach_lampe = {} + for s in self.states: + nach_lampe.setdefault(s["actor_url"], []).append(s) + for actor_url, states in nach_lampe.items(): + try: + antwort = self.requests.get(self._adresse(actor_url) + "/json/state", + timeout=self.timeout) + daten = antwort.json() + except Exception as fehler: + logger.debug("WLED %s nicht erreichbar: %s", actor_url, fehler) + continue + for s in states: + # Bei WLED ist die state_url selbst schon der Pfad. Ein + # gesetzter value_path hat trotzdem Vorrang, falls das + # Geraetemodell spaeter darauf umgestellt wird. + pfad = s.get("value_path") or s["state_url"] + try: + werte[s["id"]] = str(wert_aus_pfad(daten, pfad)) + except (KeyError, IndexError, TypeError): + logger.debug("WLED %s: Pfad %s nicht gefunden", actor_url, pfad) + return werte + + def senden(self, aktion): + vorlage = aktion["command_url"] + if not vorlage: + raise RuntimeError("WLED-Kommando ohne Vorlage") + for p in aktion["params"]: + vorlage = vorlage.replace("%" + p["name"] + "%", str(p["wert"])) + try: + rumpf = json.loads(vorlage) + except ValueError: + # Ein nicht ersetzter Platzhalter oder ein Textwert an einer + # Stelle, wo eine Zahl stehen muss. Lieber hier abbrechen als der + # Lampe etwas Unverstaendliches schicken. + raise RuntimeError("WLED-Vorlage ergibt kein gueltiges JSON: " + vorlage[:120]) + if self.dry_run: + logger.info("[dry-run] WLED %s <- %s", aktion["actor_url"], + json.dumps(rumpf, ensure_ascii=False)) + return + antwort = self.requests.post(self._adresse(aktion["actor_url"]) + "/json/state", + json=rumpf, timeout=self.timeout) + if antwort.status_code >= 400: + raise RuntimeError("WLED antwortete mit %d" % antwort.status_code) + + +# --------------------------------------------------------------------------- +# Tahoma +# --------------------------------------------------------------------------- + +class TahomaTransport(Transport): + """ + Die actor_url ist die deviceURL, die state_url ein Statusname + ("core:ClosureState"), die command_url ein Kommandoname ("setClosure"). + Geschickt wird ueber exec/apply - genauso wie in ajax/tahoma.php, nur ohne + die dortige Sonderbehandlung fuer "faehrt gerade". + + Zustaendig ist dieser Transport fuer alles, was die Kennung der eigenen + Box in der URL traegt. Am Schema laesst sich das nicht festmachen: es + beschreibt die Funkart, und dieselbe Box liefert io:// fuer die + Jalousien, rts:// fuer die Dachfenster und internal:// fuer die Alarm- + anlage. Ohne PIN in der config.ini ist niemand zustaendig - dann meldet + der Runner beim Ausloesen "kein Transport", statt still nichts zu tun. + """ + + schema = "io://" + + # Bis zu dieser Neigung fahren die Aussenjalousien direkt. + # + # Sie haben eine Kugelschreiber-Mechanik: beim Herunterfahren stehen die + # Lamellen bei etwa 30 %. In Richtung 0 % laesst sich von jeder Stellung + # aus direkt neigen; darueber hinaus muss die Mechanik erst einmal auf + # 0 % zurueck, sonst rastet sie nicht um - ein "Neigung 80 %" ohne + # diesen Umweg bleibt wirkungslos. + # + # Das gilt fuer jedes Kommando, das die Neigung setzt - "Neigung" ebenso + # wie "Position+Neigung". Erkannt wird es deshalb am Parameter und nicht + # am Kommandonamen: beide heissen ihren Neigungsparameter "Neigung". + # + # Dasselbe steht in restricted/commands.php: beide Versender brauchen es. + NEIGUNG_DIREKT_MAX = 30 + + # So heissen die beiden Parameter einer Jalousie im Geraetemodell. + NEIGUNG_PARAMETER = "Neigung" + POSITION_PARAMETER = "Position" + + # So lange wird hoechstens auf das Ende einer Fahrt gewartet. Gemessen: + # eine Neigung von 100 % auf 0 % dauert gut fuenfzehn Sekunden, eine + # volle Fahrt von oben nach unten rund sechzig. Die Grenze ist die + # Notbremse, nicht die uebliche Dauer. + JALOUSIE_WARTE_SEKUNDEN = 120 + + # So lange gilt ein "faehrt nicht" direkt nach dem Absenden als noch + # nicht aussagekraeftig: die Box meldet core:MovingState traege, kurz + # nach einem Kommando steht dort noch der alte Wert. + JALOUSIE_VORLAUF_SEKUNDEN = 8 + + def __init__(self, requests_modul, pin, token, timeout=10, dry_run=False): + self.requests = requests_modul + self.pin = pin + self.token = token + self.timeout = timeout + self.dry_run = dry_run + self.states = [] + self.kombigeraete = set() + + def passt(self, actor_url): + return bool(self.pin) and ("://" + self.pin + "/") in actor_url + + @property + def basis(self): + return "https://gateway-%s:8443/enduser-mobile-web/1/enduserAPI" % self.pin + + def _kopf(self): + return {"Content-Type": "application/json", + "Authorization": "Bearer " + self.token} + + def zustaende_anmelden(self, states): + self.states = [s for s in states if s["state_url"]] + logger.info("Tahoma: %d Messwerte an %d Geraeten", + len(self.states), len({s["actor_url"] for s in self.states})) + + def kombigeraete_setzen(self, urls): + """ + Welche Geraete Position und Neigung zusammen koennen - vom Runner aus + dem Geraetemodell gesetzt, damit der Transport dafuer nicht selbst in + die Datenbank greifen muss. + """ + self.kombigeraete = set(urls) + + def zustaende_lesen(self): + if not self.token: + return {} + werte = {} + nach_geraet = {} + for s in self.states: + nach_geraet.setdefault(s["actor_url"], []).append(s) + for geraet, states in nach_geraet.items(): + try: + antwort = self.requests.get( + self.basis + "/setup/devices/" + quote(geraet, safe="") + "/states", + headers=self._kopf(), timeout=self.timeout, verify=False) + zustaende = {z["name"]: z.get("value") for z in antwort.json()} + except Exception as fehler: + logger.debug("Tahoma %s nicht erreichbar: %s", geraet, fehler) + continue + for s in states: + if s["state_url"] in zustaende: + werte[s["id"]] = str(zustaende[s["state_url"]]) + return werte + + def senden(self, aktion): + if not self.token: + raise RuntimeError("Kein Tahoma-Token in der config.ini") + # Die Reihenfolge der Parameter ist die aus command_parameters - bei + # setClosureAndOrientation also erst Position, dann Winkel. + befehl = aktion["command_url"] + parameter = [self._zahl(p["wert"]) for p in aktion["params"]] + neigung_index = None + position_index = None + for i, p in enumerate(aktion["params"]): + if p["name"] == self.NEIGUNG_PARAMETER: + neigung_index = i + elif p["name"] == self.POSITION_PARAMETER: + position_index = i + + # "Zu" allein macht diese Jalousien nicht dicht: sie faehrt herunter, + # die Lamellen bleiben durch die Mechanik aber bei etwa 30 % offen. + # Gemeint ist "ganz unten, Lamellen geschlossen" - also dasselbe wie + # Position 100 mit Neigung 100, und damit ein Fall fuer die Regel. + if befehl == "down" and self._kannKombi(aktion["actor_url"]): + befehl = "setClosureAndOrientation" + parameter = [100, 100] + position_index, neigung_index = 0, 1 + + # Kugelschreiber-Mechanik, siehe NEIGUNG_DIREKT_MAX. Geschickt wird + # derselbe Befehl zweimal - erst mit Neigung 0, dann mit dem + # gewuenschten Wert. Die Position bleibt dabei stehen, die Jalousie + # faehrt also nur einmal. + umweg = (neigung_index is not None + and isinstance(parameter[neigung_index], (int, float)) + and parameter[neigung_index] > self.NEIGUNG_DIREKT_MAX) + vorstufe = list(parameter) + if umweg: + vorstufe[neigung_index] = 0 + + if self.dry_run: + logger.info("[dry-run] Tahoma %s %s%s", befehl, parameter, + " (zuerst %s, dann warten)" % vorstufe if umweg else "") + return + + if umweg: + self._apply(aktion["actor_url"], befehl, vorstufe) + self._warteAufJalousie(aktion["actor_url"], 0, + None if position_index is None else int(parameter[position_index])) + self._apply(aktion["actor_url"], befehl, parameter) + + def _kannKombi(self, actor_url): + """ + Hat das Geraet ein Kommando fuer Position und Neigung zusammen? + Nur solche Geraete sind Jalousien mit der Kugelschreiber-Mechanik. + """ + return actor_url in self.kombigeraete + + def _apply(self, actor_url, name, parameter): + """Ein Kommando an die Box schicken.""" + rumpf = {"label": "AutoAction", + "actions": [{"deviceURL": actor_url, + "commands": [{"name": name, "parameters": parameter}]}]} + antwort = self.requests.post(self.basis + "/exec/apply", headers=self._kopf(), + data=json.dumps(rumpf), timeout=self.timeout, verify=False) + if antwort.status_code >= 400: + raise RuntimeError("Tahoma antwortete mit %d: %s" + % (antwort.status_code, antwort.text[:120])) + + def _warteAufJalousie(self, actor_url, neigung_ziel, schliessung_ziel=None): + """ + Wartet, bis die Jalousie ihre Fahrt beendet hat und die Ziele zeigt. + + Zwei Auskuenfte zusammen, weil einzeln keine traegt: + core:MovingState taugt fuer die lange Fahrt hoch und runter, wird bei + kurzen Neigungsfahrten aber nie gesetzt; die Zustandswerte sind die + eigentliche Wahrheit, zeigen kurz nach dem Kommando aber noch den + alten Stand. Fertig ist die Fahrt, wenn nichts mehr faehrt, die Ziele + erreicht sind und entweder ein "faehrt" gesehen wurde oder der + Vorlauf um ist. + """ + start = time.time() + gestartet = False + while time.time() - start < self.JALOUSIE_WARTE_SEKUNDEN: + time.sleep(2) + try: + antwort = self.requests.get( + self.basis + "/setup/devices/" + quote(actor_url, safe="") + "/states", + headers=self._kopf(), timeout=self.timeout, verify=False) + z = {x.get("name"): x.get("value") for x in antwort.json()} + except Exception as fehler: + logger.debug("Zustand nicht lesbar: %s", fehler) + continue + if z.get("core:MovingState") is True: + gestartet = True + continue + neigung = z.get("core:SlateOrientationState") + schliessung = z.get("core:ClosureState") + # Ein fehlendes Feld darf nicht als 0 durchgehen - das waere + # ausgerechnet beim Ziel 0 ein falsches Erfolgssignal. + neigung_ok = neigung is not None and int(neigung) == neigung_ziel + schliessung_ok = (schliessung_ziel is None + or (schliessung is not None and int(schliessung) == schliessung_ziel)) + if neigung_ok and schliessung_ok and ( + gestartet or time.time() - start >= self.JALOUSIE_VORLAUF_SEKUNDEN): + return True + logger.warning("%s hat Neigung %s%% nicht innerhalb von %d s erreicht", + actor_url, neigung_ziel, self.JALOUSIE_WARTE_SEKUNDEN) + return False + + @staticmethod + def _zahl(wert): + """Tahoma erwartet Zahlen als Zahlen, Text als Text.""" + try: + return int(wert) + except (TypeError, ValueError): + pass + try: + return float(wert) + except (TypeError, ValueError): + return wert + + +# --------------------------------------------------------------------------- +# Logic - das gerechnete Geraet +# --------------------------------------------------------------------------- + +class LogicTransport(Transport): + """ + Uhrzeit, Datum, Sonnenauf- und -untergang. Es gibt nichts zu abonnieren und + nichts zu schalten, die Werte entstehen im Takt. Sonnenzeiten kommen aus + solarLog.daylight, dieselbe Tabelle, aus der auch ajax/getSunrise.php liest. + """ + + schema = "Logic" + + def __init__(self, sonnenzeiten): + """sonnenzeiten: Funktion() -> (sonnenaufgang, sonnenuntergang) als "HH:MM".""" + self.sonnenzeiten = sonnenzeiten + self.states = [] + + def passt(self, actor_url): + return actor_url == "Logic" + + def zustaende_anmelden(self, states): + self.states = states + logger.info("Logic: %d Messwerte", len(states)) + + def zustaende_lesen(self): + jetzt = datetime.now() + auf, unter = self.sonnenzeiten() + tabelle = { + "time": jetzt.strftime("%H:%M"), + "date": jetzt.strftime("%d.%m.%Y"), + "sunrise": auf, + "sunset": unter, + } + return {s["id"]: tabelle[s["state_url"]] + for s in self.states if s["state_url"] in tabelle} + + def senden(self, aktion): + raise RuntimeError("Das Geraet \"Zeitpunkt\" kann nichts schalten") diff --git a/charger_goE.py b/charger_goE.py new file mode 100644 index 0000000..a1156f6 --- /dev/null +++ b/charger_goE.py @@ -0,0 +1,132 @@ +import json +import asyncio +import aiohttp +import sys +import logging +import math +from dataclasses import dataclass +_LOGGER = logging.getLogger(__name__) + +@dataclass +class ChargerData: + p_l1ev:float = 0 + p_l2ev:float = 0 + p_l3ev:float = 0 + Iset_ev:float = 0 + Phase_set_ev:int = 0 + AllowCharging_ev:bool = False + mode:str = "" + connected:bool = False + eto:int = 0 # Gesamtzaehlerstand der Wallbox in Wh + + +async def gatherNeededStatus() -> ChargerData: + ret = ChargerData + #print("get charger status..."); + timeout = aiohttp.ClientTimeout(total=3) + try: + async with aiohttp.ClientSession() as session: + async with session.get('http://192.168.179.122/api/status?filter=psm,fup,amp,frc,nrg,lmo,car,pgrid,pakku,ppv,eto',timeout=timeout) as response: #?filter=psm,fup,amp,frc,nrg,lmo,car + if response.status == 200: + html = await response.text() + #logging.warning(html) + state = json.loads(html) + # Gesamtzaehler der Wallbox in Wh. Genauer als die Summe + # ueber die Fuenf-Minuten-Leistungswerte: die Differenz + # zweier Staende trifft den Ladevorgang auf die + # Wattstunde, waehrend eine Summe aus Mittelwerten an den + # Raendern immer danebenliegt. + ret.eto = int(state.get("eto",0) or 0) + ret.p_l1ev = state["nrg"][7]/1000.0 + ret.p_l2ev = state["nrg"][8]/1000.0 + ret.p_l3ev = state["nrg"][9]/1000.0 + ret.Iset_ev = state["amp"] + ret.Phase_set_ev = state["psm"] + ret.AllowCharging_ev = state["frc"] + if state["car"] == 2 or state["car"] == 4: + ret.connected = True + else: + ret.connected = False + if state["fup"] == True and state["lmo"] == 4: + ret.mode = "Eco" + elif state["fup"] == True and state["lmo"] == 5: + ret.mode = "Next Trip" + else: + ret.mode = "Default" + + except: + return ret + #print("Body:", html) + return ret + +async def setPVparameters(pGrid:int,pPv:int,pAkku:int): + timeout = aiohttp.ClientTimeout(total=3) + #logging.warning('{"pGrid":'+str(round(pGrid))+',"pAkku":'+str(round(pAkku))+',"pPv":'+str(round(pPv))+'}') + #_LOGGER.error('{"pGrid":'+str(round(pGrid))+',"pAkku":'+str(round(pAkku))+',"pPv":'+str(round(pPv))+'}') + try: + async with aiohttp.ClientSession() as session: + async with session.get('http://192.168.179.122/api/set?ids={"pGrid":'+str(round(pGrid))+',"pAkku":'+str(round(pAkku))+',"pPv":'+str(round(pPv))+'}',timeout=timeout) as response: + if response.status != 200: + #_LOGGER.error('goE Return: %s' % await response.text()) + return False + #else: + # _LOGGER.error('goE Return: %s' % await response.text()) + except Exception as error: + _LOGGER.error(f"Error during sending GoE Parameters: %s",error) + return False + + +async def setChargePower(power:int, currPhases:int, currAmp:int, currCharging:bool): + timeout = aiohttp.ClientTimeout(total=3) + + if(power < 3700): + phases = 1 + amp = math.floor(power/240) + elif(power > 4500): + phases = 2 + amp = math.floor((power/3)/240) + else: + amp = 16 + + if(amp < 6): + amp = 0 + elif(amp > 16): + amp = 16 + if(phases != currPhases): + logging.warning("Change phases: "+str(phases)) + #async with aiohttp.ClientSession() as session: + # async with session.get('http://192.168.178.64/api/?psm='+phases,timeout=timeout) as response: + # if response.status != 200: + # return False + if(amp != currAmp and amp): + logging.warning("Change amp: "+str(amp)) + if(currCharging == False): + logging.warning("start charging") + #async with aiohttp.ClientSession() as session: + # async with session.get('http://192.168.178.64/api/set?amp='+amp,timeout=timeout) as response: + # if response.status != 200: + # return False + elif(currCharging): + logging.warning("stop charging") + #async with aiohttp.ClientSession() as session: + # async with session.get('http://192.168.178.64/api/set?frc=1',timeout=timeout) as response: + # if response.status != 200: + # return False + + #status = charger.get_status(status_type=goecharger_api_lite.GoeCharger.STATUS_FULL) + #print(json.dumps(status, indent=4)) + + #adjust amps +#curl "http://1.2.3.4/api/set?amp=16" +#current energy: nrg energy array, U (L1, L2, L3, N), I (L1, L2, L3), P (L1, L2, L3, N, Total), pf (L1, L2, L3, N) +#set 1-phase +#curl "http://1.2.3.4/api/set?psm=1" + +#set 3-phase +#curl "http://1.2.3.4/api/set?psm=2" + +#start charging +#curl "http://1.2.3.4/api/set?frc=0" + +#stop charging +#curl "http://1.2.3.4/api/set?frc=1" diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..ce09193 --- /dev/null +++ b/config.ini.example @@ -0,0 +1,40 @@ +; Zugangsdaten des SolarManagers. +; +; Diese Datei ist die Vorlage und steht im Repository. Zum Aufsetzen einmal +; +; cp config.ini.example config.ini +; +; und ausfuellen - config.ini selbst wird nicht eingecheckt (.gitignore). +; Gelesen wird sie von konfig.py. + +; =========================================================================== +; Messwerte (solarLog) +; =========================================================================== +; Benutzt von solarManager.py, gatherWaterData.py, gatherSkodaData.py, +; skoda_testdaten.py und zeit.py. +[database] +host = localhost +port = 3310 +user = solarLog +password = +database = solarLog + +; =========================================================================== +; Wecker (wecker.py) +; =========================================================================== +[alarm] +host = localhost +port = 3310 +user = alarm +password = +database = alarm + +; =========================================================================== +; Wallbox +; =========================================================================== +; Gelesen von startWattpilotMQTT.sh, das daraus die Umgebungsvariablen +; WATTPILOT_HOST und WATTPILOT_PASSWORD setzt. +[wattpilot] +host = +password = +mqtt_host = nas.fritz.box diff --git a/crcmod/__init__.py b/crcmod/__init__.py new file mode 100644 index 0000000..80f2ac3 --- /dev/null +++ b/crcmod/__init__.py @@ -0,0 +1,8 @@ +try: + from crcmod.crcmod import * + import crcmod.predefined +except ImportError: + # Make this backward compatible + from crcmod import * + import predefined +__doc__ = crcmod.__doc__ diff --git a/crcmod/_crcfunpy.py b/crcmod/_crcfunpy.py new file mode 100644 index 0000000..01476ca --- /dev/null +++ b/crcmod/_crcfunpy.py @@ -0,0 +1,107 @@ +#----------------------------------------------------------------------------- +# Low level CRC functions for use by crcmod. This version is implemented in +# Python for a couple of reasons. 1) Provide a reference implememtation. +# 2) Provide a version that can be used on systems where a C compiler is not +# available for building extension modules. +# +# Copyright (c) 2009 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- + +def _get_buffer_view(in_obj): + if isinstance(in_obj, str): + raise TypeError('Unicode-objects must be encoded before calculating a CRC') + mv = memoryview(in_obj) + if mv.ndim > 1: + raise BufferError('Buffer must be single dimension') + return mv + + +def _crc8(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFF + for x in mv.tobytes(): + crc = table[x ^ crc] + return crc + +def _crc8r(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFF + for x in mv.tobytes(): + crc = table[x ^ crc] + return crc + +def _crc16(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFF + for x in mv.tobytes(): + crc = table[x ^ ((crc>>8) & 0xFF)] ^ ((crc << 8) & 0xFF00) + return crc + +def _crc16r(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFF + for x in mv.tobytes(): + crc = table[x ^ (crc & 0xFF)] ^ (crc >> 8) + return crc + +def _crc24(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFF + for x in mv.tobytes(): + crc = table[x ^ (crc>>16 & 0xFF)] ^ ((crc << 8) & 0xFFFF00) + return crc + +def _crc24r(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFF + for x in mv.tobytes(): + crc = table[x ^ (crc & 0xFF)] ^ (crc >> 8) + return crc + +def _crc32(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFFFF + for x in mv.tobytes(): + crc = table[x ^ ((crc>>24) & 0xFF)] ^ ((crc << 8) & 0xFFFFFF00) + return crc + +def _crc32r(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFFFF + for x in mv.tobytes(): + crc = table[x ^ (crc & 0xFF)] ^ (crc >> 8) + return crc + +def _crc64(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFFFFFFFFFFFF + for x in mv.tobytes(): + crc = table[x ^ ((crc>>56) & 0xFF)] ^ ((crc << 8) & 0xFFFFFFFFFFFFFF00) + return crc + +def _crc64r(data, crc, table): + mv = _get_buffer_view(data) + crc = crc & 0xFFFFFFFFFFFFFFFF + for x in mv.tobytes(): + crc = table[x ^ (crc & 0xFF)] ^ (crc >> 8) + return crc + diff --git a/crcmod/crcmod.py b/crcmod/crcmod.py new file mode 100644 index 0000000..ec5adaf --- /dev/null +++ b/crcmod/crcmod.py @@ -0,0 +1,457 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- +'''crcmod is a Python module for gererating objects that compute the Cyclic +Redundancy Check. Any 8, 16, 24, 32, or 64 bit polynomial can be used. + +The following are the public components of this module. + +Crc -- a class that creates instances providing the same interface as the +algorithms in the hashlib module in the Python standard library. These +instances also provide a method for generating a C/C++ function to compute +the CRC. + +mkCrcFun -- create a Python function to compute the CRC using the specified +polynomial and initial value. This provides a much simpler interface if +all you need is a function for CRC calculation. +''' + +__all__ = '''mkCrcFun Crc +'''.split() + +# Select the appropriate set of low-level CRC functions for this installation. +# If the extension module was not built, drop back to the Python implementation +# even though it is significantly slower. +try: + import crcmod._crcfunext as _crcfun + _usingExtension = True +except ImportError: + import crcmod._crcfunpy as _crcfun + _usingExtension = False + +import sys, struct + +#----------------------------------------------------------------------------- +class Crc: + '''Compute a Cyclic Redundancy Check (CRC) using the specified polynomial. + + Instances of this class have the same interface as the algorithms in the + hashlib module in the Python standard library. See the documentation of + this module for examples of how to use a Crc instance. + + The string representation of a Crc instance identifies the polynomial, + initial value, XOR out value, and the current CRC value. The print + statement can be used to output this information. + + If you need to generate a C/C++ function for use in another application, + use the generateCode method. If you need to generate code for another + language, subclass Crc and override the generateCode method. + + The following are the parameters supplied to the constructor. + + poly -- The generator polynomial to use in calculating the CRC. The value + is specified as a Python integer. The bits in this integer are the + coefficients of the polynomial. The only polynomials allowed are those + that generate 8, 16, 24, 32, or 64 bit CRCs. + + initCrc -- Initial value used to start the CRC calculation. This initial + value should be the initial shift register value XORed with the final XOR + value. That is equivalent to the CRC result the algorithm should return for + a zero-length string. Defaults to all bits set because that starting value + will take leading zero bytes into account. Starting with zero will ignore + all leading zero bytes. + + rev -- A flag that selects a bit reversed algorithm when True. Defaults to + True because the bit reversed algorithms are more efficient. + + xorOut -- Final value to XOR with the calculated CRC value. Used by some + CRC algorithms. Defaults to zero. + ''' + def __init__(self, poly, initCrc=~0, rev=True, xorOut=0, initialize=True): + if not initialize: + # Don't want to perform the initialization when using new or copy + # to create a new instance. + return + + (sizeBits, initCrc, xorOut) = _verifyParams(poly, initCrc, xorOut) + self.digest_size = sizeBits//8 + self.initCrc = initCrc + self.xorOut = xorOut + + self.poly = poly + self.reverse = rev + + (crcfun, table) = _mkCrcFun(poly, sizeBits, initCrc, rev, xorOut) + self._crc = crcfun + self.table = table + + self.crcValue = self.initCrc + + def __str__(self): + lst = [] + lst.append('poly = 0x%X' % self.poly) + lst.append('reverse = %s' % self.reverse) + fmt = '0x%%0%dX' % (self.digest_size*2) + lst.append('initCrc = %s' % (fmt % self.initCrc)) + lst.append('xorOut = %s' % (fmt % self.xorOut)) + lst.append('crcValue = %s' % (fmt % self.crcValue)) + return '\n'.join(lst) + + def new(self, arg=None): + '''Create a new instance of the Crc class initialized to the same + values as the original instance. The current CRC is set to the initial + value. If a string is provided in the optional arg parameter, it is + passed to the update method. + ''' + n = Crc(poly=None, initialize=False) + n._crc = self._crc + n.digest_size = self.digest_size + n.initCrc = self.initCrc + n.xorOut = self.xorOut + n.table = self.table + n.crcValue = self.initCrc + n.reverse = self.reverse + n.poly = self.poly + if arg is not None: + n.update(arg) + return n + + def copy(self): + '''Create a new instance of the Crc class initialized to the same + values as the original instance. The current CRC is set to the current + value. This allows multiple CRC calculations using a common initial + string. + ''' + c = self.new() + c.crcValue = self.crcValue + return c + + def update(self, data): + '''Update the current CRC value using the string specified as the data + parameter. + ''' + self.crcValue = self._crc(data, self.crcValue) + + def digest(self): + '''Return the current CRC value as a string of bytes. The length of + this string is specified in the digest_size attribute. + ''' + n = self.digest_size + crc = self.crcValue + lst = [] + while n > 0: + lst.append(crc & 0xFF) + crc = crc >> 8 + n -= 1 + lst.reverse() + return bytes(lst) + + def hexdigest(self): + '''Return the current CRC value as a string of hex digits. The length + of this string is twice the digest_size attribute. + ''' + n = self.digest_size + crc = self.crcValue + lst = [] + while n > 0: + lst.append('%02X' % (crc & 0xFF)) + crc = crc >> 8 + n -= 1 + lst.reverse() + return ''.join(lst) + + def generateCode(self, functionName, out, dataType=None, crcType=None): + '''Generate a C/C++ function. + + functionName -- String specifying the name of the function. + + out -- An open file-like object with a write method. This specifies + where the generated code is written. + + dataType -- An optional parameter specifying the data type of the input + data to the function. Defaults to UINT8. + + crcType -- An optional parameter specifying the data type of the CRC + value. Defaults to one of UINT8, UINT16, UINT32, or UINT64 depending + on the size of the CRC value. + ''' + if dataType is None: + dataType = 'UINT8' + + if crcType is None: + size = 8*self.digest_size + if size == 24: + size = 32 + crcType = 'UINT%d' % size + + if self.digest_size == 1: + # Both 8-bit CRC algorithms are the same + crcAlgor = 'table[*data ^ (%s)crc]' + elif self.reverse: + # The bit reverse algorithms are all the same except for the data + # type of the crc variable which is specified elsewhere. + crcAlgor = 'table[*data ^ (%s)crc] ^ (crc >> 8)' + else: + # The forward CRC algorithms larger than 8 bits have an extra shift + # operation to get the high byte. + shift = 8*(self.digest_size - 1) + crcAlgor = 'table[*data ^ (%%s)(crc >> %d)] ^ (crc << 8)' % shift + + fmt = '0x%%0%dX' % (2*self.digest_size) + if self.digest_size <= 4: + fmt = fmt + 'U,' + else: + # Need the long long type identifier to keep gcc from complaining. + fmt = fmt + 'ULL,' + + # Select the number of entries per row in the output code. + n = {1:8, 2:8, 3:4, 4:4, 8:2}[self.digest_size] + + lst = [] + for i, val in enumerate(self.table): + if (i % n) == 0: + lst.append('\n ') + lst.append(fmt % val) + + poly = 'polynomial: 0x%X' % self.poly + if self.reverse: + poly = poly + ', bit reverse algorithm' + + if self.xorOut: + # Need to remove the comma from the format. + preCondition = '\n crc = crc ^ %s;' % (fmt[:-1] % self.xorOut) + postCondition = preCondition + else: + preCondition = '' + postCondition = '' + + if self.digest_size == 3: + # The 24-bit CRC needs to be conditioned so that only 24-bits are + # used from the 32-bit variable. + if self.reverse: + preCondition += '\n crc = crc & 0xFFFFFFU;' + else: + postCondition += '\n crc = crc & 0xFFFFFFU;' + + + parms = { + 'dataType' : dataType, + 'crcType' : crcType, + 'name' : functionName, + 'crcAlgor' : crcAlgor % dataType, + 'crcTable' : ''.join(lst), + 'poly' : poly, + 'preCondition' : preCondition, + 'postCondition' : postCondition, + } + out.write(_codeTemplate % parms) + +#----------------------------------------------------------------------------- +def mkCrcFun(poly, initCrc=~0, rev=True, xorOut=0): + '''Return a function that computes the CRC using the specified polynomial. + + poly -- integer representation of the generator polynomial + initCrc -- default initial CRC value + rev -- when true, indicates that the data is processed bit reversed. + xorOut -- the final XOR value + + The returned function has the following user interface + def crcfun(data, crc=initCrc): + ''' + + # First we must verify the params + (sizeBits, initCrc, xorOut) = _verifyParams(poly, initCrc, xorOut) + # Make the function (and table), return the function + return _mkCrcFun(poly, sizeBits, initCrc, rev, xorOut)[0] + +#----------------------------------------------------------------------------- +# Naming convention: +# All function names ending with r are bit reverse variants of the ones +# without the r. + +#----------------------------------------------------------------------------- +# Check the polynomial to make sure that it is acceptable and return the number +# of bits in the CRC. + +def _verifyPoly(poly): + msg = 'The degree of the polynomial must be 8, 16, 24, 32 or 64' + for n in (8,16,24,32,64): + low = 1<> 1 + return y + +#----------------------------------------------------------------------------- +# The following functions compute the CRC for a single byte. These are used +# to build up the tables needed in the CRC algorithm. Assumes the high order +# bit of the polynomial has been stripped off. + +def _bytecrc(crc, poly, n): + mask = 1<<(n-1) + for i in range(8): + if crc & mask: + crc = (crc << 1) ^ poly + else: + crc = crc << 1 + mask = (1<> 1) ^ poly + else: + crc = crc >> 1 + mask = (1< 0) + { + crc = %(crcAlgor)s; + data++; + len--; + }%(postCondition)s + return crc; +} +''' + diff --git a/crcmod/predefined.py b/crcmod/predefined.py new file mode 100644 index 0000000..21e2b32 --- /dev/null +++ b/crcmod/predefined.py @@ -0,0 +1,162 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Craig McQueen +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- +''' +crcmod.predefined defines some well-known CRC algorithms. + +To use it, e.g.: + import crcmod.predefined + + crc32func = crcmod.predefined.mkPredefinedCrcFun("crc-32") + crc32class = crcmod.predefined.PredefinedCrc("crc-32") + +crcmod.predefined.Crc is an alias for crcmod.predefined.PredefinedCrc +But if doing 'from crc.predefined import *', only PredefinedCrc is imported. +''' + +# local imports +import crcmod + +__all__ = [ + 'PredefinedCrc', + 'mkPredefinedCrcFun', +] + +REVERSE = True +NON_REVERSE = False + +# The following table defines the parameters of well-known CRC algorithms. +# The "Check" value is the CRC for the ASCII byte sequence b"123456789". It +# can be used for unit tests. +_crc_definitions_table = [ +# Name Identifier-name, Poly Reverse Init-value XOR-out Check + [ 'crc-8', 'Crc8', 0x107, NON_REVERSE, 0x00, 0x00, 0xF4, ], + [ 'crc-8-darc', 'Crc8Darc', 0x139, REVERSE, 0x00, 0x00, 0x15, ], + [ 'crc-8-i-code', 'Crc8ICode', 0x11D, NON_REVERSE, 0xFD, 0x00, 0x7E, ], + [ 'crc-8-itu', 'Crc8Itu', 0x107, NON_REVERSE, 0x55, 0x55, 0xA1, ], + [ 'crc-8-maxim', 'Crc8Maxim', 0x131, REVERSE, 0x00, 0x00, 0xA1, ], + [ 'crc-8-rohc', 'Crc8Rohc', 0x107, REVERSE, 0xFF, 0x00, 0xD0, ], + [ 'crc-8-wcdma', 'Crc8Wcdma', 0x19B, REVERSE, 0x00, 0x00, 0x25, ], + + [ 'crc-16', 'Crc16', 0x18005, REVERSE, 0x0000, 0x0000, 0xBB3D, ], + [ 'crc-16-buypass', 'Crc16Buypass', 0x18005, NON_REVERSE, 0x0000, 0x0000, 0xFEE8, ], + [ 'crc-16-dds-110', 'Crc16Dds110', 0x18005, NON_REVERSE, 0x800D, 0x0000, 0x9ECF, ], + [ 'crc-16-dect', 'Crc16Dect', 0x10589, NON_REVERSE, 0x0001, 0x0001, 0x007E, ], + [ 'crc-16-dnp', 'Crc16Dnp', 0x13D65, REVERSE, 0xFFFF, 0xFFFF, 0xEA82, ], + [ 'crc-16-en-13757', 'Crc16En13757', 0x13D65, NON_REVERSE, 0xFFFF, 0xFFFF, 0xC2B7, ], + [ 'crc-16-genibus', 'Crc16Genibus', 0x11021, NON_REVERSE, 0x0000, 0xFFFF, 0xD64E, ], + [ 'crc-16-maxim', 'Crc16Maxim', 0x18005, REVERSE, 0xFFFF, 0xFFFF, 0x44C2, ], + [ 'crc-16-mcrf4xx', 'Crc16Mcrf4xx', 0x11021, REVERSE, 0xFFFF, 0x0000, 0x6F91, ], + [ 'crc-16-riello', 'Crc16Riello', 0x11021, REVERSE, 0x554D, 0x0000, 0x63D0, ], + [ 'crc-16-t10-dif', 'Crc16T10Dif', 0x18BB7, NON_REVERSE, 0x0000, 0x0000, 0xD0DB, ], + [ 'crc-16-teledisk', 'Crc16Teledisk', 0x1A097, NON_REVERSE, 0x0000, 0x0000, 0x0FB3, ], + [ 'crc-16-usb', 'Crc16Usb', 0x18005, REVERSE, 0x0000, 0xFFFF, 0xB4C8, ], + [ 'x-25', 'CrcX25', 0x11021, REVERSE, 0x0000, 0xFFFF, 0x906E, ], + [ 'xmodem', 'CrcXmodem', 0x11021, NON_REVERSE, 0x0000, 0x0000, 0x31C3, ], + [ 'modbus', 'CrcModbus', 0x18005, REVERSE, 0xFFFF, 0x0000, 0x4B37, ], + + # Note definitions of CCITT are disputable. See: + # http://homepages.tesco.net/~rainstorm/crc-catalogue.htm + # http://web.archive.org/web/20071229021252/http://www.joegeluso.com/software/articles/ccitt.htm + [ 'kermit', 'CrcKermit', 0x11021, REVERSE, 0x0000, 0x0000, 0x2189, ], + [ 'crc-ccitt-false', 'CrcCcittFalse', 0x11021, NON_REVERSE, 0xFFFF, 0x0000, 0x29B1, ], + [ 'crc-aug-ccitt', 'CrcAugCcitt', 0x11021, NON_REVERSE, 0x1D0F, 0x0000, 0xE5CC, ], + + [ 'crc-24', 'Crc24', 0x1864CFB, NON_REVERSE, 0xB704CE, 0x000000, 0x21CF02, ], + [ 'crc-24-flexray-a', 'Crc24FlexrayA', 0x15D6DCB, NON_REVERSE, 0xFEDCBA, 0x000000, 0x7979BD, ], + [ 'crc-24-flexray-b', 'Crc24FlexrayB', 0x15D6DCB, NON_REVERSE, 0xABCDEF, 0x000000, 0x1F23B8, ], + + [ 'crc-32', 'Crc32', 0x104C11DB7, REVERSE, 0x00000000, 0xFFFFFFFF, 0xCBF43926, ], + [ 'crc-32-bzip2', 'Crc32Bzip2', 0x104C11DB7, NON_REVERSE, 0x00000000, 0xFFFFFFFF, 0xFC891918, ], + [ 'crc-32c', 'Crc32C', 0x11EDC6F41, REVERSE, 0x00000000, 0xFFFFFFFF, 0xE3069283, ], + [ 'crc-32d', 'Crc32D', 0x1A833982B, REVERSE, 0x00000000, 0xFFFFFFFF, 0x87315576, ], + [ 'crc-32-mpeg', 'Crc32Mpeg', 0x104C11DB7, NON_REVERSE, 0xFFFFFFFF, 0x00000000, 0x0376E6E7, ], + [ 'posix', 'CrcPosix', 0x104C11DB7, NON_REVERSE, 0xFFFFFFFF, 0xFFFFFFFF, 0x765E7680, ], + [ 'crc-32q', 'Crc32Q', 0x1814141AB, NON_REVERSE, 0x00000000, 0x00000000, 0x3010BF7F, ], + [ 'jamcrc', 'CrcJamCrc', 0x104C11DB7, REVERSE, 0xFFFFFFFF, 0x00000000, 0x340BC6D9, ], + [ 'xfer', 'CrcXfer', 0x1000000AF, NON_REVERSE, 0x00000000, 0x00000000, 0xBD0BE338, ], + +# 64-bit +# Name Identifier-name, Poly Reverse Init-value XOR-out Check + [ 'crc-64', 'Crc64', 0x1000000000000001B, REVERSE, 0x0000000000000000, 0x0000000000000000, 0x46A5A9388A5BEFFE, ], + [ 'crc-64-we', 'Crc64We', 0x142F0E1EBA9EA3693, NON_REVERSE, 0x0000000000000000, 0xFFFFFFFFFFFFFFFF, 0x62EC59E3F1A4F00A, ], + [ 'crc-64-jones', 'Crc64Jones', 0x1AD93D23594C935A9, REVERSE, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000, 0xCAA717168609F281, ], +] + + +def _simplify_name(name): + """ + Reduce CRC definition name to a simplified form: + * lowercase + * dashes removed + * spaces removed + * any initial "CRC" string removed + """ + name = name.lower() + name = name.replace('-', '') + name = name.replace(' ', '') + if name.startswith('crc'): + name = name[len('crc'):] + return name + + +_crc_definitions_by_name = {} +_crc_definitions_by_identifier = {} +_crc_definitions = [] + +_crc_table_headings = [ 'name', 'identifier', 'poly', 'reverse', 'init', 'xor_out', 'check' ] + +for table_entry in _crc_definitions_table: + crc_definition = dict(zip(_crc_table_headings, table_entry)) + _crc_definitions.append(crc_definition) + name = _simplify_name(table_entry[0]) + if name in _crc_definitions_by_name: + raise Exception("Duplicate entry for '{0}' in CRC table".format(name)) + _crc_definitions_by_name[name] = crc_definition + _crc_definitions_by_identifier[table_entry[1]] = crc_definition + + +def _get_definition_by_name(crc_name): + definition = _crc_definitions_by_name.get(_simplify_name(crc_name), None) + if not definition: + definition = _crc_definitions_by_identifier.get(crc_name, None) + if not definition: + raise KeyError("Unkown CRC name '{0}'".format(crc_name)) + return definition + + +class PredefinedCrc(crcmod.Crc): + def __init__(self, crc_name): + definition = _get_definition_by_name(crc_name) + super().__init__(poly=definition['poly'], initCrc=definition['init'], rev=definition['reverse'], xorOut=definition['xor_out']) + + +# crcmod.predefined.Crc is an alias for crcmod.predefined.PredefinedCrc +Crc = PredefinedCrc + + +def mkPredefinedCrcFun(crc_name): + definition = _get_definition_by_name(crc_name) + return crcmod.mkCrcFun(poly=definition['poly'], initCrc=definition['init'], rev=definition['reverse'], xorOut=definition['xor_out']) + + +# crcmod.predefined.mkCrcFun is an alias for crcmod.predefined.mkPredefinedCrcFun +mkCrcFun = mkPredefinedCrcFun diff --git a/crcmod/test.py b/crcmod/test.py new file mode 100644 index 0000000..0190bb7 --- /dev/null +++ b/crcmod/test.py @@ -0,0 +1,540 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2010 Raymond L. Buvel +# Copyright (c) 2010 Craig McQueen +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +#----------------------------------------------------------------------------- +'''Unit tests for crcmod functionality''' + + +import unittest + +from array import array +import binascii + +from .crcmod import mkCrcFun, Crc +from .crcmod import _usingExtension +from .predefined import PredefinedCrc +from .predefined import mkPredefinedCrcFun +from .predefined import _crc_definitions as _predefined_crc_definitions + + +#----------------------------------------------------------------------------- +# This polynomial was chosen because it is the product of two irreducible +# polynomials. +# g8 = (x^7+x+1)*(x+1) +g8 = 0x185 + +#----------------------------------------------------------------------------- +# The following reproduces all of the entries in the Numerical Recipes table. +# This is the standard CCITT polynomial. +g16 = 0x11021 + +#----------------------------------------------------------------------------- +g24 = 0x15D6DCB + +#----------------------------------------------------------------------------- +# This is the standard AUTODIN-II polynomial which appears to be used in a +# wide variety of standards and applications. +g32 = 0x104C11DB7 + + +#----------------------------------------------------------------------------- +# I was able to locate a couple of 64-bit polynomials on the web. To make it +# easier to input the representation, define a function that builds a +# polynomial from a list of the bits that need to be turned on. + +def polyFromBits(bits): + p = 0 + for n in bits: + p = p | (1 << n) + return p + +# The following is from the paper "An Improved 64-bit Cyclic Redundancy Check +# for Protein Sequences" by David T. Jones + +g64a = polyFromBits([64, 63, 61, 59, 58, 56, 55, 52, 49, 48, 47, 46, 44, 41, + 37, 36, 34, 32, 31, 28, 26, 23, 22, 19, 16, 13, 12, 10, 9, 6, 4, + 3, 0]) + +# The following is from Standard ECMA-182 "Data Interchange on 12,7 mm 48-Track +# Magnetic Tape Cartridges -DLT1 Format-", December 1992. + +g64b = polyFromBits([64, 62, 57, 55, 54, 53, 52, 47, 46, 45, 40, 39, 38, 37, + 35, 33, 32, 31, 29, 27, 24, 23, 22, 21, 19, 17, 13, 12, 10, 9, 7, + 4, 1, 0]) + +#----------------------------------------------------------------------------- +# This class is used to check the CRC calculations against a direct +# implementation using polynomial division. + +class poly: + '''Class implementing polynomials over the field of integers mod 2''' + def __init__(self,p): + p = int(p) + if p < 0: raise ValueError('invalid polynomial') + self.p = p + + def __int__(self): + return self.p + + def __eq__(self,other): + return self.p == other.p + + def __ne__(self,other): + return self.p != other.p + + # To allow sorting of polynomials, use their long integer form for + # comparison + def __cmp__(self,other): + return cmp(self.p, other.p) + + def __bool__(self): + return self.p != 0 + + def __neg__(self): + return self # These polynomials are their own inverse under addition + + def __invert__(self): + n = max(self.deg() + 1, 1) + x = (1 << n) - 1 + return poly(self.p ^ x) + + def __add__(self,other): + return poly(self.p ^ other.p) + + def __sub__(self,other): + return poly(self.p ^ other.p) + + def __mul__(self,other): + a = self.p + b = other.p + if a == 0 or b == 0: return poly(0) + x = 0 + while b: + if b&1: + x = x ^ a + a = a<<1 + b = b>>1 + return poly(x) + + def __divmod__(self,other): + u = self.p + m = self.deg() + v = other.p + n = other.deg() + if v == 0: raise ZeroDivisionError('polynomial division by zero') + if n == 0: return (self,poly(0)) + if m < n: return (poly(0),self) + k = m-n + a = 1 << m + v = v << k + q = 0 + while k > 0: + if a & u: + u = u ^ v + q = q | 1 + q = q << 1 + a = a >> 1 + v = v >> 1 + k -= 1 + if a & u: + u = u ^ v + q = q | 1 + return (poly(q),poly(u)) + + def __div__(self,other): + return self.__divmod__(other)[0] + + def __mod__(self,other): + return self.__divmod__(other)[1] + + def __repr__(self): + return 'poly(0x%XL)' % self.p + + def __str__(self): + p = self.p + if p == 0: return '0' + lst = { 0:[], 1:['1'], 2:['x'], 3:['1','x'] }[p&3] + p = p>>2 + n = 2 + while p: + if p&1: lst.append('x^%d' % n) + p = p>>1 + n += 1 + lst.reverse() + return '+'.join(lst) + + def deg(self): + '''return the degree of the polynomial''' + a = self.p + if a == 0: return -1 + n = 0 + while a >= 0x10000: + n += 16 + a = a >> 16 + a = int(a) + while a > 1: + n += 1 + a = a >> 1 + return n + +#----------------------------------------------------------------------------- +# The following functions compute the CRC using direct polynomial division. +# These functions are checked against the result of the table driven +# algorithms. + +g8p = poly(g8) +x8p = poly(1<<8) +def crc8p(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x8p%g8p) + +g16p = poly(g16) +x16p = poly(1<<16) +def crc16p(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x16p%g16p) + +g24p = poly(g24) +x24p = poly(1<<24) +def crc24p(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x24p%g24p) + +g32p = poly(g32) +x32p = poly(1<<32) +def crc32p(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x32p%g32p) + +g64ap = poly(g64a) +x64p = poly(1<<64) +def crc64ap(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x64p%g64ap) + +g64bp = poly(g64b) +def crc64bp(d): + p = 0 + for i in d: + p = p*256 + i + p = poly(p) + return int(p*x64p%g64bp) + + +class KnownAnswerTests(unittest.TestCase): + test_messages = [ + b'T', + b'CatMouse987654321', + ] + + known_answers = [ + [ (g8,0,0), (0xFE, 0x9D) ], + [ (g8,-1,1), (0x4F, 0x9B) ], + [ (g8,0,1), (0xFE, 0x62) ], + [ (g16,0,0), (0x1A71, 0xE556) ], + [ (g16,-1,1), (0x1B26, 0xF56E) ], + [ (g16,0,1), (0x14A1, 0xC28D) ], + [ (g24,0,0), (0xBCC49D, 0xC4B507) ], + [ (g24,-1,1), (0x59BD0E, 0x0AAA37) ], + [ (g24,0,1), (0xD52B0F, 0x1523AB) ], + [ (g32,0,0), (0x6B93DDDB, 0x12DCA0F4) ], + [ (g32,0xFFFFFFFF,1), (0x41FB859F, 0xF7B400A7) ], + [ (g32,0,1), (0x6C0695ED, 0xC1A40EE5) ], + [ (g32,0,1,0xFFFFFFFF), (0xBE047A60, 0x084BFF58) ], + ] + + def test_known_answers(self): + for crcfun_params, v in self.known_answers: + crcfun = mkCrcFun(*crcfun_params) + self.assertEqual(crcfun(b'',0), 0, "Wrong answer for CRC parameters %s, input ''" % (crcfun_params,)) + for i, msg in enumerate(self.test_messages): + self.assertEqual(crcfun(msg), v[i], "Wrong answer for CRC parameters %s, input '%s'" % (crcfun_params,msg)) + self.assertEqual(crcfun(msg[4:], crcfun(msg[:4])), v[i], "Wrong answer for CRC parameters %s, input '%s'" % (crcfun_params,msg)) + self.assertEqual(crcfun(msg[-1:], crcfun(msg[:-1])), v[i], "Wrong answer for CRC parameters %s, input '%s'" % (crcfun_params,msg)) + + +class CompareReferenceCrcTest(unittest.TestCase): + test_messages = [ + b'', + b'T', + b'123456789', + b'CatMouse987654321', + ] + + test_poly_crcs = [ + [ (g8,0,0), crc8p ], + [ (g16,0,0), crc16p ], + [ (g24,0,0), crc24p ], + [ (g32,0,0), crc32p ], + [ (g64a,0,0), crc64ap ], + [ (g64b,0,0), crc64bp ], + ] + + @staticmethod + def reference_crc32(d, crc=0): + """This function modifies the return value of binascii.crc32 + to be an unsigned 32-bit value. I.e. in the range 0 to 2**32-1.""" + # Work around the future warning on constants. + if crc > 0x7FFFFFFF: + x = int(crc & 0x7FFFFFFF) + crc = x | -2147483648 + x = binascii.crc32(d,crc) + return int(x) & 0xFFFFFFFF + + def test_compare_crc32(self): + """The binascii module has a 32-bit CRC function that is used in a wide range + of applications including the checksum used in the ZIP file format. + This test compares the CRC-32 implementation of this crcmod module to + that of binascii.crc32.""" + # The following function should produce the same result as + # self.reference_crc32 which is derived from binascii.crc32. + crc32 = mkCrcFun(g32,0,1,0xFFFFFFFF) + + for msg in self.test_messages: + self.assertEqual(crc32(msg), self.reference_crc32(msg)) + + def test_compare_poly(self): + """Compare various CRCs of this crcmod module to a pure + polynomial-based implementation.""" + for crcfun_params, crc_poly_fun in self.test_poly_crcs: + # The following function should produce the same result as + # the associated polynomial CRC function. + crcfun = mkCrcFun(*crcfun_params) + + for msg in self.test_messages: + self.assertEqual(crcfun(msg), crc_poly_fun(msg)) + + +class CrcClassTest(unittest.TestCase): + """Verify the Crc class""" + + msg = b'CatMouse987654321' + + def test_simple_crc32_class(self): + """Verify the CRC class when not using xorOut""" + crc = Crc(g32) + + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0xFFFFFFFF +xorOut = 0x00000000 +crcValue = 0xFFFFFFFF''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(crc.digest(), b'\xff\xff\xff\xff') + self.assertEqual(crc.hexdigest(), 'FFFFFFFF') + + crc.update(self.msg) + self.assertEqual(crc.crcValue, 0xF7B400A7) + self.assertEqual(crc.digest(), b'\xf7\xb4\x00\xa7') + self.assertEqual(crc.hexdigest(), 'F7B400A7') + + # Verify the .copy() method + x = crc.copy() + self.assertTrue(x is not crc) + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0xFFFFFFFF +xorOut = 0x00000000 +crcValue = 0xF7B400A7''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(str(x), str_rep) + + def test_full_crc32_class(self): + """Verify the CRC class when using xorOut""" + + crc = Crc(g32, initCrc=0, xorOut= ~0) + + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0x00000000 +xorOut = 0xFFFFFFFF +crcValue = 0x00000000''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(crc.digest(), b'\x00\x00\x00\x00') + self.assertEqual(crc.hexdigest(), '00000000') + + crc.update(self.msg) + self.assertEqual(crc.crcValue, 0x84BFF58) + self.assertEqual(crc.digest(), b'\x08\x4b\xff\x58') + self.assertEqual(crc.hexdigest(), '084BFF58') + + # Verify the .copy() method + x = crc.copy() + self.assertTrue(x is not crc) + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0x00000000 +xorOut = 0xFFFFFFFF +crcValue = 0x084BFF58''' + self.assertEqual(str(crc), str_rep) + self.assertEqual(str(x), str_rep) + + # Verify the .new() method + y = crc.new() + self.assertTrue(y is not crc) + self.assertTrue(y is not x) + str_rep = \ +'''poly = 0x104C11DB7 +reverse = True +initCrc = 0x00000000 +xorOut = 0xFFFFFFFF +crcValue = 0x00000000''' + self.assertEqual(str(y), str_rep) + + +class PredefinedCrcTest(unittest.TestCase): + """Verify the predefined CRCs""" + + test_messages_for_known_answers = [ + b'', # Test cases below depend on this first entry being the empty string. + b'T', + b'CatMouse987654321', + ] + + known_answers = [ + [ 'crc-aug-ccitt', (0x1D0F, 0xD6ED, 0x5637) ], + [ 'x-25', (0x0000, 0xE4D9, 0x0A91) ], + [ 'crc-32', (0x00000000, 0xBE047A60, 0x084BFF58) ], + ] + + def test_known_answers(self): + for crcfun_name, v in self.known_answers: + crcfun = mkPredefinedCrcFun(crcfun_name) + self.assertEqual(crcfun(b'',0), 0, "Wrong answer for CRC '%s', input ''" % crcfun_name) + for i, msg in enumerate(self.test_messages_for_known_answers): + self.assertEqual(crcfun(msg), v[i], "Wrong answer for CRC %s, input '%s'" % (crcfun_name,msg)) + self.assertEqual(crcfun(msg[4:], crcfun(msg[:4])), v[i], "Wrong answer for CRC %s, input '%s'" % (crcfun_name,msg)) + self.assertEqual(crcfun(msg[-1:], crcfun(msg[:-1])), v[i], "Wrong answer for CRC %s, input '%s'" % (crcfun_name,msg)) + + def test_class_with_known_answers(self): + for crcfun_name, v in self.known_answers: + for i, msg in enumerate(self.test_messages_for_known_answers): + crc1 = PredefinedCrc(crcfun_name) + crc1.update(msg) + self.assertEqual(crc1.crcValue, v[i], "Wrong answer for crc1 %s, input '%s'" % (crcfun_name,msg)) + + crc2 = crc1.new() + # Check that crc1 maintains its same value, after .new() call. + self.assertEqual(crc1.crcValue, v[i], "Wrong state for crc1 %s, input '%s'" % (crcfun_name,msg)) + # Check that the new class instance created by .new() contains the initialisation value. + # This depends on the first string in self.test_messages_for_known_answers being + # the empty string. + self.assertEqual(crc2.crcValue, v[0], "Wrong state for crc2 %s, input '%s'" % (crcfun_name,msg)) + + crc2.update(msg) + # Check that crc1 maintains its same value, after crc2 has called .update() + self.assertEqual(crc1.crcValue, v[i], "Wrong state for crc1 %s, input '%s'" % (crcfun_name,msg)) + # Check that crc2 contains the right value after calling .update() + self.assertEqual(crc2.crcValue, v[i], "Wrong state for crc2 %s, input '%s'" % (crcfun_name,msg)) + + def test_function_predefined_table(self): + for table_entry in _predefined_crc_definitions: + # Check predefined function + crc_func = mkPredefinedCrcFun(table_entry['name']) + calc_value = crc_func(b"123456789") + self.assertEqual(calc_value, table_entry['check'], "Wrong answer for CRC '%s'" % table_entry['name']) + + def test_class_predefined_table(self): + for table_entry in _predefined_crc_definitions: + # Check predefined class + crc1 = PredefinedCrc(table_entry['name']) + crc1.update(b"123456789") + self.assertEqual(crc1.crcValue, table_entry['check'], "Wrong answer for CRC '%s'" % table_entry['name']) + + +class InputTypesTest(unittest.TestCase): + """Check the various input types that CRC functions can accept.""" + + msg = b'CatMouse987654321' + + check_crc_names = [ + 'crc-aug-ccitt', + 'x-25', + 'crc-32', + ] + + array_check_types = [ + 'B', + 'H', + 'I', + 'L', + ] + + def test_bytearray_input(self): + """Test that bytearray inputs are accepted, as an example + of a type that implements the buffer protocol.""" + for crc_name in self.check_crc_names: + crcfun = mkPredefinedCrcFun(crc_name) + for i in range(len(self.msg) + 1): + test_msg = self.msg[:i] + bytes_answer = crcfun(test_msg) + bytearray_answer = crcfun(bytearray(test_msg)) + self.assertEqual(bytes_answer, bytearray_answer) + + def test_array_input(self): + """Test that array inputs are accepted, as an example + of a type that implements the buffer protocol.""" + for crc_name in self.check_crc_names: + crcfun = mkPredefinedCrcFun(crc_name) + for i in range(len(self.msg) + 1): + test_msg = self.msg[:i] + bytes_answer = crcfun(test_msg) + for array_type in self.array_check_types: + if i % array(array_type).itemsize == 0: + test_array = array(array_type, test_msg) + array_answer = crcfun(test_array) + self.assertEqual(bytes_answer, array_answer) + + def test_unicode_input(self): + """Test that Unicode input raises TypeError""" + for crc_name in self.check_crc_names: + crcfun = mkPredefinedCrcFun(crc_name) + with self.assertRaises(TypeError): + crcfun("123456789") + + +def runtests(): + print("Using extension:", _usingExtension) + print() + unittest.main() + + +if __name__ == '__main__': + runtests() diff --git a/dateutil/__init__.py b/dateutil/__init__.py new file mode 100644 index 0000000..a2c19c0 --- /dev/null +++ b/dateutil/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +import sys + +try: + from ._version import version as __version__ +except ImportError: + __version__ = 'unknown' + +__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz', + 'utils', 'zoneinfo'] + +def __getattr__(name): + import importlib + + if name in __all__: + return importlib.import_module("." + name, __name__) + raise AttributeError( + "module {!r} has not attribute {!r}".format(__name__, name) + ) + + +def __dir__(): + # __dir__ should include all the lazy-importable modules as well. + return [x for x in globals() if x not in sys.modules] + __all__ diff --git a/dateutil/_common.py b/dateutil/_common.py new file mode 100644 index 0000000..4eb2659 --- /dev/null +++ b/dateutil/_common.py @@ -0,0 +1,43 @@ +""" +Common code used in multiple modules. +""" + + +class weekday(object): + __slots__ = ["weekday", "n"] + + def __init__(self, weekday, n=None): + self.weekday = weekday + self.n = n + + def __call__(self, n): + if n == self.n: + return self + else: + return self.__class__(self.weekday, n) + + def __eq__(self, other): + try: + if self.weekday != other.weekday or self.n != other.n: + return False + except AttributeError: + return False + return True + + def __hash__(self): + return hash(( + self.weekday, + self.n, + )) + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] + if not self.n: + return s + else: + return "%s(%+d)" % (s, self.n) + +# vim:ts=4:sw=4:et diff --git a/dateutil/_version.py b/dateutil/_version.py new file mode 100644 index 0000000..ddda980 --- /dev/null +++ b/dateutil/_version.py @@ -0,0 +1,4 @@ +# file generated by setuptools_scm +# don't change, don't track in version control +__version__ = version = '2.9.0.post0' +__version_tuple__ = version_tuple = (2, 9, 0) diff --git a/dateutil/easter.py b/dateutil/easter.py new file mode 100644 index 0000000..f74d1f7 --- /dev/null +++ b/dateutil/easter.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +This module offers a generic Easter computing method for any given year, using +Western, Orthodox or Julian algorithms. +""" + +import datetime + +__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] + +EASTER_JULIAN = 1 +EASTER_ORTHODOX = 2 +EASTER_WESTERN = 3 + + +def easter(year, method=EASTER_WESTERN): + """ + This method was ported from the work done by GM Arts, + on top of the algorithm by Claus Tondering, which was + based in part on the algorithm of Ouding (1940), as + quoted in "Explanatory Supplement to the Astronomical + Almanac", P. Kenneth Seidelmann, editor. + + This algorithm implements three different Easter + calculation methods: + + 1. Original calculation in Julian calendar, valid in + dates after 326 AD + 2. Original method, with date converted to Gregorian + calendar, valid in years 1583 to 4099 + 3. Revised method, in Gregorian calendar, valid in + years 1583 to 4099 as well + + These methods are represented by the constants: + + * ``EASTER_JULIAN = 1`` + * ``EASTER_ORTHODOX = 2`` + * ``EASTER_WESTERN = 3`` + + The default method is method 3. + + More about the algorithm may be found at: + + `GM Arts: Easter Algorithms `_ + + and + + `The Calendar FAQ: Easter `_ + + """ + + if not (1 <= method <= 3): + raise ValueError("invalid method") + + # g - Golden year - 1 + # c - Century + # h - (23 - Epact) mod 30 + # i - Number of days from March 21 to Paschal Full Moon + # j - Weekday for PFM (0=Sunday, etc) + # p - Number of days from March 21 to Sunday on or before PFM + # (-6 to 28 methods 1 & 3, to 56 for method 2) + # e - Extra days to add for method 2 (converting Julian + # date to Gregorian date) + + y = year + g = y % 19 + e = 0 + if method < 3: + # Old method + i = (19*g + 15) % 30 + j = (y + y//4 + i) % 7 + if method == 2: + # Extra dates to convert Julian to Gregorian date + e = 10 + if y > 1600: + e = e + y//100 - 16 - (y//100 - 16)//4 + else: + # New method + c = y//100 + h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30 + i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11)) + j = (y + y//4 + i + 2 - c + c//4) % 7 + + # p can be from -6 to 56 corresponding to dates 22 March to 23 May + # (later dates apply to method 2, although 23 May never actually occurs) + p = i - j + e + d = 1 + (p + 27 + (p + 6)//40) % 31 + m = 3 + (p + 26)//30 + return datetime.date(int(y), int(m), int(d)) diff --git a/dateutil/parser/__init__.py b/dateutil/parser/__init__.py new file mode 100644 index 0000000..d174b0e --- /dev/null +++ b/dateutil/parser/__init__.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from ._parser import parse, parser, parserinfo, ParserError +from ._parser import DEFAULTPARSER, DEFAULTTZPARSER +from ._parser import UnknownTimezoneWarning + +from ._parser import __doc__ + +from .isoparser import isoparser, isoparse + +__all__ = ['parse', 'parser', 'parserinfo', + 'isoparse', 'isoparser', + 'ParserError', + 'UnknownTimezoneWarning'] + + +### +# Deprecate portions of the private interface so that downstream code that +# is improperly relying on it is given *some* notice. + + +def __deprecated_private_func(f): + from functools import wraps + import warnings + + msg = ('{name} is a private function and may break without warning, ' + 'it will be moved and or renamed in future versions.') + msg = msg.format(name=f.__name__) + + @wraps(f) + def deprecated_func(*args, **kwargs): + warnings.warn(msg, DeprecationWarning) + return f(*args, **kwargs) + + return deprecated_func + +def __deprecate_private_class(c): + import warnings + + msg = ('{name} is a private class and may break without warning, ' + 'it will be moved and or renamed in future versions.') + msg = msg.format(name=c.__name__) + + class private_class(c): + __doc__ = c.__doc__ + + def __init__(self, *args, **kwargs): + warnings.warn(msg, DeprecationWarning) + super(private_class, self).__init__(*args, **kwargs) + + private_class.__name__ = c.__name__ + + return private_class + + +from ._parser import _timelex, _resultbase +from ._parser import _tzparser, _parsetz + +_timelex = __deprecate_private_class(_timelex) +_tzparser = __deprecate_private_class(_tzparser) +_resultbase = __deprecate_private_class(_resultbase) +_parsetz = __deprecated_private_func(_parsetz) diff --git a/dateutil/parser/_parser.py b/dateutil/parser/_parser.py new file mode 100644 index 0000000..37d1663 --- /dev/null +++ b/dateutil/parser/_parser.py @@ -0,0 +1,1613 @@ +# -*- coding: utf-8 -*- +""" +This module offers a generic date/time string parser which is able to parse +most known formats to represent a date and/or time. + +This module attempts to be forgiving with regards to unlikely input formats, +returning a datetime object even for dates which are ambiguous. If an element +of a date/time stamp is omitted, the following rules are applied: + +- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour + on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is + specified. +- If a time zone is omitted, a timezone-naive datetime is returned. + +If any other elements are missing, they are taken from the +:class:`datetime.datetime` object passed to the parameter ``default``. If this +results in a day number exceeding the valid number of days per month, the +value falls back to the end of the month. + +Additional resources about date/time string formats can be found below: + +- `A summary of the international standard date and time notation + `_ +- `W3C Date and Time Formats `_ +- `Time Formats (Planetary Rings Node) `_ +- `CPAN ParseDate module + `_ +- `Java SimpleDateFormat Class + `_ +""" +from __future__ import unicode_literals + +import datetime +import re +import string +import time +import warnings + +from calendar import monthrange +from io import StringIO + +import six +from six import integer_types, text_type + +from decimal import Decimal + +from warnings import warn + +from .. import relativedelta +from .. import tz + +__all__ = ["parse", "parserinfo", "ParserError"] + + +# TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth +# making public and/or figuring out if there is something we can +# take off their plate. +class _timelex(object): + # Fractional seconds are sometimes split by a comma + _split_decimal = re.compile("([.,])") + + def __init__(self, instream): + if isinstance(instream, (bytes, bytearray)): + instream = instream.decode() + + if isinstance(instream, text_type): + instream = StringIO(instream) + elif getattr(instream, 'read', None) is None: + raise TypeError('Parser must be a string or character stream, not ' + '{itype}'.format(itype=instream.__class__.__name__)) + + self.instream = instream + self.charstack = [] + self.tokenstack = [] + self.eof = False + + def get_token(self): + """ + This function breaks the time string into lexical units (tokens), which + can be parsed by the parser. Lexical units are demarcated by changes in + the character set, so any continuous string of letters is considered + one unit, any continuous string of numbers is considered one unit. + + The main complication arises from the fact that dots ('.') can be used + both as separators (e.g. "Sep.20.2009") or decimal points (e.g. + "4:30:21.447"). As such, it is necessary to read the full context of + any dot-separated strings before breaking it into tokens; as such, this + function maintains a "token stack", for when the ambiguous context + demands that multiple tokens be parsed at once. + """ + if self.tokenstack: + return self.tokenstack.pop(0) + + seenletters = False + token = None + state = None + + while not self.eof: + # We only realize that we've reached the end of a token when we + # find a character that's not part of the current token - since + # that character may be part of the next token, it's stored in the + # charstack. + if self.charstack: + nextchar = self.charstack.pop(0) + else: + nextchar = self.instream.read(1) + while nextchar == '\x00': + nextchar = self.instream.read(1) + + if not nextchar: + self.eof = True + break + elif not state: + # First character of the token - determines if we're starting + # to parse a word, a number or something else. + token = nextchar + if self.isword(nextchar): + state = 'a' + elif self.isnum(nextchar): + state = '0' + elif self.isspace(nextchar): + token = ' ' + break # emit token + else: + break # emit token + elif state == 'a': + # If we've already started reading a word, we keep reading + # letters until we find something that's not part of a word. + seenletters = True + if self.isword(nextchar): + token += nextchar + elif nextchar == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0': + # If we've already started reading a number, we keep reading + # numbers until we find something that doesn't fit. + if self.isnum(nextchar): + token += nextchar + elif nextchar == '.' or (nextchar == ',' and len(token) >= 2): + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == 'a.': + # If we've seen some letters and a dot separator, continue + # parsing, and the tokens will be broken up later. + seenletters = True + if nextchar == '.' or self.isword(nextchar): + token += nextchar + elif self.isnum(nextchar) and token[-1] == '.': + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0.': + # If we've seen at least one dot separator, keep going, we'll + # break up the tokens later. + if nextchar == '.' or self.isnum(nextchar): + token += nextchar + elif self.isword(nextchar) and token[-1] == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + + if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or + token[-1] in '.,')): + l = self._split_decimal.split(token) + token = l[0] + for tok in l[1:]: + if tok: + self.tokenstack.append(tok) + + if state == '0.' and token.count('.') == 0: + token = token.replace(',', '.') + + return token + + def __iter__(self): + return self + + def __next__(self): + token = self.get_token() + if token is None: + raise StopIteration + + return token + + def next(self): + return self.__next__() # Python 2.x support + + @classmethod + def split(cls, s): + return list(cls(s)) + + @classmethod + def isword(cls, nextchar): + """ Whether or not the next character is part of a word """ + return nextchar.isalpha() + + @classmethod + def isnum(cls, nextchar): + """ Whether the next character is part of a number """ + return nextchar.isdigit() + + @classmethod + def isspace(cls, nextchar): + """ Whether the next character is whitespace """ + return nextchar.isspace() + + +class _resultbase(object): + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def _repr(self, classname): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (classname, ", ".join(l)) + + def __len__(self): + return (sum(getattr(self, attr) is not None + for attr in self.__slots__)) + + def __repr__(self): + return self._repr(self.__class__.__name__) + + +class parserinfo(object): + """ + Class which handles what inputs are accepted. Subclass this to customize + the language and acceptable values for each parameter. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. Default is ``False``. + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + Default is ``False``. + """ + + # m from a.m/p.m, t from ISO T separator + JUMP = [" ", ".", ",", ";", "-", "/", "'", + "at", "on", "and", "ad", "m", "t", "of", + "st", "nd", "rd", "th"] + + WEEKDAYS = [("Mon", "Monday"), + ("Tue", "Tuesday"), # TODO: "Tues" + ("Wed", "Wednesday"), + ("Thu", "Thursday"), # TODO: "Thurs" + ("Fri", "Friday"), + ("Sat", "Saturday"), + ("Sun", "Sunday")] + MONTHS = [("Jan", "January"), + ("Feb", "February"), # TODO: "Febr" + ("Mar", "March"), + ("Apr", "April"), + ("May", "May"), + ("Jun", "June"), + ("Jul", "July"), + ("Aug", "August"), + ("Sep", "Sept", "September"), + ("Oct", "October"), + ("Nov", "November"), + ("Dec", "December")] + HMS = [("h", "hour", "hours"), + ("m", "minute", "minutes"), + ("s", "second", "seconds")] + AMPM = [("am", "a"), + ("pm", "p")] + UTCZONE = ["UTC", "GMT", "Z", "z"] + PERTAIN = ["of"] + TZOFFSET = {} + # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate", + # "Anno Domini", "Year of Our Lord"] + + def __init__(self, dayfirst=False, yearfirst=False): + self._jump = self._convert(self.JUMP) + self._weekdays = self._convert(self.WEEKDAYS) + self._months = self._convert(self.MONTHS) + self._hms = self._convert(self.HMS) + self._ampm = self._convert(self.AMPM) + self._utczone = self._convert(self.UTCZONE) + self._pertain = self._convert(self.PERTAIN) + + self.dayfirst = dayfirst + self.yearfirst = yearfirst + + self._year = time.localtime().tm_year + self._century = self._year // 100 * 100 + + def _convert(self, lst): + dct = {} + for i, v in enumerate(lst): + if isinstance(v, tuple): + for v in v: + dct[v.lower()] = i + else: + dct[v.lower()] = i + return dct + + def jump(self, name): + return name.lower() in self._jump + + def weekday(self, name): + try: + return self._weekdays[name.lower()] + except KeyError: + pass + return None + + def month(self, name): + try: + return self._months[name.lower()] + 1 + except KeyError: + pass + return None + + def hms(self, name): + try: + return self._hms[name.lower()] + except KeyError: + return None + + def ampm(self, name): + try: + return self._ampm[name.lower()] + except KeyError: + return None + + def pertain(self, name): + return name.lower() in self._pertain + + def utczone(self, name): + return name.lower() in self._utczone + + def tzoffset(self, name): + if name in self._utczone: + return 0 + + return self.TZOFFSET.get(name) + + def convertyear(self, year, century_specified=False): + """ + Converts two-digit years to year within [-50, 49] + range of self._year (current local time) + """ + + # Function contract is that the year is always positive + assert year >= 0 + + if year < 100 and not century_specified: + # assume current century to start + year += self._century + + if year >= self._year + 50: # if too far in future + year -= 100 + elif year < self._year - 50: # if too far in past + year += 100 + + return year + + def validate(self, res): + # move to info + if res.year is not None: + res.year = self.convertyear(res.year, res.century_specified) + + if ((res.tzoffset == 0 and not res.tzname) or + (res.tzname == 'Z' or res.tzname == 'z')): + res.tzname = "UTC" + res.tzoffset = 0 + elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): + res.tzoffset = 0 + return True + + +class _ymd(list): + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.century_specified = False + self.dstridx = None + self.mstridx = None + self.ystridx = None + + @property + def has_year(self): + return self.ystridx is not None + + @property + def has_month(self): + return self.mstridx is not None + + @property + def has_day(self): + return self.dstridx is not None + + def could_be_day(self, value): + if self.has_day: + return False + elif not self.has_month: + return 1 <= value <= 31 + elif not self.has_year: + # Be permissive, assume leap year + month = self[self.mstridx] + return 1 <= value <= monthrange(2000, month)[1] + else: + month = self[self.mstridx] + year = self[self.ystridx] + return 1 <= value <= monthrange(year, month)[1] + + def append(self, val, label=None): + if hasattr(val, '__len__'): + if val.isdigit() and len(val) > 2: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + elif val > 100: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + + super(self.__class__, self).append(int(val)) + + if label == 'M': + if self.has_month: + raise ValueError('Month is already set') + self.mstridx = len(self) - 1 + elif label == 'D': + if self.has_day: + raise ValueError('Day is already set') + self.dstridx = len(self) - 1 + elif label == 'Y': + if self.has_year: + raise ValueError('Year is already set') + self.ystridx = len(self) - 1 + + def _resolve_from_stridxs(self, strids): + """ + Try to resolve the identities of year/month/day elements using + ystridx, mstridx, and dstridx, if enough of these are specified. + """ + if len(self) == 3 and len(strids) == 2: + # we can back out the remaining stridx value + missing = [x for x in range(3) if x not in strids.values()] + key = [x for x in ['y', 'm', 'd'] if x not in strids] + assert len(missing) == len(key) == 1 + key = key[0] + val = missing[0] + strids[key] = val + + assert len(self) == len(strids) # otherwise this should not be called + out = {key: self[strids[key]] for key in strids} + return (out.get('y'), out.get('m'), out.get('d')) + + def resolve_ymd(self, yearfirst, dayfirst): + len_ymd = len(self) + year, month, day = (None, None, None) + + strids = (('y', self.ystridx), + ('m', self.mstridx), + ('d', self.dstridx)) + + strids = {key: val for key, val in strids if val is not None} + if (len(self) == len(strids) > 0 or + (len(self) == 3 and len(strids) == 2)): + return self._resolve_from_stridxs(strids) + + mstridx = self.mstridx + + if len_ymd > 3: + raise ValueError("More than three YMD values") + elif len_ymd == 1 or (mstridx is not None and len_ymd == 2): + # One member, or two members with a month string + if mstridx is not None: + month = self[mstridx] + # since mstridx is 0 or 1, self[mstridx-1] always + # looks up the other element + other = self[mstridx - 1] + else: + other = self[0] + + if len_ymd > 1 or mstridx is None: + if other > 31: + year = other + else: + day = other + + elif len_ymd == 2: + # Two members with numbers + if self[0] > 31: + # 99-01 + year, month = self + elif self[1] > 31: + # 01-99 + month, year = self + elif dayfirst and self[1] <= 12: + # 13-01 + day, month = self + else: + # 01-13 + month, day = self + + elif len_ymd == 3: + # Three members + if mstridx == 0: + if self[1] > 31: + # Apr-2003-25 + month, year, day = self + else: + month, day, year = self + elif mstridx == 1: + if self[0] > 31 or (yearfirst and self[2] <= 31): + # 99-Jan-01 + year, month, day = self + else: + # 01-Jan-01 + # Give precedence to day-first, since + # two-digit years is usually hand-written. + day, month, year = self + + elif mstridx == 2: + # WTF!? + if self[1] > 31: + # 01-99-Jan + day, year, month = self + else: + # 99-01-Jan + year, day, month = self + + else: + if (self[0] > 31 or + self.ystridx == 0 or + (yearfirst and self[1] <= 12 and self[2] <= 31)): + # 99-01-01 + if dayfirst and self[2] <= 12: + year, day, month = self + else: + year, month, day = self + elif self[0] > 12 or (dayfirst and self[1] <= 12): + # 13-01-01 + day, month, year = self + else: + # 01-13-01 + month, day, year = self + + return year, month, day + + +class parser(object): + def __init__(self, info=None): + self.info = info or parserinfo() + + def parse(self, timestr, default=None, + ignoretz=False, tzinfos=None, **kwargs): + """ + Parse the date/time string into a :class:`datetime.datetime` object. + + :param timestr: + Any date/time string using the supported formats. + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a + naive :class:`datetime.datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param \\*\\*kwargs: + Keyword arguments as passed to ``_parse()``. + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ParserError: + Raised for invalid or unknown string format, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date + would be created. + + :raises TypeError: + Raised for non-string or character stream input. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + + if default is None: + default = datetime.datetime.now().replace(hour=0, minute=0, + second=0, microsecond=0) + + res, skipped_tokens = self._parse(timestr, **kwargs) + + if res is None: + raise ParserError("Unknown string format: %s", timestr) + + if len(res) == 0: + raise ParserError("String does not contain a date: %s", timestr) + + try: + ret = self._build_naive(res, default) + except ValueError as e: + six.raise_from(ParserError(str(e) + ": %s", timestr), e) + + if not ignoretz: + ret = self._build_tzaware(ret, res, tzinfos) + + if kwargs.get('fuzzy_with_tokens', False): + return ret, skipped_tokens + else: + return ret + + class _result(_resultbase): + __slots__ = ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond", + "tzname", "tzoffset", "ampm","any_unused_tokens"] + + def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False, + fuzzy_with_tokens=False): + """ + Private method which performs the heavy lifting of parsing, called from + ``parse()``, which passes on its ``kwargs`` to this function. + + :param timestr: + The string to parse. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. If set to ``None``, this value is retrieved from the + current :class:`parserinfo` object (which itself defaults to + ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + If this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + """ + if fuzzy_with_tokens: + fuzzy = True + + info = self.info + + if dayfirst is None: + dayfirst = info.dayfirst + + if yearfirst is None: + yearfirst = info.yearfirst + + res = self._result() + l = _timelex.split(timestr) # Splits the timestr into tokens + + skipped_idxs = [] + + # year/month/day list + ymd = _ymd() + + len_l = len(l) + i = 0 + try: + while i < len_l: + + # Check if it's a number + value_repr = l[i] + try: + value = float(value_repr) + except ValueError: + value = None + + if value is not None: + # Numeric token + i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy) + + # Check weekday + elif info.weekday(l[i]) is not None: + value = info.weekday(l[i]) + res.weekday = value + + # Check month name + elif info.month(l[i]) is not None: + value = info.month(l[i]) + ymd.append(value, 'M') + + if i + 1 < len_l: + if l[i + 1] in ('-', '/'): + # Jan-01[-99] + sep = l[i + 1] + ymd.append(l[i + 2]) + + if i + 3 < len_l and l[i + 3] == sep: + # Jan-01-99 + ymd.append(l[i + 4]) + i += 2 + + i += 2 + + elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and + info.pertain(l[i + 2])): + # Jan of 01 + # In this case, 01 is clearly year + if l[i + 4].isdigit(): + # Convert it here to become unambiguous + value = int(l[i + 4]) + year = str(info.convertyear(value)) + ymd.append(year, 'Y') + else: + # Wrong guess + pass + # TODO: not hit in tests + i += 4 + + # Check am/pm + elif info.ampm(l[i]) is not None: + value = info.ampm(l[i]) + val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy) + + if val_is_ampm: + res.hour = self._adjust_ampm(res.hour, value) + res.ampm = value + + elif fuzzy: + skipped_idxs.append(i) + + # Check for a timezone name + elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]): + res.tzname = l[i] + res.tzoffset = info.tzoffset(res.tzname) + + # Check for something like GMT+3, or BRST+3. Notice + # that it doesn't mean "I am 3 hours after GMT", but + # "my time +3 is GMT". If found, we reverse the + # logic so that timezone parsing code will get it + # right. + if i + 1 < len_l and l[i + 1] in ('+', '-'): + l[i + 1] = ('+', '-')[l[i + 1] == '+'] + res.tzoffset = None + if info.utczone(res.tzname): + # With something like GMT+3, the timezone + # is *not* GMT. + res.tzname = None + + # Check for a numbered timezone + elif res.hour is not None and l[i] in ('+', '-'): + signal = (-1, 1)[l[i] == '+'] + len_li = len(l[i + 1]) + + # TODO: check that l[i + 1] is integer? + if len_li == 4: + # -0300 + hour_offset = int(l[i + 1][:2]) + min_offset = int(l[i + 1][2:]) + elif i + 2 < len_l and l[i + 2] == ':': + # -03:00 + hour_offset = int(l[i + 1]) + min_offset = int(l[i + 3]) # TODO: Check that l[i+3] is minute-like? + i += 2 + elif len_li <= 2: + # -[0]3 + hour_offset = int(l[i + 1][:2]) + min_offset = 0 + else: + raise ValueError(timestr) + + res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60) + + # Look for a timezone name between parenthesis + if (i + 5 < len_l and + info.jump(l[i + 2]) and l[i + 3] == '(' and + l[i + 5] == ')' and + 3 <= len(l[i + 4]) and + self._could_be_tzname(res.hour, res.tzname, + None, l[i + 4])): + # -0300 (BRST) + res.tzname = l[i + 4] + i += 4 + + i += 1 + + # Check jumps + elif not (info.jump(l[i]) or fuzzy): + raise ValueError(timestr) + + else: + skipped_idxs.append(i) + i += 1 + + # Process year/month/day + year, month, day = ymd.resolve_ymd(yearfirst, dayfirst) + + res.century_specified = ymd.century_specified + res.year = year + res.month = month + res.day = day + + except (IndexError, ValueError): + return None, None + + if not info.validate(res): + return None, None + + if fuzzy_with_tokens: + skipped_tokens = self._recombine_skipped(l, skipped_idxs) + return res, tuple(skipped_tokens) + else: + return res, None + + def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy): + # Token is a number + value_repr = tokens[idx] + try: + value = self._to_decimal(value_repr) + except Exception as e: + six.raise_from(ValueError('Unknown numeric token'), e) + + len_li = len(value_repr) + + len_l = len(tokens) + + if (len(ymd) == 3 and len_li in (2, 4) and + res.hour is None and + (idx + 1 >= len_l or + (tokens[idx + 1] != ':' and + info.hms(tokens[idx + 1]) is None))): + # 19990101T23[59] + s = tokens[idx] + res.hour = int(s[:2]) + + if len_li == 4: + res.minute = int(s[2:]) + + elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6): + # YYMMDD or HHMMSS[.ss] + s = tokens[idx] + + if not ymd and '.' not in tokens[idx]: + ymd.append(s[:2]) + ymd.append(s[2:4]) + ymd.append(s[4:]) + else: + # 19990101T235959[.59] + + # TODO: Check if res attributes already set. + res.hour = int(s[:2]) + res.minute = int(s[2:4]) + res.second, res.microsecond = self._parsems(s[4:]) + + elif len_li in (8, 12, 14): + # YYYYMMDD + s = tokens[idx] + ymd.append(s[:4], 'Y') + ymd.append(s[4:6]) + ymd.append(s[6:8]) + + if len_li > 8: + res.hour = int(s[8:10]) + res.minute = int(s[10:12]) + + if len_li > 12: + res.second = int(s[12:]) + + elif self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None: + # HH[ ]h or MM[ ]m or SS[.ss][ ]s + hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True) + (idx, hms) = self._parse_hms(idx, tokens, info, hms_idx) + if hms is not None: + # TODO: checking that hour/minute/second are not + # already set? + self._assign_hms(res, value_repr, hms) + + elif idx + 2 < len_l and tokens[idx + 1] == ':': + # HH:MM[:SS[.ss]] + res.hour = int(value) + value = self._to_decimal(tokens[idx + 2]) # TODO: try/except for this? + (res.minute, res.second) = self._parse_min_sec(value) + + if idx + 4 < len_l and tokens[idx + 3] == ':': + res.second, res.microsecond = self._parsems(tokens[idx + 4]) + + idx += 2 + + idx += 2 + + elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'): + sep = tokens[idx + 1] + ymd.append(value_repr) + + if idx + 2 < len_l and not info.jump(tokens[idx + 2]): + if tokens[idx + 2].isdigit(): + # 01-01[-01] + ymd.append(tokens[idx + 2]) + else: + # 01-Jan[-01] + value = info.month(tokens[idx + 2]) + + if value is not None: + ymd.append(value, 'M') + else: + raise ValueError() + + if idx + 3 < len_l and tokens[idx + 3] == sep: + # We have three members + value = info.month(tokens[idx + 4]) + + if value is not None: + ymd.append(value, 'M') + else: + ymd.append(tokens[idx + 4]) + idx += 2 + + idx += 1 + idx += 1 + + elif idx + 1 >= len_l or info.jump(tokens[idx + 1]): + if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None: + # 12 am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2])) + idx += 1 + else: + # Year, month or day + ymd.append(value) + idx += 1 + + elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24): + # 12am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1])) + idx += 1 + + elif ymd.could_be_day(value): + ymd.append(value) + + elif not fuzzy: + raise ValueError() + + return idx + + def _find_hms_idx(self, idx, tokens, info, allow_jump): + len_l = len(tokens) + + if idx+1 < len_l and info.hms(tokens[idx+1]) is not None: + # There is an "h", "m", or "s" label following this token. We take + # assign the upcoming label to the current token. + # e.g. the "12" in 12h" + hms_idx = idx + 1 + + elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and + info.hms(tokens[idx+2]) is not None): + # There is a space and then an "h", "m", or "s" label. + # e.g. the "12" in "12 h" + hms_idx = idx + 2 + + elif idx > 0 and info.hms(tokens[idx-1]) is not None: + # There is a "h", "m", or "s" preceding this token. Since neither + # of the previous cases was hit, there is no label following this + # token, so we use the previous label. + # e.g. the "04" in "12h04" + hms_idx = idx-1 + + elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and + info.hms(tokens[idx-2]) is not None): + # If we are looking at the final token, we allow for a + # backward-looking check to skip over a space. + # TODO: Are we sure this is the right condition here? + hms_idx = idx - 2 + + else: + hms_idx = None + + return hms_idx + + def _assign_hms(self, res, value_repr, hms): + # See GH issue #427, fixing float rounding + value = self._to_decimal(value_repr) + + if hms == 0: + # Hour + res.hour = int(value) + if value % 1: + res.minute = int(60*(value % 1)) + + elif hms == 1: + (res.minute, res.second) = self._parse_min_sec(value) + + elif hms == 2: + (res.second, res.microsecond) = self._parsems(value_repr) + + def _could_be_tzname(self, hour, tzname, tzoffset, token): + return (hour is not None and + tzname is None and + tzoffset is None and + len(token) <= 5 and + (all(x in string.ascii_uppercase for x in token) + or token in self.info.UTCZONE)) + + def _ampm_valid(self, hour, ampm, fuzzy): + """ + For fuzzy parsing, 'a' or 'am' (both valid English words) + may erroneously trigger the AM/PM flag. Deal with that + here. + """ + val_is_ampm = True + + # If there's already an AM/PM flag, this one isn't one. + if fuzzy and ampm is not None: + val_is_ampm = False + + # If AM/PM is found and hour is not, raise a ValueError + if hour is None: + if fuzzy: + val_is_ampm = False + else: + raise ValueError('No hour specified with AM or PM flag.') + elif not 0 <= hour <= 12: + # If AM/PM is found, it's a 12 hour clock, so raise + # an error for invalid range + if fuzzy: + val_is_ampm = False + else: + raise ValueError('Invalid hour specified for 12-hour clock.') + + return val_is_ampm + + def _adjust_ampm(self, hour, ampm): + if hour < 12 and ampm == 1: + hour += 12 + elif hour == 12 and ampm == 0: + hour = 0 + return hour + + def _parse_min_sec(self, value): + # TODO: Every usage of this function sets res.second to the return + # value. Are there any cases where second will be returned as None and + # we *don't* want to set res.second = None? + minute = int(value) + second = None + + sec_remainder = value % 1 + if sec_remainder: + second = int(60 * sec_remainder) + return (minute, second) + + def _parse_hms(self, idx, tokens, info, hms_idx): + # TODO: Is this going to admit a lot of false-positives for when we + # just happen to have digits and "h", "m" or "s" characters in non-date + # text? I guess hex hashes won't have that problem, but there's plenty + # of random junk out there. + if hms_idx is None: + hms = None + new_idx = idx + elif hms_idx > idx: + hms = info.hms(tokens[hms_idx]) + new_idx = hms_idx + else: + # Looking backwards, increment one. + hms = info.hms(tokens[hms_idx]) + 1 + new_idx = idx + + return (new_idx, hms) + + # ------------------------------------------------------------------ + # Handling for individual tokens. These are kept as methods instead + # of functions for the sake of customizability via subclassing. + + def _parsems(self, value): + """Parse a I[.F] seconds value into (seconds, microseconds).""" + if "." not in value: + return int(value), 0 + else: + i, f = value.split(".") + return int(i), int(f.ljust(6, "0")[:6]) + + def _to_decimal(self, val): + try: + decimal_value = Decimal(val) + # See GH 662, edge case, infinite value should not be converted + # via `_to_decimal` + if not decimal_value.is_finite(): + raise ValueError("Converted decimal value is infinite or NaN") + except Exception as e: + msg = "Could not convert %s to decimal" % val + six.raise_from(ValueError(msg), e) + else: + return decimal_value + + # ------------------------------------------------------------------ + # Post-Parsing construction of datetime output. These are kept as + # methods instead of functions for the sake of customizability via + # subclassing. + + def _build_tzinfo(self, tzinfos, tzname, tzoffset): + if callable(tzinfos): + tzdata = tzinfos(tzname, tzoffset) + else: + tzdata = tzinfos.get(tzname) + # handle case where tzinfo is paased an options that returns None + # eg tzinfos = {'BRST' : None} + if isinstance(tzdata, datetime.tzinfo) or tzdata is None: + tzinfo = tzdata + elif isinstance(tzdata, text_type): + tzinfo = tz.tzstr(tzdata) + elif isinstance(tzdata, integer_types): + tzinfo = tz.tzoffset(tzname, tzdata) + else: + raise TypeError("Offset must be tzinfo subclass, tz string, " + "or int offset.") + return tzinfo + + def _build_tzaware(self, naive, res, tzinfos): + if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)): + tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset) + aware = naive.replace(tzinfo=tzinfo) + aware = self._assign_tzname(aware, res.tzname) + + elif res.tzname and res.tzname in time.tzname: + aware = naive.replace(tzinfo=tz.tzlocal()) + + # Handle ambiguous local datetime + aware = self._assign_tzname(aware, res.tzname) + + # This is mostly relevant for winter GMT zones parsed in the UK + if (aware.tzname() != res.tzname and + res.tzname in self.info.UTCZONE): + aware = aware.replace(tzinfo=tz.UTC) + + elif res.tzoffset == 0: + aware = naive.replace(tzinfo=tz.UTC) + + elif res.tzoffset: + aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) + + elif not res.tzname and not res.tzoffset: + # i.e. no timezone information was found. + aware = naive + + elif res.tzname: + # tz-like string was parsed but we don't know what to do + # with it + warnings.warn("tzname {tzname} identified but not understood. " + "Pass `tzinfos` argument in order to correctly " + "return a timezone-aware datetime. In a future " + "version, this will raise an " + "exception.".format(tzname=res.tzname), + category=UnknownTimezoneWarning) + aware = naive + + return aware + + def _build_naive(self, res, default): + repl = {} + for attr in ("year", "month", "day", "hour", + "minute", "second", "microsecond"): + value = getattr(res, attr) + if value is not None: + repl[attr] = value + + if 'day' not in repl: + # If the default day exceeds the last day of the month, fall back + # to the end of the month. + cyear = default.year if res.year is None else res.year + cmonth = default.month if res.month is None else res.month + cday = default.day if res.day is None else res.day + + if cday > monthrange(cyear, cmonth)[1]: + repl['day'] = monthrange(cyear, cmonth)[1] + + naive = default.replace(**repl) + + if res.weekday is not None and not res.day: + naive = naive + relativedelta.relativedelta(weekday=res.weekday) + + return naive + + def _assign_tzname(self, dt, tzname): + if dt.tzname() != tzname: + new_dt = tz.enfold(dt, fold=1) + if new_dt.tzname() == tzname: + return new_dt + + return dt + + def _recombine_skipped(self, tokens, skipped_idxs): + """ + >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"] + >>> skipped_idxs = [0, 1, 2, 5] + >>> _recombine_skipped(tokens, skipped_idxs) + ["foo bar", "baz"] + """ + skipped_tokens = [] + for i, idx in enumerate(sorted(skipped_idxs)): + if i > 0 and idx - 1 == skipped_idxs[i - 1]: + skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx] + else: + skipped_tokens.append(tokens[idx]) + + return skipped_tokens + + +DEFAULTPARSER = parser() + + +def parse(timestr, parserinfo=None, **kwargs): + """ + + Parse a string in one of the supported formats, using the + ``parserinfo`` parameters. + + :param timestr: + A string containing a date/time stamp. + + :param parserinfo: + A :class:`parserinfo` object containing parameters for the parser. + If ``None``, the default arguments to the :class:`parserinfo` + constructor are used. + + The ``**kwargs`` parameter takes the following keyword arguments: + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM and + YMD. If set to ``None``, this value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken to + be the year, otherwise the last number is taken to be the year. If + this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ParserError: + Raised for invalid or unknown string formats, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date would + be created. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + if parserinfo: + return parser(parserinfo).parse(timestr, **kwargs) + else: + return DEFAULTPARSER.parse(timestr, **kwargs) + + +class _tzparser(object): + + class _result(_resultbase): + + __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", + "start", "end"] + + class _attr(_resultbase): + __slots__ = ["month", "week", "weekday", + "yday", "jyday", "day", "time"] + + def __repr__(self): + return self._repr("") + + def __init__(self): + _resultbase.__init__(self) + self.start = self._attr() + self.end = self._attr() + + def parse(self, tzstr): + res = self._result() + l = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x] + used_idxs = list() + try: + + len_l = len(l) + + i = 0 + while i < len_l: + # BRST+3[BRDT[+2]] + j = i + while j < len_l and not [x for x in l[j] + if x in "0123456789:,-+"]: + j += 1 + if j != i: + if not res.stdabbr: + offattr = "stdoffset" + res.stdabbr = "".join(l[i:j]) + else: + offattr = "dstoffset" + res.dstabbr = "".join(l[i:j]) + + for ii in range(j): + used_idxs.append(ii) + i = j + if (i < len_l and (l[i] in ('+', '-') or l[i][0] in + "0123456789")): + if l[i] in ('+', '-'): + # Yes, that's right. See the TZ variable + # documentation. + signal = (1, -1)[l[i] == '+'] + used_idxs.append(i) + i += 1 + else: + signal = -1 + len_li = len(l[i]) + if len_li == 4: + # -0300 + setattr(res, offattr, (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) * signal) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + setattr(res, offattr, + (int(l[i]) * 3600 + + int(l[i + 2]) * 60) * signal) + used_idxs.append(i) + i += 2 + elif len_li <= 2: + # -[0]3 + setattr(res, offattr, + int(l[i][:2]) * 3600 * signal) + else: + return None + used_idxs.append(i) + i += 1 + if res.dstabbr: + break + else: + break + + + if i < len_l: + for j in range(i, len_l): + if l[j] == ';': + l[j] = ',' + + assert l[i] == ',' + + i += 1 + + if i >= len_l: + pass + elif (8 <= l.count(',') <= 9 and + not [y for x in l[i:] if x != ',' + for y in x if y not in "0123456789+-"]): + # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] + for x in (res.start, res.end): + x.month = int(l[i]) + used_idxs.append(i) + i += 2 + if l[i] == '-': + value = int(l[i + 1]) * -1 + used_idxs.append(i) + i += 1 + else: + value = int(l[i]) + used_idxs.append(i) + i += 2 + if value: + x.week = value + x.weekday = (int(l[i]) - 1) % 7 + else: + x.day = int(l[i]) + used_idxs.append(i) + i += 2 + x.time = int(l[i]) + used_idxs.append(i) + i += 2 + if i < len_l: + if l[i] in ('-', '+'): + signal = (-1, 1)[l[i] == "+"] + used_idxs.append(i) + i += 1 + else: + signal = 1 + used_idxs.append(i) + res.dstoffset = (res.stdoffset + int(l[i]) * signal) + + # This was a made-up format that is not in normal use + warn(('Parsed time zone "%s"' % tzstr) + + 'is in a non-standard dateutil-specific format, which ' + + 'is now deprecated; support for parsing this format ' + + 'will be removed in future versions. It is recommended ' + + 'that you switch to a standard format like the GNU ' + + 'TZ variable format.', tz.DeprecatedTzFormatWarning) + elif (l.count(',') == 2 and l[i:].count('/') <= 2 and + not [y for x in l[i:] if x not in (',', '/', 'J', 'M', + '.', '-', ':') + for y in x if y not in "0123456789"]): + for x in (res.start, res.end): + if l[i] == 'J': + # non-leap year day (1 based) + used_idxs.append(i) + i += 1 + x.jyday = int(l[i]) + elif l[i] == 'M': + # month[-.]week[-.]weekday + used_idxs.append(i) + i += 1 + x.month = int(l[i]) + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.week = int(l[i]) + if x.week == 5: + x.week = -1 + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.weekday = (int(l[i]) - 1) % 7 + else: + # year day (zero based) + x.yday = int(l[i]) + 1 + + used_idxs.append(i) + i += 1 + + if i < len_l and l[i] == '/': + used_idxs.append(i) + i += 1 + # start time + len_li = len(l[i]) + if len_li == 4: + # -0300 + x.time = (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60 + used_idxs.append(i) + i += 2 + if i + 1 < len_l and l[i + 1] == ':': + used_idxs.append(i) + i += 2 + x.time += int(l[i]) + elif len_li <= 2: + # -[0]3 + x.time = (int(l[i][:2]) * 3600) + else: + return None + used_idxs.append(i) + i += 1 + + assert i == len_l or l[i] == ',' + + i += 1 + + assert i >= len_l + + except (IndexError, ValueError, AssertionError): + return None + + unused_idxs = set(range(len_l)).difference(used_idxs) + res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"}) + return res + + +DEFAULTTZPARSER = _tzparser() + + +def _parsetz(tzstr): + return DEFAULTTZPARSER.parse(tzstr) + + +class ParserError(ValueError): + """Exception subclass used for any failure to parse a datetime string. + + This is a subclass of :py:exc:`ValueError`, and should be raised any time + earlier versions of ``dateutil`` would have raised ``ValueError``. + + .. versionadded:: 2.8.1 + """ + def __str__(self): + try: + return self.args[0] % self.args[1:] + except (TypeError, IndexError): + return super(ParserError, self).__str__() + + def __repr__(self): + args = ", ".join("'%s'" % arg for arg in self.args) + return "%s(%s)" % (self.__class__.__name__, args) + + +class UnknownTimezoneWarning(RuntimeWarning): + """Raised when the parser finds a timezone it cannot parse into a tzinfo. + + .. versionadded:: 2.7.0 + """ +# vim:ts=4:sw=4:et diff --git a/dateutil/parser/isoparser.py b/dateutil/parser/isoparser.py new file mode 100644 index 0000000..7060087 --- /dev/null +++ b/dateutil/parser/isoparser.py @@ -0,0 +1,416 @@ +# -*- coding: utf-8 -*- +""" +This module offers a parser for ISO-8601 strings + +It is intended to support all valid date, time and datetime formats per the +ISO-8601 specification. + +..versionadded:: 2.7.0 +""" +from datetime import datetime, timedelta, time, date +import calendar +from dateutil import tz + +from functools import wraps + +import re +import six + +__all__ = ["isoparse", "isoparser"] + + +def _takes_ascii(f): + @wraps(f) + def func(self, str_in, *args, **kwargs): + # If it's a stream, read the whole thing + str_in = getattr(str_in, 'read', lambda: str_in)() + + # If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII + if isinstance(str_in, six.text_type): + # ASCII is the same in UTF-8 + try: + str_in = str_in.encode('ascii') + except UnicodeEncodeError as e: + msg = 'ISO-8601 strings should contain only ASCII characters' + six.raise_from(ValueError(msg), e) + + return f(self, str_in, *args, **kwargs) + + return func + + +class isoparser(object): + def __init__(self, sep=None): + """ + :param sep: + A single character that separates date and time portions. If + ``None``, the parser will accept any single character. + For strict ISO-8601 adherence, pass ``'T'``. + """ + if sep is not None: + if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'): + raise ValueError('Separator must be a single, non-numeric ' + + 'ASCII character') + + sep = sep.encode('ascii') + + self._sep = sep + + @_takes_ascii + def isoparse(self, dt_str): + """ + Parse an ISO-8601 datetime string into a :class:`datetime.datetime`. + + An ISO-8601 datetime string consists of a date portion, followed + optionally by a time portion - the date and time portions are separated + by a single character separator, which is ``T`` in the official + standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be + combined with a time portion. + + Supported date formats are: + + Common: + + - ``YYYY`` + - ``YYYY-MM`` + - ``YYYY-MM-DD`` or ``YYYYMMDD`` + + Uncommon: + + - ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0) + - ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day + + The ISO week and day numbering follows the same logic as + :func:`datetime.date.isocalendar`. + + Supported time formats are: + + - ``hh`` + - ``hh:mm`` or ``hhmm`` + - ``hh:mm:ss`` or ``hhmmss`` + - ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits) + + Midnight is a special case for `hh`, as the standard supports both + 00:00 and 24:00 as a representation. The decimal separator can be + either a dot or a comma. + + + .. caution:: + + Support for fractional components other than seconds is part of the + ISO-8601 standard, but is not currently implemented in this parser. + + Supported time zone offset formats are: + + - `Z` (UTC) + - `±HH:MM` + - `±HHMM` + - `±HH` + + Offsets will be represented as :class:`dateutil.tz.tzoffset` objects, + with the exception of UTC, which will be represented as + :class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such + as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`. + + :param dt_str: + A string or stream containing only an ISO-8601 datetime string + + :return: + Returns a :class:`datetime.datetime` representing the string. + Unspecified components default to their lowest value. + + .. warning:: + + As of version 2.7.0, the strictness of the parser should not be + considered a stable part of the contract. Any valid ISO-8601 string + that parses correctly with the default settings will continue to + parse correctly in future versions, but invalid strings that + currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not + guaranteed to continue failing in future versions if they encode + a valid date. + + .. versionadded:: 2.7.0 + """ + components, pos = self._parse_isodate(dt_str) + + if len(dt_str) > pos: + if self._sep is None or dt_str[pos:pos + 1] == self._sep: + components += self._parse_isotime(dt_str[pos + 1:]) + else: + raise ValueError('String contains unknown ISO components') + + if len(components) > 3 and components[3] == 24: + components[3] = 0 + return datetime(*components) + timedelta(days=1) + + return datetime(*components) + + @_takes_ascii + def parse_isodate(self, datestr): + """ + Parse the date portion of an ISO string. + + :param datestr: + The string portion of an ISO string, without a separator + + :return: + Returns a :class:`datetime.date` object + """ + components, pos = self._parse_isodate(datestr) + if pos < len(datestr): + raise ValueError('String contains unknown ISO ' + + 'components: {!r}'.format(datestr.decode('ascii'))) + return date(*components) + + @_takes_ascii + def parse_isotime(self, timestr): + """ + Parse the time portion of an ISO string. + + :param timestr: + The time portion of an ISO string, without a separator + + :return: + Returns a :class:`datetime.time` object + """ + components = self._parse_isotime(timestr) + if components[0] == 24: + components[0] = 0 + return time(*components) + + @_takes_ascii + def parse_tzstr(self, tzstr, zero_as_utc=True): + """ + Parse a valid ISO time zone string. + + See :func:`isoparser.isoparse` for details on supported formats. + + :param tzstr: + A string representing an ISO time zone offset + + :param zero_as_utc: + Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones + + :return: + Returns :class:`dateutil.tz.tzoffset` for offsets and + :class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is + specified) offsets equivalent to UTC. + """ + return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc) + + # Constants + _DATE_SEP = b'-' + _TIME_SEP = b':' + _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)') + + def _parse_isodate(self, dt_str): + try: + return self._parse_isodate_common(dt_str) + except ValueError: + return self._parse_isodate_uncommon(dt_str) + + def _parse_isodate_common(self, dt_str): + len_str = len(dt_str) + components = [1, 1, 1] + + if len_str < 4: + raise ValueError('ISO string too short') + + # Year + components[0] = int(dt_str[0:4]) + pos = 4 + if pos >= len_str: + return components, pos + + has_sep = dt_str[pos:pos + 1] == self._DATE_SEP + if has_sep: + pos += 1 + + # Month + if len_str - pos < 2: + raise ValueError('Invalid common month') + + components[1] = int(dt_str[pos:pos + 2]) + pos += 2 + + if pos >= len_str: + if has_sep: + return components, pos + else: + raise ValueError('Invalid ISO format') + + if has_sep: + if dt_str[pos:pos + 1] != self._DATE_SEP: + raise ValueError('Invalid separator in ISO string') + pos += 1 + + # Day + if len_str - pos < 2: + raise ValueError('Invalid common day') + components[2] = int(dt_str[pos:pos + 2]) + return components, pos + 2 + + def _parse_isodate_uncommon(self, dt_str): + if len(dt_str) < 4: + raise ValueError('ISO string too short') + + # All ISO formats start with the year + year = int(dt_str[0:4]) + + has_sep = dt_str[4:5] == self._DATE_SEP + + pos = 4 + has_sep # Skip '-' if it's there + if dt_str[pos:pos + 1] == b'W': + # YYYY-?Www-?D? + pos += 1 + weekno = int(dt_str[pos:pos + 2]) + pos += 2 + + dayno = 1 + if len(dt_str) > pos: + if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep: + raise ValueError('Inconsistent use of dash separator') + + pos += has_sep + + dayno = int(dt_str[pos:pos + 1]) + pos += 1 + + base_date = self._calculate_weekdate(year, weekno, dayno) + else: + # YYYYDDD or YYYY-DDD + if len(dt_str) - pos < 3: + raise ValueError('Invalid ordinal day') + + ordinal_day = int(dt_str[pos:pos + 3]) + pos += 3 + + if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)): + raise ValueError('Invalid ordinal day' + + ' {} for year {}'.format(ordinal_day, year)) + + base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1) + + components = [base_date.year, base_date.month, base_date.day] + return components, pos + + def _calculate_weekdate(self, year, week, day): + """ + Calculate the day of corresponding to the ISO year-week-day calendar. + + This function is effectively the inverse of + :func:`datetime.date.isocalendar`. + + :param year: + The year in the ISO calendar + + :param week: + The week in the ISO calendar - range is [1, 53] + + :param day: + The day in the ISO calendar - range is [1 (MON), 7 (SUN)] + + :return: + Returns a :class:`datetime.date` + """ + if not 0 < week < 54: + raise ValueError('Invalid week: {}'.format(week)) + + if not 0 < day < 8: # Range is 1-7 + raise ValueError('Invalid weekday: {}'.format(day)) + + # Get week 1 for the specific year: + jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it + week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1) + + # Now add the specific number of weeks and days to get what we want + week_offset = (week - 1) * 7 + (day - 1) + return week_1 + timedelta(days=week_offset) + + def _parse_isotime(self, timestr): + len_str = len(timestr) + components = [0, 0, 0, 0, None] + pos = 0 + comp = -1 + + if len_str < 2: + raise ValueError('ISO time too short') + + has_sep = False + + while pos < len_str and comp < 5: + comp += 1 + + if timestr[pos:pos + 1] in b'-+Zz': + # Detect time zone boundary + components[-1] = self._parse_tzstr(timestr[pos:]) + pos = len_str + break + + if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP: + has_sep = True + pos += 1 + elif comp == 2 and has_sep: + if timestr[pos:pos+1] != self._TIME_SEP: + raise ValueError('Inconsistent use of colon separator') + pos += 1 + + if comp < 3: + # Hour, minute, second + components[comp] = int(timestr[pos:pos + 2]) + pos += 2 + + if comp == 3: + # Fraction of a second + frac = self._FRACTION_REGEX.match(timestr[pos:]) + if not frac: + continue + + us_str = frac.group(1)[:6] # Truncate to microseconds + components[comp] = int(us_str) * 10**(6 - len(us_str)) + pos += len(frac.group()) + + if pos < len_str: + raise ValueError('Unused components in ISO string') + + if components[0] == 24: + # Standard supports 00:00 and 24:00 as representations of midnight + if any(component != 0 for component in components[1:4]): + raise ValueError('Hour may only be 24 at 24:00:00.000') + + return components + + def _parse_tzstr(self, tzstr, zero_as_utc=True): + if tzstr == b'Z' or tzstr == b'z': + return tz.UTC + + if len(tzstr) not in {3, 5, 6}: + raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters') + + if tzstr[0:1] == b'-': + mult = -1 + elif tzstr[0:1] == b'+': + mult = 1 + else: + raise ValueError('Time zone offset requires sign') + + hours = int(tzstr[1:3]) + if len(tzstr) == 3: + minutes = 0 + else: + minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):]) + + if zero_as_utc and hours == 0 and minutes == 0: + return tz.UTC + else: + if minutes > 59: + raise ValueError('Invalid minutes in time zone offset') + + if hours > 23: + raise ValueError('Invalid hours in time zone offset') + + return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60) + + +DEFAULT_ISOPARSER = isoparser() +isoparse = DEFAULT_ISOPARSER.isoparse diff --git a/dateutil/relativedelta.py b/dateutil/relativedelta.py new file mode 100644 index 0000000..cd323a5 --- /dev/null +++ b/dateutil/relativedelta.py @@ -0,0 +1,599 @@ +# -*- coding: utf-8 -*- +import datetime +import calendar + +import operator +from math import copysign + +from six import integer_types +from warnings import warn + +from ._common import weekday + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) + +__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + + +class relativedelta(object): + """ + The relativedelta type is designed to be applied to an existing datetime and + can replace specific components of that datetime, or represents an interval + of time. + + It is based on the specification of the excellent work done by M.-A. Lemburg + in his + `mx.DateTime `_ extension. + However, notice that this type does *NOT* implement the same algorithm as + his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. + + There are two different ways to build a relativedelta instance. The + first one is passing it two date/datetime classes:: + + relativedelta(datetime1, datetime2) + + The second one is passing it any number of the following keyword arguments:: + + relativedelta(arg1=x,arg2=y,arg3=z...) + + year, month, day, hour, minute, second, microsecond: + Absolute information (argument is singular); adding or subtracting a + relativedelta with absolute information does not perform an arithmetic + operation, but rather REPLACES the corresponding value in the + original datetime with the value(s) in relativedelta. + + years, months, weeks, days, hours, minutes, seconds, microseconds: + Relative information, may be negative (argument is plural); adding + or subtracting a relativedelta with relative information performs + the corresponding arithmetic operation on the original datetime value + with the information in the relativedelta. + + weekday: + One of the weekday instances (MO, TU, etc) available in the + relativedelta module. These instances may receive a parameter N, + specifying the Nth weekday, which could be positive or negative + (like MO(+1) or MO(-2)). Not specifying it is the same as specifying + +1. You can also use an integer, where 0=MO. This argument is always + relative e.g. if the calculated date is already Monday, using MO(1) + or MO(-1) won't change the day. To effectively make it absolute, use + it in combination with the day argument (e.g. day=1, MO(1) for first + Monday of the month). + + leapdays: + Will add given days to the date found, if year is a leap + year, and the date found is post 28 of february. + + yearday, nlyearday: + Set the yearday or the non-leap year day (jump leap days). + These are converted to day/month/leapdays information. + + There are relative and absolute forms of the keyword + arguments. The plural is relative, and the singular is + absolute. For each argument in the order below, the absolute form + is applied first (by setting each attribute to that value) and + then the relative form (by adding the value to the attribute). + + The order of attributes considered when this relativedelta is + added to a datetime is: + + 1. Year + 2. Month + 3. Day + 4. Hours + 5. Minutes + 6. Seconds + 7. Microseconds + + Finally, weekday is applied, using the rule described above. + + For example + + >>> from datetime import datetime + >>> from dateutil.relativedelta import relativedelta, MO + >>> dt = datetime(2018, 4, 9, 13, 37, 0) + >>> delta = relativedelta(hours=25, day=1, weekday=MO(1)) + >>> dt + delta + datetime.datetime(2018, 4, 2, 14, 37) + + First, the day is set to 1 (the first of the month), then 25 hours + are added, to get to the 2nd day and 14th hour, finally the + weekday is applied, but since the 2nd is already a Monday there is + no effect. + + """ + + def __init__(self, dt1=None, dt2=None, + years=0, months=0, days=0, leapdays=0, weeks=0, + hours=0, minutes=0, seconds=0, microseconds=0, + year=None, month=None, day=None, weekday=None, + yearday=None, nlyearday=None, + hour=None, minute=None, second=None, microsecond=None): + + if dt1 and dt2: + # datetime is a subclass of date. So both must be date + if not (isinstance(dt1, datetime.date) and + isinstance(dt2, datetime.date)): + raise TypeError("relativedelta only diffs datetime/date") + + # We allow two dates, or two datetimes, so we coerce them to be + # of the same type + if (isinstance(dt1, datetime.datetime) != + isinstance(dt2, datetime.datetime)): + if not isinstance(dt1, datetime.datetime): + dt1 = datetime.datetime.fromordinal(dt1.toordinal()) + elif not isinstance(dt2, datetime.datetime): + dt2 = datetime.datetime.fromordinal(dt2.toordinal()) + + self.years = 0 + self.months = 0 + self.days = 0 + self.leapdays = 0 + self.hours = 0 + self.minutes = 0 + self.seconds = 0 + self.microseconds = 0 + self.year = None + self.month = None + self.day = None + self.weekday = None + self.hour = None + self.minute = None + self.second = None + self.microsecond = None + self._has_time = 0 + + # Get year / month delta between the two + months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month) + self._set_months(months) + + # Remove the year/month delta so the timedelta is just well-defined + # time units (seconds, days and microseconds) + dtm = self.__radd__(dt2) + + # If we've overshot our target, make an adjustment + if dt1 < dt2: + compare = operator.gt + increment = 1 + else: + compare = operator.lt + increment = -1 + + while compare(dt1, dtm): + months += increment + self._set_months(months) + dtm = self.__radd__(dt2) + + # Get the timedelta between the "months-adjusted" date and dt1 + delta = dt1 - dtm + self.seconds = delta.seconds + delta.days * 86400 + self.microseconds = delta.microseconds + else: + # Check for non-integer values in integer-only quantities + if any(x is not None and x != int(x) for x in (years, months)): + raise ValueError("Non-integer years and months are " + "ambiguous and not currently supported.") + + # Relative information + self.years = int(years) + self.months = int(months) + self.days = days + weeks * 7 + self.leapdays = leapdays + self.hours = hours + self.minutes = minutes + self.seconds = seconds + self.microseconds = microseconds + + # Absolute information + self.year = year + self.month = month + self.day = day + self.hour = hour + self.minute = minute + self.second = second + self.microsecond = microsecond + + if any(x is not None and int(x) != x + for x in (year, month, day, hour, + minute, second, microsecond)): + # For now we'll deprecate floats - later it'll be an error. + warn("Non-integer value passed as absolute information. " + + "This is not a well-defined condition and will raise " + + "errors in future versions.", DeprecationWarning) + + if isinstance(weekday, integer_types): + self.weekday = weekdays[weekday] + else: + self.weekday = weekday + + yday = 0 + if nlyearday: + yday = nlyearday + elif yearday: + yday = yearday + if yearday > 59: + self.leapdays = -1 + if yday: + ydayidx = [31, 59, 90, 120, 151, 181, 212, + 243, 273, 304, 334, 366] + for idx, ydays in enumerate(ydayidx): + if yday <= ydays: + self.month = idx+1 + if idx == 0: + self.day = yday + else: + self.day = yday-ydayidx[idx-1] + break + else: + raise ValueError("invalid year day (%d)" % yday) + + self._fix() + + def _fix(self): + if abs(self.microseconds) > 999999: + s = _sign(self.microseconds) + div, mod = divmod(self.microseconds * s, 1000000) + self.microseconds = mod * s + self.seconds += div * s + if abs(self.seconds) > 59: + s = _sign(self.seconds) + div, mod = divmod(self.seconds * s, 60) + self.seconds = mod * s + self.minutes += div * s + if abs(self.minutes) > 59: + s = _sign(self.minutes) + div, mod = divmod(self.minutes * s, 60) + self.minutes = mod * s + self.hours += div * s + if abs(self.hours) > 23: + s = _sign(self.hours) + div, mod = divmod(self.hours * s, 24) + self.hours = mod * s + self.days += div * s + if abs(self.months) > 11: + s = _sign(self.months) + div, mod = divmod(self.months * s, 12) + self.months = mod * s + self.years += div * s + if (self.hours or self.minutes or self.seconds or self.microseconds + or self.hour is not None or self.minute is not None or + self.second is not None or self.microsecond is not None): + self._has_time = 1 + else: + self._has_time = 0 + + @property + def weeks(self): + return int(self.days / 7.0) + + @weeks.setter + def weeks(self, value): + self.days = self.days - (self.weeks * 7) + value * 7 + + def _set_months(self, months): + self.months = months + if abs(self.months) > 11: + s = _sign(self.months) + div, mod = divmod(self.months * s, 12) + self.months = mod * s + self.years = div * s + else: + self.years = 0 + + def normalized(self): + """ + Return a version of this object represented entirely using integer + values for the relative attributes. + + >>> relativedelta(days=1.5, hours=2).normalized() + relativedelta(days=+1, hours=+14) + + :return: + Returns a :class:`dateutil.relativedelta.relativedelta` object. + """ + # Cascade remainders down (rounding each to roughly nearest microsecond) + days = int(self.days) + + hours_f = round(self.hours + 24 * (self.days - days), 11) + hours = int(hours_f) + + minutes_f = round(self.minutes + 60 * (hours_f - hours), 10) + minutes = int(minutes_f) + + seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8) + seconds = int(seconds_f) + + microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds)) + + # Constructor carries overflow back up with call to _fix() + return self.__class__(years=self.years, months=self.months, + days=days, hours=hours, minutes=minutes, + seconds=seconds, microseconds=microseconds, + leapdays=self.leapdays, year=self.year, + month=self.month, day=self.day, + weekday=self.weekday, hour=self.hour, + minute=self.minute, second=self.second, + microsecond=self.microsecond) + + def __add__(self, other): + if isinstance(other, relativedelta): + return self.__class__(years=other.years + self.years, + months=other.months + self.months, + days=other.days + self.days, + hours=other.hours + self.hours, + minutes=other.minutes + self.minutes, + seconds=other.seconds + self.seconds, + microseconds=(other.microseconds + + self.microseconds), + leapdays=other.leapdays or self.leapdays, + year=(other.year if other.year is not None + else self.year), + month=(other.month if other.month is not None + else self.month), + day=(other.day if other.day is not None + else self.day), + weekday=(other.weekday if other.weekday is not None + else self.weekday), + hour=(other.hour if other.hour is not None + else self.hour), + minute=(other.minute if other.minute is not None + else self.minute), + second=(other.second if other.second is not None + else self.second), + microsecond=(other.microsecond if other.microsecond + is not None else + self.microsecond)) + if isinstance(other, datetime.timedelta): + return self.__class__(years=self.years, + months=self.months, + days=self.days + other.days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds + other.seconds, + microseconds=self.microseconds + other.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + if not isinstance(other, datetime.date): + return NotImplemented + elif self._has_time and not isinstance(other, datetime.datetime): + other = datetime.datetime.fromordinal(other.toordinal()) + year = (self.year or other.year)+self.years + month = self.month or other.month + if self.months: + assert 1 <= abs(self.months) <= 12 + month += self.months + if month > 12: + year += 1 + month -= 12 + elif month < 1: + year -= 1 + month += 12 + day = min(calendar.monthrange(year, month)[1], + self.day or other.day) + repl = {"year": year, "month": month, "day": day} + for attr in ["hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + repl[attr] = value + days = self.days + if self.leapdays and month > 2 and calendar.isleap(year): + days += self.leapdays + ret = (other.replace(**repl) + + datetime.timedelta(days=days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds, + microseconds=self.microseconds)) + if self.weekday: + weekday, nth = self.weekday.weekday, self.weekday.n or 1 + jumpdays = (abs(nth) - 1) * 7 + if nth > 0: + jumpdays += (7 - ret.weekday() + weekday) % 7 + else: + jumpdays += (ret.weekday() - weekday) % 7 + jumpdays *= -1 + ret += datetime.timedelta(days=jumpdays) + return ret + + def __radd__(self, other): + return self.__add__(other) + + def __rsub__(self, other): + return self.__neg__().__radd__(other) + + def __sub__(self, other): + if not isinstance(other, relativedelta): + return NotImplemented # In case the other object defines __rsub__ + return self.__class__(years=self.years - other.years, + months=self.months - other.months, + days=self.days - other.days, + hours=self.hours - other.hours, + minutes=self.minutes - other.minutes, + seconds=self.seconds - other.seconds, + microseconds=self.microseconds - other.microseconds, + leapdays=self.leapdays or other.leapdays, + year=(self.year if self.year is not None + else other.year), + month=(self.month if self.month is not None else + other.month), + day=(self.day if self.day is not None else + other.day), + weekday=(self.weekday if self.weekday is not None else + other.weekday), + hour=(self.hour if self.hour is not None else + other.hour), + minute=(self.minute if self.minute is not None else + other.minute), + second=(self.second if self.second is not None else + other.second), + microsecond=(self.microsecond if self.microsecond + is not None else + other.microsecond)) + + def __abs__(self): + return self.__class__(years=abs(self.years), + months=abs(self.months), + days=abs(self.days), + hours=abs(self.hours), + minutes=abs(self.minutes), + seconds=abs(self.seconds), + microseconds=abs(self.microseconds), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + def __neg__(self): + return self.__class__(years=-self.years, + months=-self.months, + days=-self.days, + hours=-self.hours, + minutes=-self.minutes, + seconds=-self.seconds, + microseconds=-self.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + def __bool__(self): + return not (not self.years and + not self.months and + not self.days and + not self.hours and + not self.minutes and + not self.seconds and + not self.microseconds and + not self.leapdays and + self.year is None and + self.month is None and + self.day is None and + self.weekday is None and + self.hour is None and + self.minute is None and + self.second is None and + self.microsecond is None) + # Compatibility with Python 2.x + __nonzero__ = __bool__ + + def __mul__(self, other): + try: + f = float(other) + except TypeError: + return NotImplemented + + return self.__class__(years=int(self.years * f), + months=int(self.months * f), + days=int(self.days * f), + hours=int(self.hours * f), + minutes=int(self.minutes * f), + seconds=int(self.seconds * f), + microseconds=int(self.microseconds * f), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + __rmul__ = __mul__ + + def __eq__(self, other): + if not isinstance(other, relativedelta): + return NotImplemented + if self.weekday or other.weekday: + if not self.weekday or not other.weekday: + return False + if self.weekday.weekday != other.weekday.weekday: + return False + n1, n2 = self.weekday.n, other.weekday.n + if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): + return False + return (self.years == other.years and + self.months == other.months and + self.days == other.days and + self.hours == other.hours and + self.minutes == other.minutes and + self.seconds == other.seconds and + self.microseconds == other.microseconds and + self.leapdays == other.leapdays and + self.year == other.year and + self.month == other.month and + self.day == other.day and + self.hour == other.hour and + self.minute == other.minute and + self.second == other.second and + self.microsecond == other.microsecond) + + def __hash__(self): + return hash(( + self.weekday, + self.years, + self.months, + self.days, + self.hours, + self.minutes, + self.seconds, + self.microseconds, + self.leapdays, + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + )) + + def __ne__(self, other): + return not self.__eq__(other) + + def __div__(self, other): + try: + reciprocal = 1 / float(other) + except TypeError: + return NotImplemented + + return self.__mul__(reciprocal) + + __truediv__ = __div__ + + def __repr__(self): + l = [] + for attr in ["years", "months", "days", "leapdays", + "hours", "minutes", "seconds", "microseconds"]: + value = getattr(self, attr) + if value: + l.append("{attr}={value:+g}".format(attr=attr, value=value)) + for attr in ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + l.append("{attr}={value}".format(attr=attr, value=repr(value))) + return "{classname}({attrs})".format(classname=self.__class__.__name__, + attrs=", ".join(l)) + + +def _sign(x): + return int(copysign(1, x)) + +# vim:ts=4:sw=4:et diff --git a/dateutil/rrule.py b/dateutil/rrule.py new file mode 100644 index 0000000..571a0d2 --- /dev/null +++ b/dateutil/rrule.py @@ -0,0 +1,1737 @@ +# -*- coding: utf-8 -*- +""" +The rrule module offers a small, complete, and very fast, implementation of +the recurrence rules documented in the +`iCalendar RFC `_, +including support for caching of results. +""" +import calendar +import datetime +import heapq +import itertools +import re +import sys +from functools import wraps +# For warning about deprecation of until and count +from warnings import warn + +from six import advance_iterator, integer_types + +from six.moves import _thread, range + +from ._common import weekday as weekdaybase + +try: + from math import gcd +except ImportError: + from fractions import gcd + +__all__ = ["rrule", "rruleset", "rrulestr", + "YEARLY", "MONTHLY", "WEEKLY", "DAILY", + "HOURLY", "MINUTELY", "SECONDLY", + "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + +# Every mask is 7 days longer to handle cross-year weekly periods. +M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + + [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) +M365MASK = list(M366MASK) +M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) +MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +MDAY365MASK = list(MDAY366MASK) +M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) +NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +NMDAY365MASK = list(NMDAY366MASK) +M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) +M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) +WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 +del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] +MDAY365MASK = tuple(MDAY365MASK) +M365MASK = tuple(M365MASK) + +FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'] + +(YEARLY, + MONTHLY, + WEEKLY, + DAILY, + HOURLY, + MINUTELY, + SECONDLY) = list(range(7)) + +# Imported on demand. +easter = None +parser = None + + +class weekday(weekdaybase): + """ + This version of weekday does not allow n = 0. + """ + def __init__(self, wkday, n=None): + if n == 0: + raise ValueError("Can't create weekday with n==0") + + super(weekday, self).__init__(wkday, n) + + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) + + +def _invalidates_cache(f): + """ + Decorator for rruleset methods which may invalidate the + cached length. + """ + @wraps(f) + def inner_func(self, *args, **kwargs): + rv = f(self, *args, **kwargs) + self._invalidate_cache() + return rv + + return inner_func + + +class rrulebase(object): + def __init__(self, cache=False): + if cache: + self._cache = [] + self._cache_lock = _thread.allocate_lock() + self._invalidate_cache() + else: + self._cache = None + self._cache_complete = False + self._len = None + + def __iter__(self): + if self._cache_complete: + return iter(self._cache) + elif self._cache is None: + return self._iter() + else: + return self._iter_cached() + + def _invalidate_cache(self): + if self._cache is not None: + self._cache = [] + self._cache_complete = False + self._cache_gen = self._iter() + + if self._cache_lock.locked(): + self._cache_lock.release() + + self._len = None + + def _iter_cached(self): + i = 0 + gen = self._cache_gen + cache = self._cache + acquire = self._cache_lock.acquire + release = self._cache_lock.release + while gen: + if i == len(cache): + acquire() + if self._cache_complete: + break + try: + for j in range(10): + cache.append(advance_iterator(gen)) + except StopIteration: + self._cache_gen = gen = None + self._cache_complete = True + break + release() + yield cache[i] + i += 1 + while i < self._len: + yield cache[i] + i += 1 + + def __getitem__(self, item): + if self._cache_complete: + return self._cache[item] + elif isinstance(item, slice): + if item.step and item.step < 0: + return list(iter(self))[item] + else: + return list(itertools.islice(self, + item.start or 0, + item.stop or sys.maxsize, + item.step or 1)) + elif item >= 0: + gen = iter(self) + try: + for i in range(item+1): + res = advance_iterator(gen) + except StopIteration: + raise IndexError + return res + else: + return list(iter(self))[item] + + def __contains__(self, item): + if self._cache_complete: + return item in self._cache + else: + for i in self: + if i == item: + return True + elif i > item: + return False + return False + + # __len__() introduces a large performance penalty. + def count(self): + """ Returns the number of recurrences in this set. It will have go + through the whole recurrence, if this hasn't been done before. """ + if self._len is None: + for x in self: + pass + return self._len + + def before(self, dt, inc=False): + """ Returns the last recurrence before the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + last = None + if inc: + for i in gen: + if i > dt: + break + last = i + else: + for i in gen: + if i >= dt: + break + last = i + return last + + def after(self, dt, inc=False): + """ Returns the first recurrence after the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + if inc: + for i in gen: + if i >= dt: + return i + else: + for i in gen: + if i > dt: + return i + return None + + def xafter(self, dt, count=None, inc=False): + """ + Generator which yields up to `count` recurrences after the given + datetime instance, equivalent to `after`. + + :param dt: + The datetime at which to start generating recurrences. + + :param count: + The maximum number of recurrences to generate. If `None` (default), + dates are generated until the recurrence rule is exhausted. + + :param inc: + If `dt` is an instance of the rule and `inc` is `True`, it is + included in the output. + + :yields: Yields a sequence of `datetime` objects. + """ + + if self._cache_complete: + gen = self._cache + else: + gen = self + + # Select the comparison function + if inc: + comp = lambda dc, dtc: dc >= dtc + else: + comp = lambda dc, dtc: dc > dtc + + # Generate dates + n = 0 + for d in gen: + if comp(d, dt): + if count is not None: + n += 1 + if n > count: + break + + yield d + + def between(self, after, before, inc=False, count=1): + """ Returns all the occurrences of the rrule between after and before. + The inc keyword defines what happens if after and/or before are + themselves occurrences. With inc=True, they will be included in the + list, if they are found in the recurrence set. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + started = False + l = [] + if inc: + for i in gen: + if i > before: + break + elif not started: + if i >= after: + started = True + l.append(i) + else: + l.append(i) + else: + for i in gen: + if i >= before: + break + elif not started: + if i > after: + started = True + l.append(i) + else: + l.append(i) + return l + + +class rrule(rrulebase): + """ + That's the base of the rrule operation. It accepts all the keywords + defined in the RFC as its constructor parameters (except byday, + which was renamed to byweekday) and more. The constructor prototype is:: + + rrule(freq) + + Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, + or SECONDLY. + + .. note:: + Per RFC section 3.3.10, recurrence instances falling on invalid dates + and times are ignored rather than coerced: + + Recurrence rules may generate recurrence instances with an invalid + date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM + on a day where the local time is moved forward by an hour at 1:00 + AM). Such recurrence instances MUST be ignored and MUST NOT be + counted as part of the recurrence set. + + This can lead to possibly surprising behavior when, for example, the + start date occurs at the end of the month: + + >>> from dateutil.rrule import rrule, MONTHLY + >>> from datetime import datetime + >>> start_date = datetime(2014, 12, 31) + >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date)) + ... # doctest: +NORMALIZE_WHITESPACE + [datetime.datetime(2014, 12, 31, 0, 0), + datetime.datetime(2015, 1, 31, 0, 0), + datetime.datetime(2015, 3, 31, 0, 0), + datetime.datetime(2015, 5, 31, 0, 0)] + + Additionally, it supports the following keyword arguments: + + :param dtstart: + The recurrence start. Besides being the base for the recurrence, + missing parameters in the final recurrence instances will also be + extracted from this date. If not given, datetime.now() will be used + instead. + :param interval: + The interval between each freq iteration. For example, when using + YEARLY, an interval of 2 means once every two years, but with HOURLY, + it means once every two hours. The default interval is 1. + :param wkst: + The week start day. Must be one of the MO, TU, WE constants, or an + integer, specifying the first day of the week. This will affect + recurrences based on weekly periods. The default week start is got + from calendar.firstweekday(), and may be modified by + calendar.setfirstweekday(). + :param count: + If given, this determines how many occurrences will be generated. + + .. note:: + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. + :param until: + If given, this must be a datetime instance specifying the upper-bound + limit of the recurrence. The last recurrence in the rule is the greatest + datetime that is less than or equal to the value specified in the + ``until`` parameter. + + .. note:: + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. + :param bysetpos: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each given integer will specify an occurrence + number, corresponding to the nth occurrence of the rule inside the + frequency period. For example, a bysetpos of -1 if combined with a + MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will + result in the last work day of every month. + :param bymonth: + If given, it must be either an integer, or a sequence of integers, + meaning the months to apply the recurrence to. + :param bymonthday: + If given, it must be either an integer, or a sequence of integers, + meaning the month days to apply the recurrence to. + :param byyearday: + If given, it must be either an integer, or a sequence of integers, + meaning the year days to apply the recurrence to. + :param byeaster: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each integer will define an offset from the + Easter Sunday. Passing the offset 0 to byeaster will yield the Easter + Sunday itself. This is an extension to the RFC specification. + :param byweekno: + If given, it must be either an integer, or a sequence of integers, + meaning the week numbers to apply the recurrence to. Week numbers + have the meaning described in ISO8601, that is, the first week of + the year is that containing at least four days of the new year. + :param byweekday: + If given, it must be either an integer (0 == MO), a sequence of + integers, one of the weekday constants (MO, TU, etc), or a sequence + of these constants. When given, these variables will define the + weekdays where the recurrence will be applied. It's also possible to + use an argument n for the weekday instances, which will mean the nth + occurrence of this weekday in the period. For example, with MONTHLY, + or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the + first friday of the month where the recurrence happens. Notice that in + the RFC documentation, this is specified as BYDAY, but was renamed to + avoid the ambiguity of that keyword. + :param byhour: + If given, it must be either an integer, or a sequence of integers, + meaning the hours to apply the recurrence to. + :param byminute: + If given, it must be either an integer, or a sequence of integers, + meaning the minutes to apply the recurrence to. + :param bysecond: + If given, it must be either an integer, or a sequence of integers, + meaning the seconds to apply the recurrence to. + :param cache: + If given, it must be a boolean value specifying to enable or disable + caching of results. If you will use the same rrule instance multiple + times, enabling caching will improve the performance considerably. + """ + def __init__(self, freq, dtstart=None, + interval=1, wkst=None, count=None, until=None, bysetpos=None, + bymonth=None, bymonthday=None, byyearday=None, byeaster=None, + byweekno=None, byweekday=None, + byhour=None, byminute=None, bysecond=None, + cache=False): + super(rrule, self).__init__(cache) + global easter + if not dtstart: + if until and until.tzinfo: + dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) + else: + dtstart = datetime.datetime.now().replace(microsecond=0) + elif not isinstance(dtstart, datetime.datetime): + dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) + else: + dtstart = dtstart.replace(microsecond=0) + self._dtstart = dtstart + self._tzinfo = dtstart.tzinfo + self._freq = freq + self._interval = interval + self._count = count + + # Cache the original byxxx rules, if they are provided, as the _byxxx + # attributes do not necessarily map to the inputs, and this can be + # a problem in generating the strings. Only store things if they've + # been supplied (the string retrieval will just use .get()) + self._original_rule = {} + + if until and not isinstance(until, datetime.datetime): + until = datetime.datetime.fromordinal(until.toordinal()) + self._until = until + + if self._dtstart and self._until: + if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None): + # According to RFC5545 Section 3.3.10: + # https://tools.ietf.org/html/rfc5545#section-3.3.10 + # + # > If the "DTSTART" property is specified as a date with UTC + # > time or a date with local time and time zone reference, + # > then the UNTIL rule part MUST be specified as a date with + # > UTC time. + raise ValueError( + 'RRULE UNTIL values must be specified in UTC when DTSTART ' + 'is timezone-aware' + ) + + if count is not None and until: + warn("Using both 'count' and 'until' is inconsistent with RFC 5545" + " and has been deprecated in dateutil. Future versions will " + "raise an error.", DeprecationWarning) + + if wkst is None: + self._wkst = calendar.firstweekday() + elif isinstance(wkst, integer_types): + self._wkst = wkst + else: + self._wkst = wkst.weekday + + if bysetpos is None: + self._bysetpos = None + elif isinstance(bysetpos, integer_types): + if bysetpos == 0 or not (-366 <= bysetpos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + self._bysetpos = (bysetpos,) + else: + self._bysetpos = tuple(bysetpos) + for pos in self._bysetpos: + if pos == 0 or not (-366 <= pos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + + if self._bysetpos: + self._original_rule['bysetpos'] = self._bysetpos + + if (byweekno is None and byyearday is None and bymonthday is None and + byweekday is None and byeaster is None): + if freq == YEARLY: + if bymonth is None: + bymonth = dtstart.month + self._original_rule['bymonth'] = None + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == MONTHLY: + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == WEEKLY: + byweekday = dtstart.weekday() + self._original_rule['byweekday'] = None + + # bymonth + if bymonth is None: + self._bymonth = None + else: + if isinstance(bymonth, integer_types): + bymonth = (bymonth,) + + self._bymonth = tuple(sorted(set(bymonth))) + + if 'bymonth' not in self._original_rule: + self._original_rule['bymonth'] = self._bymonth + + # byyearday + if byyearday is None: + self._byyearday = None + else: + if isinstance(byyearday, integer_types): + byyearday = (byyearday,) + + self._byyearday = tuple(sorted(set(byyearday))) + self._original_rule['byyearday'] = self._byyearday + + # byeaster + if byeaster is not None: + if not easter: + from dateutil import easter + if isinstance(byeaster, integer_types): + self._byeaster = (byeaster,) + else: + self._byeaster = tuple(sorted(byeaster)) + + self._original_rule['byeaster'] = self._byeaster + else: + self._byeaster = None + + # bymonthday + if bymonthday is None: + self._bymonthday = () + self._bynmonthday = () + else: + if isinstance(bymonthday, integer_types): + bymonthday = (bymonthday,) + + bymonthday = set(bymonthday) # Ensure it's unique + + self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) + self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) + + # Storing positive numbers first, then negative numbers + if 'bymonthday' not in self._original_rule: + self._original_rule['bymonthday'] = tuple( + itertools.chain(self._bymonthday, self._bynmonthday)) + + # byweekno + if byweekno is None: + self._byweekno = None + else: + if isinstance(byweekno, integer_types): + byweekno = (byweekno,) + + self._byweekno = tuple(sorted(set(byweekno))) + + self._original_rule['byweekno'] = self._byweekno + + # byweekday / bynweekday + if byweekday is None: + self._byweekday = None + self._bynweekday = None + else: + # If it's one of the valid non-sequence types, convert to a + # single-element sequence before the iterator that builds the + # byweekday set. + if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): + byweekday = (byweekday,) + + self._byweekday = set() + self._bynweekday = set() + for wday in byweekday: + if isinstance(wday, integer_types): + self._byweekday.add(wday) + elif not wday.n or freq > MONTHLY: + self._byweekday.add(wday.weekday) + else: + self._bynweekday.add((wday.weekday, wday.n)) + + if not self._byweekday: + self._byweekday = None + elif not self._bynweekday: + self._bynweekday = None + + if self._byweekday is not None: + self._byweekday = tuple(sorted(self._byweekday)) + orig_byweekday = [weekday(x) for x in self._byweekday] + else: + orig_byweekday = () + + if self._bynweekday is not None: + self._bynweekday = tuple(sorted(self._bynweekday)) + orig_bynweekday = [weekday(*x) for x in self._bynweekday] + else: + orig_bynweekday = () + + if 'byweekday' not in self._original_rule: + self._original_rule['byweekday'] = tuple(itertools.chain( + orig_byweekday, orig_bynweekday)) + + # byhour + if byhour is None: + if freq < HOURLY: + self._byhour = {dtstart.hour} + else: + self._byhour = None + else: + if isinstance(byhour, integer_types): + byhour = (byhour,) + + if freq == HOURLY: + self._byhour = self.__construct_byset(start=dtstart.hour, + byxxx=byhour, + base=24) + else: + self._byhour = set(byhour) + + self._byhour = tuple(sorted(self._byhour)) + self._original_rule['byhour'] = self._byhour + + # byminute + if byminute is None: + if freq < MINUTELY: + self._byminute = {dtstart.minute} + else: + self._byminute = None + else: + if isinstance(byminute, integer_types): + byminute = (byminute,) + + if freq == MINUTELY: + self._byminute = self.__construct_byset(start=dtstart.minute, + byxxx=byminute, + base=60) + else: + self._byminute = set(byminute) + + self._byminute = tuple(sorted(self._byminute)) + self._original_rule['byminute'] = self._byminute + + # bysecond + if bysecond is None: + if freq < SECONDLY: + self._bysecond = ((dtstart.second,)) + else: + self._bysecond = None + else: + if isinstance(bysecond, integer_types): + bysecond = (bysecond,) + + self._bysecond = set(bysecond) + + if freq == SECONDLY: + self._bysecond = self.__construct_byset(start=dtstart.second, + byxxx=bysecond, + base=60) + else: + self._bysecond = set(bysecond) + + self._bysecond = tuple(sorted(self._bysecond)) + self._original_rule['bysecond'] = self._bysecond + + if self._freq >= HOURLY: + self._timeset = None + else: + self._timeset = [] + for hour in self._byhour: + for minute in self._byminute: + for second in self._bysecond: + self._timeset.append( + datetime.time(hour, minute, second, + tzinfo=self._tzinfo)) + self._timeset.sort() + self._timeset = tuple(self._timeset) + + def __str__(self): + """ + Output a string that would generate this RRULE if passed to rrulestr. + This is mostly compatible with RFC5545, except for the + dateutil-specific extension BYEASTER. + """ + + output = [] + h, m, s = [None] * 3 + if self._dtstart: + output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S')) + h, m, s = self._dtstart.timetuple()[3:6] + + parts = ['FREQ=' + FREQNAMES[self._freq]] + if self._interval != 1: + parts.append('INTERVAL=' + str(self._interval)) + + if self._wkst: + parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) + + if self._count is not None: + parts.append('COUNT=' + str(self._count)) + + if self._until: + parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S')) + + if self._original_rule.get('byweekday') is not None: + # The str() method on weekday objects doesn't generate + # RFC5545-compliant strings, so we should modify that. + original_rule = dict(self._original_rule) + wday_strings = [] + for wday in original_rule['byweekday']: + if wday.n: + wday_strings.append('{n:+d}{wday}'.format( + n=wday.n, + wday=repr(wday)[0:2])) + else: + wday_strings.append(repr(wday)) + + original_rule['byweekday'] = wday_strings + else: + original_rule = self._original_rule + + partfmt = '{name}={vals}' + for name, key in [('BYSETPOS', 'bysetpos'), + ('BYMONTH', 'bymonth'), + ('BYMONTHDAY', 'bymonthday'), + ('BYYEARDAY', 'byyearday'), + ('BYWEEKNO', 'byweekno'), + ('BYDAY', 'byweekday'), + ('BYHOUR', 'byhour'), + ('BYMINUTE', 'byminute'), + ('BYSECOND', 'bysecond'), + ('BYEASTER', 'byeaster')]: + value = original_rule.get(key) + if value: + parts.append(partfmt.format(name=name, vals=(','.join(str(v) + for v in value)))) + + output.append('RRULE:' + ';'.join(parts)) + return '\n'.join(output) + + def replace(self, **kwargs): + """Return new rrule with same attributes except for those attributes given new + values by whichever keyword arguments are specified.""" + new_kwargs = {"interval": self._interval, + "count": self._count, + "dtstart": self._dtstart, + "freq": self._freq, + "until": self._until, + "wkst": self._wkst, + "cache": False if self._cache is None else True } + new_kwargs.update(self._original_rule) + new_kwargs.update(kwargs) + return rrule(**new_kwargs) + + def _iter(self): + year, month, day, hour, minute, second, weekday, yearday, _ = \ + self._dtstart.timetuple() + + # Some local variables to speed things up a bit + freq = self._freq + interval = self._interval + wkst = self._wkst + until = self._until + bymonth = self._bymonth + byweekno = self._byweekno + byyearday = self._byyearday + byweekday = self._byweekday + byeaster = self._byeaster + bymonthday = self._bymonthday + bynmonthday = self._bynmonthday + bysetpos = self._bysetpos + byhour = self._byhour + byminute = self._byminute + bysecond = self._bysecond + + ii = _iterinfo(self) + ii.rebuild(year, month) + + getdayset = {YEARLY: ii.ydayset, + MONTHLY: ii.mdayset, + WEEKLY: ii.wdayset, + DAILY: ii.ddayset, + HOURLY: ii.ddayset, + MINUTELY: ii.ddayset, + SECONDLY: ii.ddayset}[freq] + + if freq < HOURLY: + timeset = self._timeset + else: + gettimeset = {HOURLY: ii.htimeset, + MINUTELY: ii.mtimeset, + SECONDLY: ii.stimeset}[freq] + if ((freq >= HOURLY and + self._byhour and hour not in self._byhour) or + (freq >= MINUTELY and + self._byminute and minute not in self._byminute) or + (freq >= SECONDLY and + self._bysecond and second not in self._bysecond)): + timeset = () + else: + timeset = gettimeset(hour, minute, second) + + total = 0 + count = self._count + while True: + # Get dayset with the right frequency + dayset, start, end = getdayset(year, month, day) + + # Do the "hard" work ;-) + filtered = False + for i in dayset[start:end]: + if ((bymonth and ii.mmask[i] not in bymonth) or + (byweekno and not ii.wnomask[i]) or + (byweekday and ii.wdaymask[i] not in byweekday) or + (ii.nwdaymask and not ii.nwdaymask[i]) or + (byeaster and not ii.eastermask[i]) or + ((bymonthday or bynmonthday) and + ii.mdaymask[i] not in bymonthday and + ii.nmdaymask[i] not in bynmonthday) or + (byyearday and + ((i < ii.yearlen and i+1 not in byyearday and + -ii.yearlen+i not in byyearday) or + (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and + -ii.nextyearlen+i-ii.yearlen not in byyearday)))): + dayset[i] = None + filtered = True + + # Output results + if bysetpos and timeset: + poslist = [] + for pos in bysetpos: + if pos < 0: + daypos, timepos = divmod(pos, len(timeset)) + else: + daypos, timepos = divmod(pos-1, len(timeset)) + try: + i = [x for x in dayset[start:end] + if x is not None][daypos] + time = timeset[timepos] + except IndexError: + pass + else: + date = datetime.date.fromordinal(ii.yearordinal+i) + res = datetime.datetime.combine(date, time) + if res not in poslist: + poslist.append(res) + poslist.sort() + for res in poslist: + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + total += 1 + yield res + else: + for i in dayset[start:end]: + if i is not None: + date = datetime.date.fromordinal(ii.yearordinal + i) + for time in timeset: + res = datetime.datetime.combine(date, time) + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + + total += 1 + yield res + + # Handle frequency and interval + fixday = False + if freq == YEARLY: + year += interval + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == MONTHLY: + month += interval + if month > 12: + div, mod = divmod(month, 12) + month = mod + year += div + if month == 0: + month = 12 + year -= 1 + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == WEEKLY: + if wkst > weekday: + day += -(weekday+1+(6-wkst))+self._interval*7 + else: + day += -(weekday-wkst)+self._interval*7 + weekday = wkst + fixday = True + elif freq == DAILY: + day += interval + fixday = True + elif freq == HOURLY: + if filtered: + # Jump to one iteration before next day + hour += ((23-hour)//interval)*interval + + if byhour: + ndays, hour = self.__mod_distance(value=hour, + byxxx=self._byhour, + base=24) + else: + ndays, hour = divmod(hour+interval, 24) + + if ndays: + day += ndays + fixday = True + + timeset = gettimeset(hour, minute, second) + elif freq == MINUTELY: + if filtered: + # Jump to one iteration before next day + minute += ((1439-(hour*60+minute))//interval)*interval + + valid = False + rep_rate = (24*60) + for j in range(rep_rate // gcd(interval, rep_rate)): + if byminute: + nhours, minute = \ + self.__mod_distance(value=minute, + byxxx=self._byminute, + base=60) + else: + nhours, minute = divmod(minute+interval, 60) + + div, hour = divmod(hour+nhours, 24) + if div: + day += div + fixday = True + filtered = False + + if not byhour or hour in byhour: + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval and ' + + 'byhour resulting in empty rule.') + + timeset = gettimeset(hour, minute, second) + elif freq == SECONDLY: + if filtered: + # Jump to one iteration before next day + second += (((86399 - (hour * 3600 + minute * 60 + second)) + // interval) * interval) + + rep_rate = (24 * 3600) + valid = False + for j in range(0, rep_rate // gcd(interval, rep_rate)): + if bysecond: + nminutes, second = \ + self.__mod_distance(value=second, + byxxx=self._bysecond, + base=60) + else: + nminutes, second = divmod(second+interval, 60) + + div, minute = divmod(minute+nminutes, 60) + if div: + hour += div + div, hour = divmod(hour, 24) + if div: + day += div + fixday = True + + if ((not byhour or hour in byhour) and + (not byminute or minute in byminute) and + (not bysecond or second in bysecond)): + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval, ' + + 'byhour and byminute resulting in empty' + + ' rule.') + + timeset = gettimeset(hour, minute, second) + + if fixday and day > 28: + daysinmonth = calendar.monthrange(year, month)[1] + if day > daysinmonth: + while day > daysinmonth: + day -= daysinmonth + month += 1 + if month == 13: + month = 1 + year += 1 + if year > datetime.MAXYEAR: + self._len = total + return + daysinmonth = calendar.monthrange(year, month)[1] + ii.rebuild(year, month) + + def __construct_byset(self, start, byxxx, base): + """ + If a `BYXXX` sequence is passed to the constructor at the same level as + `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some + specifications which cannot be reached given some starting conditions. + + This occurs whenever the interval is not coprime with the base of a + given unit and the difference between the starting position and the + ending position is not coprime with the greatest common denominator + between the interval and the base. For example, with a FREQ of hourly + starting at 17:00 and an interval of 4, the only valid values for + BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not + coprime. + + :param start: + Specifies the starting position. + :param byxxx: + An iterable containing the list of allowed values. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + This does not preserve the type of the iterable, returning a set, since + the values should be unique and the order is irrelevant, this will + speed up later lookups. + + In the event of an empty set, raises a :exception:`ValueError`, as this + results in an empty rrule. + """ + + cset = set() + + # Support a single byxxx value. + if isinstance(byxxx, integer_types): + byxxx = (byxxx, ) + + for num in byxxx: + i_gcd = gcd(self._interval, base) + # Use divmod rather than % because we need to wrap negative nums. + if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: + cset.add(num) + + if len(cset) == 0: + raise ValueError("Invalid rrule byxxx generates an empty set.") + + return cset + + def __mod_distance(self, value, byxxx, base): + """ + Calculates the next value in a sequence where the `FREQ` parameter is + specified along with a `BYXXX` parameter at the same "level" + (e.g. `HOURLY` specified with `BYHOUR`). + + :param value: + The old value of the component. + :param byxxx: + The `BYXXX` set, which should have been generated by + `rrule._construct_byset`, or something else which checks that a + valid rule is present. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + If a valid value is not found after `base` iterations (the maximum + number before the sequence would start to repeat), this raises a + :exception:`ValueError`, as no valid values were found. + + This returns a tuple of `divmod(n*interval, base)`, where `n` is the + smallest number of `interval` repetitions until the next specified + value in `byxxx` is found. + """ + accumulator = 0 + for ii in range(1, base + 1): + # Using divmod() over % to account for negative intervals + div, value = divmod(value + self._interval, base) + accumulator += div + if value in byxxx: + return (accumulator, value) + + +class _iterinfo(object): + __slots__ = ["rrule", "lastyear", "lastmonth", + "yearlen", "nextyearlen", "yearordinal", "yearweekday", + "mmask", "mrange", "mdaymask", "nmdaymask", + "wdaymask", "wnomask", "nwdaymask", "eastermask"] + + def __init__(self, rrule): + for attr in self.__slots__: + setattr(self, attr, None) + self.rrule = rrule + + def rebuild(self, year, month): + # Every mask is 7 days longer to handle cross-year weekly periods. + rr = self.rrule + if year != self.lastyear: + self.yearlen = 365 + calendar.isleap(year) + self.nextyearlen = 365 + calendar.isleap(year + 1) + firstyday = datetime.date(year, 1, 1) + self.yearordinal = firstyday.toordinal() + self.yearweekday = firstyday.weekday() + + wday = datetime.date(year, 1, 1).weekday() + if self.yearlen == 365: + self.mmask = M365MASK + self.mdaymask = MDAY365MASK + self.nmdaymask = NMDAY365MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M365RANGE + else: + self.mmask = M366MASK + self.mdaymask = MDAY366MASK + self.nmdaymask = NMDAY366MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M366RANGE + + if not rr._byweekno: + self.wnomask = None + else: + self.wnomask = [0]*(self.yearlen+7) + # no1wkst = firstwkst = self.wdaymask.index(rr._wkst) + no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 + if no1wkst >= 4: + no1wkst = 0 + # Number of days in the year, plus the days we got + # from last year. + wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 + else: + # Number of days in the year, minus the days we + # left in last year. + wyearlen = self.yearlen-no1wkst + div, mod = divmod(wyearlen, 7) + numweeks = div+mod//4 + for n in rr._byweekno: + if n < 0: + n += numweeks+1 + if not (0 < n <= numweeks): + continue + if n > 1: + i = no1wkst+(n-1)*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + else: + i = no1wkst + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if 1 in rr._byweekno: + # Check week number 1 of next year as well + # TODO: Check -numweeks for next year. + i = no1wkst+numweeks*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + if i < self.yearlen: + # If week starts in next year, we + # don't care about it. + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if no1wkst: + # Check last week number of last year as + # well. If no1wkst is 0, either the year + # started on week start, or week number 1 + # got days from last year, so there are no + # days from last year's last week number in + # this year. + if -1 not in rr._byweekno: + lyearweekday = datetime.date(year-1, 1, 1).weekday() + lno1wkst = (7-lyearweekday+rr._wkst) % 7 + lyearlen = 365+calendar.isleap(year-1) + if lno1wkst >= 4: + lno1wkst = 0 + lnumweeks = 52+(lyearlen + + (lyearweekday-rr._wkst) % 7) % 7//4 + else: + lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 + else: + lnumweeks = -1 + if lnumweeks in rr._byweekno: + for i in range(no1wkst): + self.wnomask[i] = 1 + + if (rr._bynweekday and (month != self.lastmonth or + year != self.lastyear)): + ranges = [] + if rr._freq == YEARLY: + if rr._bymonth: + for month in rr._bymonth: + ranges.append(self.mrange[month-1:month+1]) + else: + ranges = [(0, self.yearlen)] + elif rr._freq == MONTHLY: + ranges = [self.mrange[month-1:month+1]] + if ranges: + # Weekly frequency won't get here, so we may not + # care about cross-year weekly periods. + self.nwdaymask = [0]*self.yearlen + for first, last in ranges: + last -= 1 + for wday, n in rr._bynweekday: + if n < 0: + i = last+(n+1)*7 + i -= (self.wdaymask[i]-wday) % 7 + else: + i = first+(n-1)*7 + i += (7-self.wdaymask[i]+wday) % 7 + if first <= i <= last: + self.nwdaymask[i] = 1 + + if rr._byeaster: + self.eastermask = [0]*(self.yearlen+7) + eyday = easter.easter(year).toordinal()-self.yearordinal + for offset in rr._byeaster: + self.eastermask[eyday+offset] = 1 + + self.lastyear = year + self.lastmonth = month + + def ydayset(self, year, month, day): + return list(range(self.yearlen)), 0, self.yearlen + + def mdayset(self, year, month, day): + dset = [None]*self.yearlen + start, end = self.mrange[month-1:month+1] + for i in range(start, end): + dset[i] = i + return dset, start, end + + def wdayset(self, year, month, day): + # We need to handle cross-year weeks here. + dset = [None]*(self.yearlen+7) + i = datetime.date(year, month, day).toordinal()-self.yearordinal + start = i + for j in range(7): + dset[i] = i + i += 1 + # if (not (0 <= i < self.yearlen) or + # self.wdaymask[i] == self.rrule._wkst): + # This will cross the year boundary, if necessary. + if self.wdaymask[i] == self.rrule._wkst: + break + return dset, start, i + + def ddayset(self, year, month, day): + dset = [None] * self.yearlen + i = datetime.date(year, month, day).toordinal() - self.yearordinal + dset[i] = i + return dset, i, i + 1 + + def htimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for minute in rr._byminute: + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, + tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def mtimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def stimeset(self, hour, minute, second): + return (datetime.time(hour, minute, second, + tzinfo=self.rrule._tzinfo),) + + +class rruleset(rrulebase): + """ The rruleset type allows more complex recurrence setups, mixing + multiple rules, dates, exclusion rules, and exclusion dates. The type + constructor takes the following keyword arguments: + + :param cache: If True, caching of results will be enabled, improving + performance of multiple queries considerably. """ + + class _genitem(object): + def __init__(self, genlist, gen): + try: + self.dt = advance_iterator(gen) + genlist.append(self) + except StopIteration: + pass + self.genlist = genlist + self.gen = gen + + def __next__(self): + try: + self.dt = advance_iterator(self.gen) + except StopIteration: + if self.genlist[0] is self: + heapq.heappop(self.genlist) + else: + self.genlist.remove(self) + heapq.heapify(self.genlist) + + next = __next__ + + def __lt__(self, other): + return self.dt < other.dt + + def __gt__(self, other): + return self.dt > other.dt + + def __eq__(self, other): + return self.dt == other.dt + + def __ne__(self, other): + return self.dt != other.dt + + def __init__(self, cache=False): + super(rruleset, self).__init__(cache) + self._rrule = [] + self._rdate = [] + self._exrule = [] + self._exdate = [] + + @_invalidates_cache + def rrule(self, rrule): + """ Include the given :py:class:`rrule` instance in the recurrence set + generation. """ + self._rrule.append(rrule) + + @_invalidates_cache + def rdate(self, rdate): + """ Include the given :py:class:`datetime` instance in the recurrence + set generation. """ + self._rdate.append(rdate) + + @_invalidates_cache + def exrule(self, exrule): + """ Include the given rrule instance in the recurrence set exclusion + list. Dates which are part of the given recurrence rules will not + be generated, even if some inclusive rrule or rdate matches them. + """ + self._exrule.append(exrule) + + @_invalidates_cache + def exdate(self, exdate): + """ Include the given datetime instance in the recurrence set + exclusion list. Dates included that way will not be generated, + even if some inclusive rrule or rdate matches them. """ + self._exdate.append(exdate) + + def _iter(self): + rlist = [] + self._rdate.sort() + self._genitem(rlist, iter(self._rdate)) + for gen in [iter(x) for x in self._rrule]: + self._genitem(rlist, gen) + exlist = [] + self._exdate.sort() + self._genitem(exlist, iter(self._exdate)) + for gen in [iter(x) for x in self._exrule]: + self._genitem(exlist, gen) + lastdt = None + total = 0 + heapq.heapify(rlist) + heapq.heapify(exlist) + while rlist: + ritem = rlist[0] + if not lastdt or lastdt != ritem.dt: + while exlist and exlist[0] < ritem: + exitem = exlist[0] + advance_iterator(exitem) + if exlist and exlist[0] is exitem: + heapq.heapreplace(exlist, exitem) + if not exlist or ritem != exlist[0]: + total += 1 + yield ritem.dt + lastdt = ritem.dt + advance_iterator(ritem) + if rlist and rlist[0] is ritem: + heapq.heapreplace(rlist, ritem) + self._len = total + + + + +class _rrulestr(object): + """ Parses a string representation of a recurrence rule or set of + recurrence rules. + + :param s: + Required, a string defining one or more recurrence rules. + + :param dtstart: + If given, used as the default recurrence start if not specified in the + rule string. + + :param cache: + If set ``True`` caching of results will be enabled, improving + performance of multiple queries considerably. + + :param unfold: + If set ``True`` indicates that a rule string is split over more + than one line and should be joined before processing. + + :param forceset: + If set ``True`` forces a :class:`dateutil.rrule.rruleset` to + be returned. + + :param compatible: + If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime.datetime` object is returned. + + :param tzids: + If given, a callable or mapping used to retrieve a + :class:`datetime.tzinfo` from a string representation. + Defaults to :func:`dateutil.tz.gettz`. + + :param tzinfos: + Additional time zone names / aliases which may be present in a string + representation. See :func:`dateutil.parser.parse` for more + information. + + :return: + Returns a :class:`dateutil.rrule.rruleset` or + :class:`dateutil.rrule.rrule` + """ + + _freq_map = {"YEARLY": YEARLY, + "MONTHLY": MONTHLY, + "WEEKLY": WEEKLY, + "DAILY": DAILY, + "HOURLY": HOURLY, + "MINUTELY": MINUTELY, + "SECONDLY": SECONDLY} + + _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, + "FR": 4, "SA": 5, "SU": 6} + + def _handle_int(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = int(value) + + def _handle_int_list(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = [int(x) for x in value.split(',')] + + _handle_INTERVAL = _handle_int + _handle_COUNT = _handle_int + _handle_BYSETPOS = _handle_int_list + _handle_BYMONTH = _handle_int_list + _handle_BYMONTHDAY = _handle_int_list + _handle_BYYEARDAY = _handle_int_list + _handle_BYEASTER = _handle_int_list + _handle_BYWEEKNO = _handle_int_list + _handle_BYHOUR = _handle_int_list + _handle_BYMINUTE = _handle_int_list + _handle_BYSECOND = _handle_int_list + + def _handle_FREQ(self, rrkwargs, name, value, **kwargs): + rrkwargs["freq"] = self._freq_map[value] + + def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): + global parser + if not parser: + from dateutil import parser + try: + rrkwargs["until"] = parser.parse(value, + ignoretz=kwargs.get("ignoretz"), + tzinfos=kwargs.get("tzinfos")) + except ValueError: + raise ValueError("invalid until date") + + def _handle_WKST(self, rrkwargs, name, value, **kwargs): + rrkwargs["wkst"] = self._weekday_map[value] + + def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): + """ + Two ways to specify this: +1MO or MO(+1) + """ + l = [] + for wday in value.split(','): + if '(' in wday: + # If it's of the form TH(+1), etc. + splt = wday.split('(') + w = splt[0] + n = int(splt[1][:-1]) + elif len(wday): + # If it's of the form +1MO + for i in range(len(wday)): + if wday[i] not in '+-0123456789': + break + n = wday[:i] or None + w = wday[i:] + if n: + n = int(n) + else: + raise ValueError("Invalid (empty) BYDAY specification.") + + l.append(weekdays[self._weekday_map[w]](n)) + rrkwargs["byweekday"] = l + + _handle_BYDAY = _handle_BYWEEKDAY + + def _parse_rfc_rrule(self, line, + dtstart=None, + cache=False, + ignoretz=False, + tzinfos=None): + if line.find(':') != -1: + name, value = line.split(':') + if name != "RRULE": + raise ValueError("unknown parameter name") + else: + value = line + rrkwargs = {} + for pair in value.split(';'): + name, value = pair.split('=') + name = name.upper() + value = value.upper() + try: + getattr(self, "_handle_"+name)(rrkwargs, name, value, + ignoretz=ignoretz, + tzinfos=tzinfos) + except AttributeError: + raise ValueError("unknown parameter '%s'" % name) + except (KeyError, ValueError): + raise ValueError("invalid '%s': %s" % (name, value)) + return rrule(dtstart=dtstart, cache=cache, **rrkwargs) + + def _parse_date_value(self, date_value, parms, rule_tzids, + ignoretz, tzids, tzinfos): + global parser + if not parser: + from dateutil import parser + + datevals = [] + value_found = False + TZID = None + + for parm in parms: + if parm.startswith("TZID="): + try: + tzkey = rule_tzids[parm.split('TZID=')[-1]] + except KeyError: + continue + if tzids is None: + from . import tz + tzlookup = tz.gettz + elif callable(tzids): + tzlookup = tzids + else: + tzlookup = getattr(tzids, 'get', None) + if tzlookup is None: + msg = ('tzids must be a callable, mapping, or None, ' + 'not %s' % tzids) + raise ValueError(msg) + + TZID = tzlookup(tzkey) + continue + + # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found + # only once. + if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}: + raise ValueError("unsupported parm: " + parm) + else: + if value_found: + msg = ("Duplicate value parameter found in: " + parm) + raise ValueError(msg) + value_found = True + + for datestr in date_value.split(','): + date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos) + if TZID is not None: + if date.tzinfo is None: + date = date.replace(tzinfo=TZID) + else: + raise ValueError('DTSTART/EXDATE specifies multiple timezone') + datevals.append(date) + + return datevals + + def _parse_rfc(self, s, + dtstart=None, + cache=False, + unfold=False, + forceset=False, + compatible=False, + ignoretz=False, + tzids=None, + tzinfos=None): + global parser + if compatible: + forceset = True + unfold = True + + TZID_NAMES = dict(map( + lambda x: (x.upper(), x), + re.findall('TZID=(?P[^:]+):', s) + )) + s = s.upper() + if not s.strip(): + raise ValueError("empty string") + if unfold: + lines = s.splitlines() + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + else: + lines = s.split() + if (not forceset and len(lines) == 1 and (s.find(':') == -1 or + s.startswith('RRULE:'))): + return self._parse_rfc_rrule(lines[0], cache=cache, + dtstart=dtstart, ignoretz=ignoretz, + tzinfos=tzinfos) + else: + rrulevals = [] + rdatevals = [] + exrulevals = [] + exdatevals = [] + for line in lines: + if not line: + continue + if line.find(':') == -1: + name = "RRULE" + value = line + else: + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0] + parms = parms[1:] + if name == "RRULE": + for parm in parms: + raise ValueError("unsupported RRULE parm: "+parm) + rrulevals.append(value) + elif name == "RDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError("unsupported RDATE parm: "+parm) + rdatevals.append(value) + elif name == "EXRULE": + for parm in parms: + raise ValueError("unsupported EXRULE parm: "+parm) + exrulevals.append(value) + elif name == "EXDATE": + exdatevals.extend( + self._parse_date_value(value, parms, + TZID_NAMES, ignoretz, + tzids, tzinfos) + ) + elif name == "DTSTART": + dtvals = self._parse_date_value(value, parms, TZID_NAMES, + ignoretz, tzids, tzinfos) + if len(dtvals) != 1: + raise ValueError("Multiple DTSTART values specified:" + + value) + dtstart = dtvals[0] + else: + raise ValueError("unsupported property: "+name) + if (forceset or len(rrulevals) > 1 or rdatevals + or exrulevals or exdatevals): + if not parser and (rdatevals or exdatevals): + from dateutil import parser + rset = rruleset(cache=cache) + for value in rrulevals: + rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in rdatevals: + for datestr in value.split(','): + rset.rdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exrulevals: + rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exdatevals: + rset.exdate(value) + if compatible and dtstart: + rset.rdate(dtstart) + return rset + else: + return self._parse_rfc_rrule(rrulevals[0], + dtstart=dtstart, + cache=cache, + ignoretz=ignoretz, + tzinfos=tzinfos) + + def __call__(self, s, **kwargs): + return self._parse_rfc(s, **kwargs) + + +rrulestr = _rrulestr() + +# vim:ts=4:sw=4:et diff --git a/dateutil/tz/__init__.py b/dateutil/tz/__init__.py new file mode 100644 index 0000000..af1352c --- /dev/null +++ b/dateutil/tz/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from .tz import * +from .tz import __doc__ + +__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", + "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz", + "enfold", "datetime_ambiguous", "datetime_exists", + "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"] + + +class DeprecatedTzFormatWarning(Warning): + """Warning raised when time zones are parsed from deprecated formats.""" diff --git a/dateutil/tz/_common.py b/dateutil/tz/_common.py new file mode 100644 index 0000000..e6ac118 --- /dev/null +++ b/dateutil/tz/_common.py @@ -0,0 +1,419 @@ +from six import PY2 + +from functools import wraps + +from datetime import datetime, timedelta, tzinfo + + +ZERO = timedelta(0) + +__all__ = ['tzname_in_python2', 'enfold'] + + +def tzname_in_python2(namefunc): + """Change unicode output into bytestrings in Python 2 + + tzname() API changed in Python 3. It used to return bytes, but was changed + to unicode strings + """ + if PY2: + @wraps(namefunc) + def adjust_encoding(*args, **kwargs): + name = namefunc(*args, **kwargs) + if name is not None: + name = name.encode() + + return name + + return adjust_encoding + else: + return namefunc + + +# The following is adapted from Alexander Belopolsky's tz library +# https://github.com/abalkin/tz +if hasattr(datetime, 'fold'): + # This is the pre-python 3.6 fold situation + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + .. versionadded:: 2.6.0 + """ + return dt.replace(fold=fold) + +else: + class _DatetimeWithFold(datetime): + """ + This is a class designed to provide a PEP 495-compliant interface for + Python versions before 3.6. It is used only for dates in a fold, so + the ``fold`` attribute is fixed at ``1``. + + .. versionadded:: 2.6.0 + """ + __slots__ = () + + def replace(self, *args, **kwargs): + """ + Return a datetime with the same attributes, except for those + attributes given new values by whichever keyword arguments are + specified. Note that tzinfo=None can be specified to create a naive + datetime from an aware datetime with no conversion of date and time + data. + + This is reimplemented in ``_DatetimeWithFold`` because pypy3 will + return a ``datetime.datetime`` even if ``fold`` is unchanged. + """ + argnames = ( + 'year', 'month', 'day', 'hour', 'minute', 'second', + 'microsecond', 'tzinfo' + ) + + for arg, argname in zip(args, argnames): + if argname in kwargs: + raise TypeError('Duplicate argument: {}'.format(argname)) + + kwargs[argname] = arg + + for argname in argnames: + if argname not in kwargs: + kwargs[argname] = getattr(self, argname) + + dt_class = self.__class__ if kwargs.get('fold', 1) else datetime + + return dt_class(**kwargs) + + @property + def fold(self): + return 1 + + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + .. versionadded:: 2.6.0 + """ + if getattr(dt, 'fold', 0) == fold: + return dt + + args = dt.timetuple()[:6] + args += (dt.microsecond, dt.tzinfo) + + if fold: + return _DatetimeWithFold(*args) + else: + return datetime(*args) + + +def _validate_fromutc_inputs(f): + """ + The CPython version of ``fromutc`` checks that the input is a ``datetime`` + object and that ``self`` is attached as its ``tzinfo``. + """ + @wraps(f) + def fromutc(self, dt): + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + return f(self, dt) + + return fromutc + + +class _tzinfo(tzinfo): + """ + Base class for all ``dateutil`` ``tzinfo`` objects. + """ + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + + dt = dt.replace(tzinfo=self) + + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None) + + return same_dt and not same_offset + + def _fold_status(self, dt_utc, dt_wall): + """ + Determine the fold status of a "wall" datetime, given a representation + of the same datetime as a (naive) UTC datetime. This is calculated based + on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all + datetimes, and that this offset is the actual number of hours separating + ``dt_utc`` and ``dt_wall``. + + :param dt_utc: + Representation of the datetime as UTC + + :param dt_wall: + Representation of the datetime as "wall time". This parameter must + either have a `fold` attribute or have a fold-naive + :class:`datetime.tzinfo` attached, otherwise the calculation may + fail. + """ + if self.is_ambiguous(dt_wall): + delta_wall = dt_wall - dt_utc + _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst())) + else: + _fold = 0 + + return _fold + + def _fold(self, dt): + return getattr(dt, 'fold', 0) + + def _fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurrence, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.datetime` object. + """ + + # Re-implement the algorithm from Python's datetime.py + dtoff = dt.utcoffset() + if dtoff is None: + raise ValueError("fromutc() requires a non-None utcoffset() " + "result") + + # The original datetime.py code assumes that `dst()` defaults to + # zero during ambiguous times. PEP 495 inverts this presumption, so + # for pre-PEP 495 versions of python, we need to tweak the algorithm. + dtdst = dt.dst() + if dtdst is None: + raise ValueError("fromutc() requires a non-None dst() result") + delta = dtoff - dtdst + + dt += delta + # Set fold=1 so we can default to being in the fold for + # ambiguous dates. + dtdst = enfold(dt, fold=1).dst() + if dtdst is None: + raise ValueError("fromutc(): dt.dst gave inconsistent " + "results; cannot convert") + return dt + dtdst + + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurrence, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.datetime` object. + """ + dt_wall = self._fromutc(dt) + + # Calculate the fold status given the two datetimes. + _fold = self._fold_status(dt, dt_wall) + + # Set the default fold value for ambiguous dates + return enfold(dt_wall, fold=_fold) + + +class tzrangebase(_tzinfo): + """ + This is an abstract base class for time zones represented by an annual + transition into and out of DST. Child classes should implement the following + methods: + + * ``__init__(self, *args, **kwargs)`` + * ``transitions(self, year)`` - this is expected to return a tuple of + datetimes representing the DST on and off transitions in standard + time. + + A fully initialized ``tzrangebase`` subclass should also provide the + following attributes: + * ``hasdst``: Boolean whether or not the zone uses DST. + * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects + representing the respective UTC offsets. + * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short + abbreviations in DST and STD, respectively. + * ``_hasdst``: Whether or not the zone has DST. + + .. versionadded:: 2.6.0 + """ + def __init__(self): + raise NotImplementedError('tzrangebase is an abstract base class') + + def utcoffset(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_base_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + if self._isdst(dt): + return self._dst_abbr + else: + return self._std_abbr + + def fromutc(self, dt): + """ Given a datetime in UTC, return local time """ + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + # Get transitions - if there are none, fixed offset + transitions = self.transitions(dt.year) + if transitions is None: + return dt + self.utcoffset(dt) + + # Get the transition times in UTC + dston, dstoff = transitions + + dston -= self._std_offset + dstoff -= self._std_offset + + utc_transitions = (dston, dstoff) + dt_utc = dt.replace(tzinfo=None) + + isdst = self._naive_isdst(dt_utc, utc_transitions) + + if isdst: + dt_wall = dt + self._dst_offset + else: + dt_wall = dt + self._std_offset + + _fold = int(not isdst and self.is_ambiguous(dt_wall)) + + return enfold(dt_wall, fold=_fold) + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + if not self.hasdst: + return False + + start, end = self.transitions(dt.year) + + dt = dt.replace(tzinfo=None) + return (end <= dt < end + self._dst_base_offset) + + def _isdst(self, dt): + if not self.hasdst: + return False + elif dt is None: + return None + + transitions = self.transitions(dt.year) + + if transitions is None: + return False + + dt = dt.replace(tzinfo=None) + + isdst = self._naive_isdst(dt, transitions) + + # Handle ambiguous dates + if not isdst and self.is_ambiguous(dt): + return not self._fold(dt) + else: + return isdst + + def _naive_isdst(self, dt, transitions): + dston, dstoff = transitions + + dt = dt.replace(tzinfo=None) + + if dston < dstoff: + isdst = dston <= dt < dstoff + else: + isdst = not dstoff <= dt < dston + + return isdst + + @property + def _dst_base_offset(self): + return self._dst_offset - self._std_offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(...)" % self.__class__.__name__ + + __reduce__ = object.__reduce__ diff --git a/dateutil/tz/_factories.py b/dateutil/tz/_factories.py new file mode 100644 index 0000000..f8a6589 --- /dev/null +++ b/dateutil/tz/_factories.py @@ -0,0 +1,80 @@ +from datetime import timedelta +import weakref +from collections import OrderedDict + +from six.moves import _thread + + +class _TzSingleton(type): + def __init__(cls, *args, **kwargs): + cls.__instance = None + super(_TzSingleton, cls).__init__(*args, **kwargs) + + def __call__(cls): + if cls.__instance is None: + cls.__instance = super(_TzSingleton, cls).__call__() + return cls.__instance + + +class _TzFactory(type): + def instance(cls, *args, **kwargs): + """Alternate constructor that returns a fresh instance""" + return type.__call__(cls, *args, **kwargs) + + +class _TzOffsetFactory(_TzFactory): + def __init__(cls, *args, **kwargs): + cls.__instances = weakref.WeakValueDictionary() + cls.__strong_cache = OrderedDict() + cls.__strong_cache_size = 8 + + cls._cache_lock = _thread.allocate_lock() + + def __call__(cls, name, offset): + if isinstance(offset, timedelta): + key = (name, offset.total_seconds()) + else: + key = (name, offset) + + instance = cls.__instances.get(key, None) + if instance is None: + instance = cls.__instances.setdefault(key, + cls.instance(name, offset)) + + # This lock may not be necessary in Python 3. See GH issue #901 + with cls._cache_lock: + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + # Remove an item if the strong cache is overpopulated + if len(cls.__strong_cache) > cls.__strong_cache_size: + cls.__strong_cache.popitem(last=False) + + return instance + + +class _TzStrFactory(_TzFactory): + def __init__(cls, *args, **kwargs): + cls.__instances = weakref.WeakValueDictionary() + cls.__strong_cache = OrderedDict() + cls.__strong_cache_size = 8 + + cls.__cache_lock = _thread.allocate_lock() + + def __call__(cls, s, posix_offset=False): + key = (s, posix_offset) + instance = cls.__instances.get(key, None) + + if instance is None: + instance = cls.__instances.setdefault(key, + cls.instance(s, posix_offset)) + + # This lock may not be necessary in Python 3. See GH issue #901 + with cls.__cache_lock: + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + # Remove an item if the strong cache is overpopulated + if len(cls.__strong_cache) > cls.__strong_cache_size: + cls.__strong_cache.popitem(last=False) + + return instance + diff --git a/dateutil/tz/tz.py b/dateutil/tz/tz.py new file mode 100644 index 0000000..6175914 --- /dev/null +++ b/dateutil/tz/tz.py @@ -0,0 +1,1849 @@ +# -*- coding: utf-8 -*- +""" +This module offers timezone implementations subclassing the abstract +:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format +files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, +etc), TZ environment string (in all known formats), given ranges (with help +from relative deltas), local machine timezone, fixed offset timezone, and UTC +timezone. +""" +import datetime +import struct +import time +import sys +import os +import bisect +import weakref +from collections import OrderedDict + +import six +from six import string_types +from six.moves import _thread +from ._common import tzname_in_python2, _tzinfo +from ._common import tzrangebase, enfold +from ._common import _validate_fromutc_inputs + +from ._factories import _TzSingleton, _TzOffsetFactory +from ._factories import _TzStrFactory +try: + from .win import tzwin, tzwinlocal +except ImportError: + tzwin = tzwinlocal = None + +# For warning about rounding tzinfo +from warnings import warn + +ZERO = datetime.timedelta(0) +EPOCH = datetime.datetime(1970, 1, 1, 0, 0) +EPOCHORDINAL = EPOCH.toordinal() + + +@six.add_metaclass(_TzSingleton) +class tzutc(datetime.tzinfo): + """ + This is a tzinfo object that represents the UTC time zone. + + **Examples:** + + .. doctest:: + + >>> from datetime import * + >>> from dateutil.tz import * + + >>> datetime.now() + datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) + + >>> datetime.now(tzutc()) + datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) + + >>> datetime.now(tzutc()).tzname() + 'UTC' + + .. versionchanged:: 2.7.0 + ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will + always return the same object. + + .. doctest:: + + >>> from dateutil.tz import tzutc, UTC + >>> tzutc() is tzutc() + True + >>> tzutc() is UTC + True + """ + def utcoffset(self, dt): + return ZERO + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return "UTC" + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Fast track version of fromutc() returns the original ``dt`` object for + any valid :py:class:`datetime.datetime` object. + """ + return dt + + def __eq__(self, other): + if not isinstance(other, (tzutc, tzoffset)): + return NotImplemented + + return (isinstance(other, tzutc) or + (isinstance(other, tzoffset) and other._offset == ZERO)) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +#: Convenience constant providing a :class:`tzutc()` instance +#: +#: .. versionadded:: 2.7.0 +UTC = tzutc() + + +@six.add_metaclass(_TzOffsetFactory) +class tzoffset(datetime.tzinfo): + """ + A simple class for representing a fixed offset from UTC. + + :param name: + The timezone name, to be returned when ``tzname()`` is called. + :param offset: + The time zone offset in seconds, or (since version 2.6.0, represented + as a :py:class:`datetime.timedelta` object). + """ + def __init__(self, name, offset): + self._name = name + + try: + # Allow a timedelta + offset = offset.total_seconds() + except (TypeError, AttributeError): + pass + + self._offset = datetime.timedelta(seconds=_get_supported_offset(offset)) + + def utcoffset(self, dt): + return self._offset + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._name + + @_validate_fromutc_inputs + def fromutc(self, dt): + return dt + self._offset + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + + def __eq__(self, other): + if not isinstance(other, tzoffset): + return NotImplemented + + return self._offset == other._offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s, %s)" % (self.__class__.__name__, + repr(self._name), + int(self._offset.total_seconds())) + + __reduce__ = object.__reduce__ + + +class tzlocal(_tzinfo): + """ + A :class:`tzinfo` subclass built around the ``time`` timezone functions. + """ + def __init__(self): + super(tzlocal, self).__init__() + + self._std_offset = datetime.timedelta(seconds=-time.timezone) + if time.daylight: + self._dst_offset = datetime.timedelta(seconds=-time.altzone) + else: + self._dst_offset = self._std_offset + + self._dst_saved = self._dst_offset - self._std_offset + self._hasdst = bool(self._dst_saved) + self._tznames = tuple(time.tzname) + + def utcoffset(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset - self._std_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._tznames[self._isdst(dt)] + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + naive_dst = self._naive_is_dst(dt) + return (not naive_dst and + (naive_dst != self._naive_is_dst(dt - self._dst_saved))) + + def _naive_is_dst(self, dt): + timestamp = _datetime_to_timestamp(dt) + return time.localtime(timestamp + time.timezone).tm_isdst + + def _isdst(self, dt, fold_naive=True): + # We can't use mktime here. It is unstable when deciding if + # the hour near to a change is DST or not. + # + # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, + # dt.minute, dt.second, dt.weekday(), 0, -1)) + # return time.localtime(timestamp).tm_isdst + # + # The code above yields the following result: + # + # >>> import tz, datetime + # >>> t = tz.tzlocal() + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # + # Here is a more stable implementation: + # + if not self._hasdst: + return False + + # Check for ambiguous times: + dstval = self._naive_is_dst(dt) + fold = getattr(dt, 'fold', None) + + if self.is_ambiguous(dt): + if fold is not None: + return not self._fold(dt) + else: + return True + + return dstval + + def __eq__(self, other): + if isinstance(other, tzlocal): + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset) + elif isinstance(other, tzutc): + return (not self._hasdst and + self._tznames[0] in {'UTC', 'GMT'} and + self._std_offset == ZERO) + elif isinstance(other, tzoffset): + return (not self._hasdst and + self._tznames[0] == other._name and + self._std_offset == other._offset) + else: + return NotImplemented + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +class _ttinfo(object): + __slots__ = ["offset", "delta", "isdst", "abbr", + "isstd", "isgmt", "dstoffset"] + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def __repr__(self): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) + + def __eq__(self, other): + if not isinstance(other, _ttinfo): + return NotImplemented + + return (self.offset == other.offset and + self.delta == other.delta and + self.isdst == other.isdst and + self.abbr == other.abbr and + self.isstd == other.isstd and + self.isgmt == other.isgmt and + self.dstoffset == other.dstoffset) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __getstate__(self): + state = {} + for name in self.__slots__: + state[name] = getattr(self, name, None) + return state + + def __setstate__(self, state): + for name in self.__slots__: + if name in state: + setattr(self, name, state[name]) + + +class _tzfile(object): + """ + Lightweight class for holding the relevant transition and time zone + information read from binary tzfiles. + """ + attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list', + 'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first'] + + def __init__(self, **kwargs): + for attr in self.attrs: + setattr(self, attr, kwargs.get(attr, None)) + + +class tzfile(_tzinfo): + """ + This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)`` + format timezone files to extract current and historical zone information. + + :param fileobj: + This can be an opened file stream or a file name that the time zone + information can be read from. + + :param filename: + This is an optional parameter specifying the source of the time zone + information in the event that ``fileobj`` is a file object. If omitted + and ``fileobj`` is a file stream, this parameter will be set either to + ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``. + + See `Sources for Time Zone and Daylight Saving Time Data + `_ for more information. + Time zone files can be compiled from the `IANA Time Zone database files + `_ with the `zic time zone compiler + `_ + + .. note:: + + Only construct a ``tzfile`` directly if you have a specific timezone + file on disk that you want to read into a Python ``tzinfo`` object. + If you want to get a ``tzfile`` representing a specific IANA zone, + (e.g. ``'America/New_York'``), you should call + :func:`dateutil.tz.gettz` with the zone identifier. + + + **Examples:** + + Using the US Eastern time zone as an example, we can see that a ``tzfile`` + provides time zone information for the standard Daylight Saving offsets: + + .. testsetup:: tzfile + + from dateutil.tz import gettz + from datetime import datetime + + .. doctest:: tzfile + + >>> NYC = gettz('America/New_York') + >>> NYC + tzfile('/usr/share/zoneinfo/America/New_York') + + >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST + 2016-01-03 00:00:00-05:00 + + >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT + 2016-07-07 00:00:00-04:00 + + + The ``tzfile`` structure contains a fully history of the time zone, + so historical dates will also have the right offsets. For example, before + the adoption of the UTC standards, New York used local solar mean time: + + .. doctest:: tzfile + + >>> print(datetime(1901, 4, 12, tzinfo=NYC)) # LMT + 1901-04-12 00:00:00-04:56 + + And during World War II, New York was on "Eastern War Time", which was a + state of permanent daylight saving time: + + .. doctest:: tzfile + + >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT + 1944-02-07 00:00:00-04:00 + + """ + + def __init__(self, fileobj, filename=None): + super(tzfile, self).__init__() + + file_opened_here = False + if isinstance(fileobj, string_types): + self._filename = fileobj + fileobj = open(fileobj, 'rb') + file_opened_here = True + elif filename is not None: + self._filename = filename + elif hasattr(fileobj, "name"): + self._filename = fileobj.name + else: + self._filename = repr(fileobj) + + if fileobj is not None: + if not file_opened_here: + fileobj = _nullcontext(fileobj) + + with fileobj as file_stream: + tzobj = self._read_tzfile(file_stream) + + self._set_tzdata(tzobj) + + def _set_tzdata(self, tzobj): + """ Set the time zone data of this object from a _tzfile object """ + # Copy the relevant attributes over as private attributes + for attr in _tzfile.attrs: + setattr(self, '_' + attr, getattr(tzobj, attr)) + + def _read_tzfile(self, fileobj): + out = _tzfile() + + # From tzfile(5): + # + # The time zone information files used by tzset(3) + # begin with the magic characters "TZif" to identify + # them as time zone information files, followed by + # sixteen bytes reserved for future use, followed by + # six four-byte values of type long, written in a + # ``standard'' byte order (the high-order byte + # of the value is written first). + if fileobj.read(4).decode() != "TZif": + raise ValueError("magic not found") + + fileobj.read(16) + + ( + # The number of UTC/local indicators stored in the file. + ttisgmtcnt, + + # The number of standard/wall indicators stored in the file. + ttisstdcnt, + + # The number of leap seconds for which data is + # stored in the file. + leapcnt, + + # The number of "transition times" for which data + # is stored in the file. + timecnt, + + # The number of "local time types" for which data + # is stored in the file (must not be zero). + typecnt, + + # The number of characters of "time zone + # abbreviation strings" stored in the file. + charcnt, + + ) = struct.unpack(">6l", fileobj.read(24)) + + # The above header is followed by tzh_timecnt four-byte + # values of type long, sorted in ascending order. + # These values are written in ``standard'' byte order. + # Each is used as a transition time (as returned by + # time(2)) at which the rules for computing local time + # change. + + if timecnt: + out.trans_list_utc = list(struct.unpack(">%dl" % timecnt, + fileobj.read(timecnt*4))) + else: + out.trans_list_utc = [] + + # Next come tzh_timecnt one-byte values of type unsigned + # char; each one tells which of the different types of + # ``local time'' types described in the file is associated + # with the same-indexed transition time. These values + # serve as indices into an array of ttinfo structures that + # appears next in the file. + + if timecnt: + out.trans_idx = struct.unpack(">%dB" % timecnt, + fileobj.read(timecnt)) + else: + out.trans_idx = [] + + # Each ttinfo structure is written as a four-byte value + # for tt_gmtoff of type long, in a standard byte + # order, followed by a one-byte value for tt_isdst + # and a one-byte value for tt_abbrind. In each + # structure, tt_gmtoff gives the number of + # seconds to be added to UTC, tt_isdst tells whether + # tm_isdst should be set by localtime(3), and + # tt_abbrind serves as an index into the array of + # time zone abbreviation characters that follow the + # ttinfo structure(s) in the file. + + ttinfo = [] + + for i in range(typecnt): + ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) + + abbr = fileobj.read(charcnt).decode() + + # Then there are tzh_leapcnt pairs of four-byte + # values, written in standard byte order; the + # first value of each pair gives the time (as + # returned by time(2)) at which a leap second + # occurs; the second gives the total number of + # leap seconds to be applied after the given time. + # The pairs of values are sorted in ascending order + # by time. + + # Not used, for now (but seek for correct file position) + if leapcnt: + fileobj.seek(leapcnt * 8, os.SEEK_CUR) + + # Then there are tzh_ttisstdcnt standard/wall + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as standard + # time or wall clock time, and are used when + # a time zone file is used in handling POSIX-style + # time zone environment variables. + + if ttisstdcnt: + isstd = struct.unpack(">%db" % ttisstdcnt, + fileobj.read(ttisstdcnt)) + + # Finally, there are tzh_ttisgmtcnt UTC/local + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as UTC or + # local time, and are used when a time zone file + # is used in handling POSIX-style time zone envi- + # ronment variables. + + if ttisgmtcnt: + isgmt = struct.unpack(">%db" % ttisgmtcnt, + fileobj.read(ttisgmtcnt)) + + # Build ttinfo list + out.ttinfo_list = [] + for i in range(typecnt): + gmtoff, isdst, abbrind = ttinfo[i] + gmtoff = _get_supported_offset(gmtoff) + tti = _ttinfo() + tti.offset = gmtoff + tti.dstoffset = datetime.timedelta(0) + tti.delta = datetime.timedelta(seconds=gmtoff) + tti.isdst = isdst + tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] + tti.isstd = (ttisstdcnt > i and isstd[i] != 0) + tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) + out.ttinfo_list.append(tti) + + # Replace ttinfo indexes for ttinfo objects. + out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx] + + # Set standard, dst, and before ttinfos. before will be + # used when a given time is before any transitions, + # and will be set to the first non-dst ttinfo, or to + # the first dst, if all of them are dst. + out.ttinfo_std = None + out.ttinfo_dst = None + out.ttinfo_before = None + if out.ttinfo_list: + if not out.trans_list_utc: + out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0] + else: + for i in range(timecnt-1, -1, -1): + tti = out.trans_idx[i] + if not out.ttinfo_std and not tti.isdst: + out.ttinfo_std = tti + elif not out.ttinfo_dst and tti.isdst: + out.ttinfo_dst = tti + + if out.ttinfo_std and out.ttinfo_dst: + break + else: + if out.ttinfo_dst and not out.ttinfo_std: + out.ttinfo_std = out.ttinfo_dst + + for tti in out.ttinfo_list: + if not tti.isdst: + out.ttinfo_before = tti + break + else: + out.ttinfo_before = out.ttinfo_list[0] + + # Now fix transition times to become relative to wall time. + # + # I'm not sure about this. In my tests, the tz source file + # is setup to wall time, and in the binary file isstd and + # isgmt are off, so it should be in wall time. OTOH, it's + # always in gmt time. Let me know if you have comments + # about this. + lastdst = None + lastoffset = None + lastdstoffset = None + lastbaseoffset = None + out.trans_list = [] + + for i, tti in enumerate(out.trans_idx): + offset = tti.offset + dstoffset = 0 + + if lastdst is not None: + if tti.isdst: + if not lastdst: + dstoffset = offset - lastoffset + + if not dstoffset and lastdstoffset: + dstoffset = lastdstoffset + + tti.dstoffset = datetime.timedelta(seconds=dstoffset) + lastdstoffset = dstoffset + + # If a time zone changes its base offset during a DST transition, + # then you need to adjust by the previous base offset to get the + # transition time in local time. Otherwise you use the current + # base offset. Ideally, I would have some mathematical proof of + # why this is true, but I haven't really thought about it enough. + baseoffset = offset - dstoffset + adjustment = baseoffset + if (lastbaseoffset is not None and baseoffset != lastbaseoffset + and tti.isdst != lastdst): + # The base DST has changed + adjustment = lastbaseoffset + + lastdst = tti.isdst + lastoffset = offset + lastbaseoffset = baseoffset + + out.trans_list.append(out.trans_list_utc[i] + adjustment) + + out.trans_idx = tuple(out.trans_idx) + out.trans_list = tuple(out.trans_list) + out.trans_list_utc = tuple(out.trans_list_utc) + + return out + + def _find_last_transition(self, dt, in_utc=False): + # If there's no list, there are no transitions to find + if not self._trans_list: + return None + + timestamp = _datetime_to_timestamp(dt) + + # Find where the timestamp fits in the transition list - if the + # timestamp is a transition time, it's part of the "after" period. + trans_list = self._trans_list_utc if in_utc else self._trans_list + idx = bisect.bisect_right(trans_list, timestamp) + + # We want to know when the previous transition was, so subtract off 1 + return idx - 1 + + def _get_ttinfo(self, idx): + # For no list or after the last transition, default to _ttinfo_std + if idx is None or (idx + 1) >= len(self._trans_list): + return self._ttinfo_std + + # If there is a list and the time is before it, return _ttinfo_before + if idx < 0: + return self._ttinfo_before + + return self._trans_idx[idx] + + def _find_ttinfo(self, dt): + idx = self._resolve_ambiguous_time(dt) + + return self._get_ttinfo(idx) + + def fromutc(self, dt): + """ + The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`. + + :param dt: + A :py:class:`datetime.datetime` object. + + :raises TypeError: + Raised if ``dt`` is not a :py:class:`datetime.datetime` object. + + :raises ValueError: + Raised if this is called with a ``dt`` which does not have this + ``tzinfo`` attached. + + :return: + Returns a :py:class:`datetime.datetime` object representing the + wall time in ``self``'s time zone. + """ + # These isinstance checks are in datetime.tzinfo, so we'll preserve + # them, even if we don't care about duck typing. + if not isinstance(dt, datetime.datetime): + raise TypeError("fromutc() requires a datetime argument") + + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + # First treat UTC as wall time and get the transition we're in. + idx = self._find_last_transition(dt, in_utc=True) + tti = self._get_ttinfo(idx) + + dt_out = dt + datetime.timedelta(seconds=tti.offset) + + fold = self.is_ambiguous(dt_out, idx=idx) + + return enfold(dt_out, fold=int(fold)) + + def is_ambiguous(self, dt, idx=None): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + if idx is None: + idx = self._find_last_transition(dt) + + # Calculate the difference in offsets from current to previous + timestamp = _datetime_to_timestamp(dt) + tti = self._get_ttinfo(idx) + + if idx is None or idx <= 0: + return False + + od = self._get_ttinfo(idx - 1).offset - tti.offset + tt = self._trans_list[idx] # Transition time + + return timestamp < tt + od + + def _resolve_ambiguous_time(self, dt): + idx = self._find_last_transition(dt) + + # If we have no transitions, return the index + _fold = self._fold(dt) + if idx is None or idx == 0: + return idx + + # If it's ambiguous and we're in a fold, shift to a different index. + idx_offset = int(not _fold and self.is_ambiguous(dt, idx)) + + return idx - idx_offset + + def utcoffset(self, dt): + if dt is None: + return None + + if not self._ttinfo_std: + return ZERO + + return self._find_ttinfo(dt).delta + + def dst(self, dt): + if dt is None: + return None + + if not self._ttinfo_dst: + return ZERO + + tti = self._find_ttinfo(dt) + + if not tti.isdst: + return ZERO + + # The documentation says that utcoffset()-dst() must + # be constant for every dt. + return tti.dstoffset + + @tzname_in_python2 + def tzname(self, dt): + if not self._ttinfo_std or dt is None: + return None + return self._find_ttinfo(dt).abbr + + def __eq__(self, other): + if not isinstance(other, tzfile): + return NotImplemented + return (self._trans_list == other._trans_list and + self._trans_idx == other._trans_idx and + self._ttinfo_list == other._ttinfo_list) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) + + def __reduce__(self): + return self.__reduce_ex__(None) + + def __reduce_ex__(self, protocol): + return (self.__class__, (None, self._filename), self.__dict__) + + +class tzrange(tzrangebase): + """ + The ``tzrange`` object is a time zone specified by a set of offsets and + abbreviations, equivalent to the way the ``TZ`` variable can be specified + in POSIX-like systems, but using Python delta objects to specify DST + start, end and offsets. + + :param stdabbr: + The abbreviation for standard time (e.g. ``'EST'``). + + :param stdoffset: + An integer or :class:`datetime.timedelta` object or equivalent + specifying the base offset from UTC. + + If unspecified, +00:00 is used. + + :param dstabbr: + The abbreviation for DST / "Summer" time (e.g. ``'EDT'``). + + If specified, with no other DST information, DST is assumed to occur + and the default behavior or ``dstoffset``, ``start`` and ``end`` is + used. If unspecified and no other DST information is specified, it + is assumed that this zone has no DST. + + If this is unspecified and other DST information is *is* specified, + DST occurs in the zone but the time zone abbreviation is left + unchanged. + + :param dstoffset: + A an integer or :class:`datetime.timedelta` object or equivalent + specifying the UTC offset during DST. If unspecified and any other DST + information is specified, it is assumed to be the STD offset +1 hour. + + :param start: + A :class:`relativedelta.relativedelta` object or equivalent specifying + the time and time of year that daylight savings time starts. To + specify, for example, that DST starts at 2AM on the 2nd Sunday in + March, pass: + + ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` + + If unspecified and any other DST information is specified, the default + value is 2 AM on the first Sunday in April. + + :param end: + A :class:`relativedelta.relativedelta` object or equivalent + representing the time and time of year that daylight savings time + ends, with the same specification method as in ``start``. One note is + that this should point to the first time in the *standard* zone, so if + a transition occurs at 2AM in the DST zone and the clocks are set back + 1 hour to 1AM, set the ``hours`` parameter to +1. + + + **Examples:** + + .. testsetup:: tzrange + + from dateutil.tz import tzrange, tzstr + + .. doctest:: tzrange + + >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") + True + + >>> from dateutil.relativedelta import * + >>> range1 = tzrange("EST", -18000, "EDT") + >>> range2 = tzrange("EST", -18000, "EDT", -14400, + ... relativedelta(hours=+2, month=4, day=1, + ... weekday=SU(+1)), + ... relativedelta(hours=+1, month=10, day=31, + ... weekday=SU(-1))) + >>> tzstr('EST5EDT') == range1 == range2 + True + + """ + def __init__(self, stdabbr, stdoffset=None, + dstabbr=None, dstoffset=None, + start=None, end=None): + + global relativedelta + from dateutil import relativedelta + + self._std_abbr = stdabbr + self._dst_abbr = dstabbr + + try: + stdoffset = stdoffset.total_seconds() + except (TypeError, AttributeError): + pass + + try: + dstoffset = dstoffset.total_seconds() + except (TypeError, AttributeError): + pass + + if stdoffset is not None: + self._std_offset = datetime.timedelta(seconds=stdoffset) + else: + self._std_offset = ZERO + + if dstoffset is not None: + self._dst_offset = datetime.timedelta(seconds=dstoffset) + elif dstabbr and stdoffset is not None: + self._dst_offset = self._std_offset + datetime.timedelta(hours=+1) + else: + self._dst_offset = ZERO + + if dstabbr and start is None: + self._start_delta = relativedelta.relativedelta( + hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) + else: + self._start_delta = start + + if dstabbr and end is None: + self._end_delta = relativedelta.relativedelta( + hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) + else: + self._end_delta = end + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = bool(self._start_delta) + + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. + + :param year: + The year whose transitions you would like to query. + + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + if not self.hasdst: + return None + + base_year = datetime.datetime(year, 1, 1) + + start = base_year + self._start_delta + end = base_year + self._end_delta + + return (start, end) + + def __eq__(self, other): + if not isinstance(other, tzrange): + return NotImplemented + + return (self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr and + self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._start_delta == other._start_delta and + self._end_delta == other._end_delta) + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ + + +@six.add_metaclass(_TzStrFactory) +class tzstr(tzrange): + """ + ``tzstr`` objects are time zone objects specified by a time-zone string as + it would be passed to a ``TZ`` variable on POSIX-style systems (see + the `GNU C Library: TZ Variable`_ for more details). + + There is one notable exception, which is that POSIX-style time zones use an + inverted offset format, so normally ``GMT+3`` would be parsed as an offset + 3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an + offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX + behavior, pass a ``True`` value to ``posix_offset``. + + The :class:`tzrange` object provides the same functionality, but is + specified using :class:`relativedelta.relativedelta` objects. rather than + strings. + + :param s: + A time zone string in ``TZ`` variable format. This can be a + :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: + :class:`unicode`) or a stream emitting unicode characters + (e.g. :class:`StringIO`). + + :param posix_offset: + Optional. If set to ``True``, interpret strings such as ``GMT+3`` or + ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the + POSIX standard. + + .. caution:: + + Prior to version 2.7.0, this function also supported time zones + in the format: + + * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600`` + * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600`` + + This format is non-standard and has been deprecated; this function + will raise a :class:`DeprecatedTZFormatWarning` until + support is removed in a future version. + + .. _`GNU C Library: TZ Variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + """ + def __init__(self, s, posix_offset=False): + global parser + from dateutil.parser import _parser as parser + + self._s = s + + res = parser._parsetz(s) + if res is None or res.any_unused_tokens: + raise ValueError("unknown string format") + + # Here we break the compatibility with the TZ variable handling. + # GMT-3 actually *means* the timezone -3. + if res.stdabbr in ("GMT", "UTC") and not posix_offset: + res.stdoffset *= -1 + + # We must initialize it first, since _delta() needs + # _std_offset and _dst_offset set. Use False in start/end + # to avoid building it two times. + tzrange.__init__(self, res.stdabbr, res.stdoffset, + res.dstabbr, res.dstoffset, + start=False, end=False) + + if not res.dstabbr: + self._start_delta = None + self._end_delta = None + else: + self._start_delta = self._delta(res.start) + if self._start_delta: + self._end_delta = self._delta(res.end, isend=1) + + self.hasdst = bool(self._start_delta) + + def _delta(self, x, isend=0): + from dateutil import relativedelta + kwargs = {} + if x.month is not None: + kwargs["month"] = x.month + if x.weekday is not None: + kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) + if x.week > 0: + kwargs["day"] = 1 + else: + kwargs["day"] = 31 + elif x.day: + kwargs["day"] = x.day + elif x.yday is not None: + kwargs["yearday"] = x.yday + elif x.jyday is not None: + kwargs["nlyearday"] = x.jyday + if not kwargs: + # Default is to start on first sunday of april, and end + # on last sunday of october. + if not isend: + kwargs["month"] = 4 + kwargs["day"] = 1 + kwargs["weekday"] = relativedelta.SU(+1) + else: + kwargs["month"] = 10 + kwargs["day"] = 31 + kwargs["weekday"] = relativedelta.SU(-1) + if x.time is not None: + kwargs["seconds"] = x.time + else: + # Default is 2AM. + kwargs["seconds"] = 7200 + if isend: + # Convert to standard time, to follow the documented way + # of working with the extra hour. See the documentation + # of the tzinfo class. + delta = self._dst_offset - self._std_offset + kwargs["seconds"] -= delta.seconds + delta.days * 86400 + return relativedelta.relativedelta(**kwargs) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +class _tzicalvtzcomp(object): + def __init__(self, tzoffsetfrom, tzoffsetto, isdst, + tzname=None, rrule=None): + self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) + self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) + self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom + self.isdst = isdst + self.tzname = tzname + self.rrule = rrule + + +class _tzicalvtz(_tzinfo): + def __init__(self, tzid, comps=[]): + super(_tzicalvtz, self).__init__() + + self._tzid = tzid + self._comps = comps + self._cachedate = [] + self._cachecomp = [] + self._cache_lock = _thread.allocate_lock() + + def _find_comp(self, dt): + if len(self._comps) == 1: + return self._comps[0] + + dt = dt.replace(tzinfo=None) + + try: + with self._cache_lock: + return self._cachecomp[self._cachedate.index( + (dt, self._fold(dt)))] + except ValueError: + pass + + lastcompdt = None + lastcomp = None + + for comp in self._comps: + compdt = self._find_compdt(comp, dt) + + if compdt and (not lastcompdt or lastcompdt < compdt): + lastcompdt = compdt + lastcomp = comp + + if not lastcomp: + # RFC says nothing about what to do when a given + # time is before the first onset date. We'll look for the + # first standard component, or the first component, if + # none is found. + for comp in self._comps: + if not comp.isdst: + lastcomp = comp + break + else: + lastcomp = comp[0] + + with self._cache_lock: + self._cachedate.insert(0, (dt, self._fold(dt))) + self._cachecomp.insert(0, lastcomp) + + if len(self._cachedate) > 10: + self._cachedate.pop() + self._cachecomp.pop() + + return lastcomp + + def _find_compdt(self, comp, dt): + if comp.tzoffsetdiff < ZERO and self._fold(dt): + dt -= comp.tzoffsetdiff + + compdt = comp.rrule.before(dt, inc=True) + + return compdt + + def utcoffset(self, dt): + if dt is None: + return None + + return self._find_comp(dt).tzoffsetto + + def dst(self, dt): + comp = self._find_comp(dt) + if comp.isdst: + return comp.tzoffsetdiff + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._find_comp(dt).tzname + + def __repr__(self): + return "" % repr(self._tzid) + + __reduce__ = object.__reduce__ + + +class tzical(object): + """ + This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure + as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects. + + :param `fileobj`: + A file or stream in iCalendar format, which should be UTF-8 encoded + with CRLF endings. + + .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 + """ + def __init__(self, fileobj): + global rrule + from dateutil import rrule + + if isinstance(fileobj, string_types): + self._s = fileobj + # ical should be encoded in UTF-8 with CRLF + fileobj = open(fileobj, 'r') + else: + self._s = getattr(fileobj, 'name', repr(fileobj)) + fileobj = _nullcontext(fileobj) + + self._vtz = {} + + with fileobj as fobj: + self._parse_rfc(fobj.read()) + + def keys(self): + """ + Retrieves the available time zones as a list. + """ + return list(self._vtz.keys()) + + def get(self, tzid=None): + """ + Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``. + + :param tzid: + If there is exactly one time zone available, omitting ``tzid`` + or passing :py:const:`None` value returns it. Otherwise a valid + key (which can be retrieved from :func:`keys`) is required. + + :raises ValueError: + Raised if ``tzid`` is not specified but there are either more + or fewer than 1 zone defined. + + :returns: + Returns either a :py:class:`datetime.tzinfo` object representing + the relevant time zone or :py:const:`None` if the ``tzid`` was + not found. + """ + if tzid is None: + if len(self._vtz) == 0: + raise ValueError("no timezones defined") + elif len(self._vtz) > 1: + raise ValueError("more than one timezone available") + tzid = next(iter(self._vtz)) + + return self._vtz.get(tzid) + + def _parse_offset(self, s): + s = s.strip() + if not s: + raise ValueError("empty offset") + if s[0] in ('+', '-'): + signal = (-1, +1)[s[0] == '+'] + s = s[1:] + else: + signal = +1 + if len(s) == 4: + return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal + elif len(s) == 6: + return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal + else: + raise ValueError("invalid offset: " + s) + + def _parse_rfc(self, s): + lines = s.splitlines() + if not lines: + raise ValueError("empty string") + + # Unfold + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + + tzid = None + comps = [] + invtz = False + comptype = None + for line in lines: + if not line: + continue + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0].upper() + parms = parms[1:] + if invtz: + if name == "BEGIN": + if value in ("STANDARD", "DAYLIGHT"): + # Process component + pass + else: + raise ValueError("unknown component: "+value) + comptype = value + founddtstart = False + tzoffsetfrom = None + tzoffsetto = None + rrulelines = [] + tzname = None + elif name == "END": + if value == "VTIMEZONE": + if comptype: + raise ValueError("component not closed: "+comptype) + if not tzid: + raise ValueError("mandatory TZID not found") + if not comps: + raise ValueError( + "at least one component is needed") + # Process vtimezone + self._vtz[tzid] = _tzicalvtz(tzid, comps) + invtz = False + elif value == comptype: + if not founddtstart: + raise ValueError("mandatory DTSTART not found") + if tzoffsetfrom is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + if tzoffsetto is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + # Process component + rr = None + if rrulelines: + rr = rrule.rrulestr("\n".join(rrulelines), + compatible=True, + ignoretz=True, + cache=True) + comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, + (comptype == "DAYLIGHT"), + tzname, rr) + comps.append(comp) + comptype = None + else: + raise ValueError("invalid component end: "+value) + elif comptype: + if name == "DTSTART": + # DTSTART in VTIMEZONE takes a subset of valid RRULE + # values under RFC 5545. + for parm in parms: + if parm != 'VALUE=DATE-TIME': + msg = ('Unsupported DTSTART param in ' + + 'VTIMEZONE: ' + parm) + raise ValueError(msg) + rrulelines.append(line) + founddtstart = True + elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): + rrulelines.append(line) + elif name == "TZOFFSETFROM": + if parms: + raise ValueError( + "unsupported %s parm: %s " % (name, parms[0])) + tzoffsetfrom = self._parse_offset(value) + elif name == "TZOFFSETTO": + if parms: + raise ValueError( + "unsupported TZOFFSETTO parm: "+parms[0]) + tzoffsetto = self._parse_offset(value) + elif name == "TZNAME": + if parms: + raise ValueError( + "unsupported TZNAME parm: "+parms[0]) + tzname = value + elif name == "COMMENT": + pass + else: + raise ValueError("unsupported property: "+name) + else: + if name == "TZID": + if parms: + raise ValueError( + "unsupported TZID parm: "+parms[0]) + tzid = value + elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): + pass + else: + raise ValueError("unsupported property: "+name) + elif name == "BEGIN" and value == "VTIMEZONE": + tzid = None + comps = [] + invtz = True + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +if sys.platform != "win32": + TZFILES = ["/etc/localtime", "localtime"] + TZPATHS = ["/usr/share/zoneinfo", + "/usr/lib/zoneinfo", + "/usr/share/lib/zoneinfo", + "/etc/zoneinfo"] +else: + TZFILES = [] + TZPATHS = [] + + +def __get_gettz(): + tzlocal_classes = (tzlocal,) + if tzwinlocal is not None: + tzlocal_classes += (tzwinlocal,) + + class GettzFunc(object): + """ + Retrieve a time zone object from a string representation + + This function is intended to retrieve the :py:class:`tzinfo` subclass + that best represents the time zone that would be used if a POSIX + `TZ variable`_ were set to the same value. + + If no argument or an empty string is passed to ``gettz``, local time + is returned: + + .. code-block:: python3 + + >>> gettz() + tzfile('/etc/localtime') + + This function is also the preferred way to map IANA tz database keys + to :class:`tzfile` objects: + + .. code-block:: python3 + + >>> gettz('Pacific/Kiritimati') + tzfile('/usr/share/zoneinfo/Pacific/Kiritimati') + + On Windows, the standard is extended to include the Windows-specific + zone names provided by the operating system: + + .. code-block:: python3 + + >>> gettz('Egypt Standard Time') + tzwin('Egypt Standard Time') + + Passing a GNU ``TZ`` style string time zone specification returns a + :class:`tzstr` object: + + .. code-block:: python3 + + >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + + :param name: + A time zone name (IANA, or, on Windows, Windows keys), location of + a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone + specifier. An empty string, no argument or ``None`` is interpreted + as local time. + + :return: + Returns an instance of one of ``dateutil``'s :py:class:`tzinfo` + subclasses. + + .. versionchanged:: 2.7.0 + + After version 2.7.0, any two calls to ``gettz`` using the same + input strings will return the same object: + + .. code-block:: python3 + + >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago') + True + + In addition to improving performance, this ensures that + `"same zone" semantics`_ are used for datetimes in the same zone. + + + .. _`TZ variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + + .. _`"same zone" semantics`: + https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html + """ + def __init__(self): + + self.__instances = weakref.WeakValueDictionary() + self.__strong_cache_size = 8 + self.__strong_cache = OrderedDict() + self._cache_lock = _thread.allocate_lock() + + def __call__(self, name=None): + with self._cache_lock: + rv = self.__instances.get(name, None) + + if rv is None: + rv = self.nocache(name=name) + if not (name is None + or isinstance(rv, tzlocal_classes) + or rv is None): + # tzlocal is slightly more complicated than the other + # time zone providers because it depends on environment + # at construction time, so don't cache that. + # + # We also cannot store weak references to None, so we + # will also not store that. + self.__instances[name] = rv + else: + # No need for strong caching, return immediately + return rv + + self.__strong_cache[name] = self.__strong_cache.pop(name, rv) + + if len(self.__strong_cache) > self.__strong_cache_size: + self.__strong_cache.popitem(last=False) + + return rv + + def set_cache_size(self, size): + with self._cache_lock: + self.__strong_cache_size = size + while len(self.__strong_cache) > size: + self.__strong_cache.popitem(last=False) + + def cache_clear(self): + with self._cache_lock: + self.__instances = weakref.WeakValueDictionary() + self.__strong_cache.clear() + + @staticmethod + def nocache(name=None): + """A non-cached version of gettz""" + tz = None + if not name: + try: + name = os.environ["TZ"] + except KeyError: + pass + if name is None or name in ("", ":"): + for filepath in TZFILES: + if not os.path.isabs(filepath): + filename = filepath + for path in TZPATHS: + filepath = os.path.join(path, filename) + if os.path.isfile(filepath): + break + else: + continue + if os.path.isfile(filepath): + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = tzlocal() + else: + try: + if name.startswith(":"): + name = name[1:] + except TypeError as e: + if isinstance(name, bytes): + new_msg = "gettz argument should be str, not bytes" + six.raise_from(TypeError(new_msg), e) + else: + raise + if os.path.isabs(name): + if os.path.isfile(name): + tz = tzfile(name) + else: + tz = None + else: + for path in TZPATHS: + filepath = os.path.join(path, name) + if not os.path.isfile(filepath): + filepath = filepath.replace(' ', '_') + if not os.path.isfile(filepath): + continue + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = None + if tzwin is not None: + try: + tz = tzwin(name) + except (WindowsError, UnicodeEncodeError): + # UnicodeEncodeError is for Python 2.7 compat + tz = None + + if not tz: + from dateutil.zoneinfo import get_zonefile_instance + tz = get_zonefile_instance().get(name) + + if not tz: + for c in name: + # name is not a tzstr unless it has at least + # one offset. For short values of "name", an + # explicit for loop seems to be the fastest way + # To determine if a string contains a digit + if c in "0123456789": + try: + tz = tzstr(name) + except ValueError: + pass + break + else: + if name in ("GMT", "UTC"): + tz = UTC + elif name in time.tzname: + tz = tzlocal() + return tz + + return GettzFunc() + + +gettz = __get_gettz() +del __get_gettz + + +def datetime_exists(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + would fall in a gap. + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" exists in + ``tz``. + + .. versionadded:: 2.7.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + tz = dt.tzinfo + + dt = dt.replace(tzinfo=None) + + # This is essentially a test of whether or not the datetime can survive + # a round trip to UTC. + dt_rt = dt.replace(tzinfo=tz).astimezone(UTC).astimezone(tz) + dt_rt = dt_rt.replace(tzinfo=None) + + return dt == dt_rt + + +def datetime_ambiguous(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + is ambiguous (i.e if there are two times differentiated only by their DST + status). + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" is ambiguous in + ``tz``. + + .. versionadded:: 2.6.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + + tz = dt.tzinfo + + # If a time zone defines its own "is_ambiguous" function, we'll use that. + is_ambiguous_fn = getattr(tz, 'is_ambiguous', None) + if is_ambiguous_fn is not None: + try: + return tz.is_ambiguous(dt) + except Exception: + pass + + # If it doesn't come out and tell us it's ambiguous, we'll just check if + # the fold attribute has any effect on this particular date and time. + dt = dt.replace(tzinfo=tz) + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dst = wall_0.dst() == wall_1.dst() + + return not (same_offset and same_dst) + + +def resolve_imaginary(dt): + """ + Given a datetime that may be imaginary, return an existing datetime. + + This function assumes that an imaginary datetime represents what the + wall time would be in a zone had the offset transition not occurred, so + it will always fall forward by the transition's change in offset. + + .. doctest:: + + >>> from dateutil import tz + >>> from datetime import datetime + >>> NYC = tz.gettz('America/New_York') + >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC))) + 2017-03-12 03:30:00-04:00 + + >>> KIR = tz.gettz('Pacific/Kiritimati') + >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR))) + 1995-01-02 12:30:00+14:00 + + As a note, :func:`datetime.astimezone` is guaranteed to produce a valid, + existing datetime, so a round-trip to and from UTC is sufficient to get + an extant datetime, however, this generally "falls back" to an earlier time + rather than falling forward to the STD side (though no guarantees are made + about this behavior). + + :param dt: + A :class:`datetime.datetime` which may or may not exist. + + :return: + Returns an existing :class:`datetime.datetime`. If ``dt`` was not + imaginary, the datetime returned is guaranteed to be the same object + passed to the function. + + .. versionadded:: 2.7.0 + """ + if dt.tzinfo is not None and not datetime_exists(dt): + + curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset() + old_offset = (dt - datetime.timedelta(hours=24)).utcoffset() + + dt += curr_offset - old_offset + + return dt + + +def _datetime_to_timestamp(dt): + """ + Convert a :class:`datetime.datetime` object to an epoch timestamp in + seconds since January 1, 1970, ignoring the time zone. + """ + return (dt.replace(tzinfo=None) - EPOCH).total_seconds() + + +if sys.version_info >= (3, 6): + def _get_supported_offset(second_offset): + return second_offset +else: + def _get_supported_offset(second_offset): + # For python pre-3.6, round to full-minutes if that's not the case. + # Python's datetime doesn't accept sub-minute timezones. Check + # http://python.org/sf/1447945 or https://bugs.python.org/issue5288 + # for some information. + old_offset = second_offset + calculated_offset = 60 * ((second_offset + 30) // 60) + return calculated_offset + + +try: + # Python 3.7 feature + from contextlib import nullcontext as _nullcontext +except ImportError: + class _nullcontext(object): + """ + Class for wrapping contexts so that they are passed through in a + with statement. + """ + def __init__(self, context): + self.context = context + + def __enter__(self): + return self.context + + def __exit__(*args, **kwargs): + pass + +# vim:ts=4:sw=4:et diff --git a/dateutil/tz/win.py b/dateutil/tz/win.py new file mode 100644 index 0000000..cde07ba --- /dev/null +++ b/dateutil/tz/win.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +""" +This module provides an interface to the native time zone data on Windows, +including :py:class:`datetime.tzinfo` implementations. + +Attempting to import this module on a non-Windows platform will raise an +:py:obj:`ImportError`. +""" +# This code was originally contributed by Jeffrey Harris. +import datetime +import struct + +from six.moves import winreg +from six import text_type + +try: + import ctypes + from ctypes import wintypes +except ValueError: + # ValueError is raised on non-Windows systems for some horrible reason. + raise ImportError("Running tzwin on non-Windows system") + +from ._common import tzrangebase + +__all__ = ["tzwin", "tzwinlocal", "tzres"] + +ONEWEEK = datetime.timedelta(7) + +TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" +TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" +TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" + + +def _settzkeyname(): + handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + try: + winreg.OpenKey(handle, TZKEYNAMENT).Close() + TZKEYNAME = TZKEYNAMENT + except WindowsError: + TZKEYNAME = TZKEYNAME9X + handle.Close() + return TZKEYNAME + + +TZKEYNAME = _settzkeyname() + + +class tzres(object): + """ + Class for accessing ``tzres.dll``, which contains timezone name related + resources. + + .. versionadded:: 2.5.0 + """ + p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char + + def __init__(self, tzres_loc='tzres.dll'): + # Load the user32 DLL so we can load strings from tzres + user32 = ctypes.WinDLL('user32') + + # Specify the LoadStringW function + user32.LoadStringW.argtypes = (wintypes.HINSTANCE, + wintypes.UINT, + wintypes.LPWSTR, + ctypes.c_int) + + self.LoadStringW = user32.LoadStringW + self._tzres = ctypes.WinDLL(tzres_loc) + self.tzres_loc = tzres_loc + + def load_name(self, offset): + """ + Load a timezone name from a DLL offset (integer). + + >>> from dateutil.tzwin import tzres + >>> tzr = tzres() + >>> print(tzr.load_name(112)) + 'Eastern Standard Time' + + :param offset: + A positive integer value referring to a string from the tzres dll. + + .. note:: + + Offsets found in the registry are generally of the form + ``@tzres.dll,-114``. The offset in this case is 114, not -114. + + """ + resource = self.p_wchar() + lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR) + nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0) + return resource[:nchar] + + def name_from_string(self, tzname_str): + """ + Parse strings as returned from the Windows registry into the time zone + name as defined in the registry. + + >>> from dateutil.tzwin import tzres + >>> tzr = tzres() + >>> print(tzr.name_from_string('@tzres.dll,-251')) + 'Dateline Daylight Time' + >>> print(tzr.name_from_string('Eastern Standard Time')) + 'Eastern Standard Time' + + :param tzname_str: + A timezone name string as returned from a Windows registry key. + + :return: + Returns the localized timezone string from tzres.dll if the string + is of the form `@tzres.dll,-offset`, else returns the input string. + """ + if not tzname_str.startswith('@'): + return tzname_str + + name_splt = tzname_str.split(',-') + try: + offset = int(name_splt[1]) + except: + raise ValueError("Malformed timezone string.") + + return self.load_name(offset) + + +class tzwinbase(tzrangebase): + """tzinfo class based on win32's timezones available in the registry.""" + def __init__(self): + raise NotImplementedError('tzwinbase is an abstract base class') + + def __eq__(self, other): + # Compare on all relevant dimensions, including name. + if not isinstance(other, tzwinbase): + return NotImplemented + + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._stddayofweek == other._stddayofweek and + self._dstdayofweek == other._dstdayofweek and + self._stdweeknumber == other._stdweeknumber and + self._dstweeknumber == other._dstweeknumber and + self._stdhour == other._stdhour and + self._dsthour == other._dsthour and + self._stdminute == other._stdminute and + self._dstminute == other._dstminute and + self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr) + + @staticmethod + def list(): + """Return a list of all time zones known to the system.""" + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, TZKEYNAME) as tzkey: + result = [winreg.EnumKey(tzkey, i) + for i in range(winreg.QueryInfoKey(tzkey)[0])] + return result + + def display(self): + """ + Return the display name of the time zone. + """ + return self._display + + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. + + :param year: + The year whose transitions you would like to query. + + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + + if not self.hasdst: + return None + + dston = picknthweekday(year, self._dstmonth, self._dstdayofweek, + self._dsthour, self._dstminute, + self._dstweeknumber) + + dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek, + self._stdhour, self._stdminute, + self._stdweeknumber) + + # Ambiguous dates default to the STD side + dstoff -= self._dst_base_offset + + return dston, dstoff + + def _get_hasdst(self): + return self._dstmonth != 0 + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ + + +class tzwin(tzwinbase): + """ + Time zone object created from the zone info in the Windows registry + + These are similar to :py:class:`dateutil.tz.tzrange` objects in that + the time zone data is provided in the format of a single offset rule + for either 0 or 2 time zone transitions per year. + + :param: name + The name of a Windows time zone key, e.g. "Eastern Standard Time". + The full list of keys can be retrieved with :func:`tzwin.list`. + """ + + def __init__(self, name): + self._name = name + + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name) + with winreg.OpenKey(handle, tzkeyname) as tzkey: + keydict = valuestodict(tzkey) + + self._std_abbr = keydict["Std"] + self._dst_abbr = keydict["Dlt"] + + self._display = keydict["Display"] + + # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm + tup = struct.unpack("=3l16h", keydict["TZI"]) + stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 + dstoffset = stdoffset-tup[2] # + DaylightBias * -1 + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) + + # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx + (self._stdmonth, + self._stddayofweek, # Sunday = 0 + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[4:9] + + (self._dstmonth, + self._dstdayofweek, # Sunday = 0 + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[12:17] + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + + def __repr__(self): + return "tzwin(%s)" % repr(self._name) + + def __reduce__(self): + return (self.__class__, (self._name,)) + + +class tzwinlocal(tzwinbase): + """ + Class representing the local time zone information in the Windows registry + + While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time` + module) to retrieve time zone information, ``tzwinlocal`` retrieves the + rules directly from the Windows registry and creates an object like + :class:`dateutil.tz.tzwin`. + + Because Windows does not have an equivalent of :func:`time.tzset`, on + Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the + time zone settings *at the time that the process was started*, meaning + changes to the machine's time zone settings during the run of a program + on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`. + Because ``tzwinlocal`` reads the registry directly, it is unaffected by + this issue. + """ + def __init__(self): + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: + keydict = valuestodict(tzlocalkey) + + self._std_abbr = keydict["StandardName"] + self._dst_abbr = keydict["DaylightName"] + + try: + tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME, + sn=self._std_abbr) + with winreg.OpenKey(handle, tzkeyname) as tzkey: + _keydict = valuestodict(tzkey) + self._display = _keydict["Display"] + except OSError: + self._display = None + + stdoffset = -keydict["Bias"]-keydict["StandardBias"] + dstoffset = stdoffset-keydict["DaylightBias"] + + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) + + # For reasons unclear, in this particular key, the day of week has been + # moved to the END of the SYSTEMTIME structure. + tup = struct.unpack("=8h", keydict["StandardStart"]) + + (self._stdmonth, + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[1:5] + + self._stddayofweek = tup[7] + + tup = struct.unpack("=8h", keydict["DaylightStart"]) + + (self._dstmonth, + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[1:5] + + self._dstdayofweek = tup[7] + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + + def __repr__(self): + return "tzwinlocal()" + + def __str__(self): + # str will return the standard name, not the daylight name. + return "tzwinlocal(%s)" % repr(self._std_abbr) + + def __reduce__(self): + return (self.__class__, ()) + + +def picknthweekday(year, month, dayofweek, hour, minute, whichweek): + """ dayofweek == 0 means Sunday, whichweek 5 means last instance """ + first = datetime.datetime(year, month, 1, hour, minute) + + # This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6), + # Because 7 % 7 = 0 + weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1) + wd = weekdayone + ((whichweek - 1) * ONEWEEK) + if (wd.month != month): + wd -= ONEWEEK + + return wd + + +def valuestodict(key): + """Convert a registry key's values to a dictionary.""" + dout = {} + size = winreg.QueryInfoKey(key)[1] + tz_res = None + + for i in range(size): + key_name, value, dtype = winreg.EnumValue(key, i) + if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN: + # If it's a DWORD (32-bit integer), it's stored as unsigned - convert + # that to a proper signed integer + if value & (1 << 31): + value = value - (1 << 32) + elif dtype == winreg.REG_SZ: + # If it's a reference to the tzres DLL, load the actual string + if value.startswith('@tzres'): + tz_res = tz_res or tzres() + value = tz_res.name_from_string(value) + + value = value.rstrip('\x00') # Remove trailing nulls + + dout[key_name] = value + + return dout diff --git a/dateutil/tzwin.py b/dateutil/tzwin.py new file mode 100644 index 0000000..cebc673 --- /dev/null +++ b/dateutil/tzwin.py @@ -0,0 +1,2 @@ +# tzwin has moved to dateutil.tz.win +from .tz.win import * diff --git a/dateutil/utils.py b/dateutil/utils.py new file mode 100644 index 0000000..dd2d245 --- /dev/null +++ b/dateutil/utils.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" +This module offers general convenience and utility functions for dealing with +datetimes. + +.. versionadded:: 2.7.0 +""" +from __future__ import unicode_literals + +from datetime import datetime, time + + +def today(tzinfo=None): + """ + Returns a :py:class:`datetime` representing the current day at midnight + + :param tzinfo: + The time zone to attach (also used to determine the current day). + + :return: + A :py:class:`datetime.datetime` object representing the current day + at midnight. + """ + + dt = datetime.now(tzinfo) + return datetime.combine(dt.date(), time(0, tzinfo=tzinfo)) + + +def default_tzinfo(dt, tzinfo): + """ + Sets the ``tzinfo`` parameter on naive datetimes only + + This is useful for example when you are provided a datetime that may have + either an implicit or explicit time zone, such as when parsing a time zone + string. + + .. doctest:: + + >>> from dateutil.tz import tzoffset + >>> from dateutil.parser import parse + >>> from dateutil.utils import default_tzinfo + >>> dflt_tz = tzoffset("EST", -18000) + >>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz)) + 2014-01-01 12:30:00+00:00 + >>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz)) + 2014-01-01 12:30:00-05:00 + + :param dt: + The datetime on which to replace the time zone + + :param tzinfo: + The :py:class:`datetime.tzinfo` subclass instance to assign to + ``dt`` if (and only if) it is naive. + + :return: + Returns an aware :py:class:`datetime.datetime`. + """ + if dt.tzinfo is not None: + return dt + else: + return dt.replace(tzinfo=tzinfo) + + +def within_delta(dt1, dt2, delta): + """ + Useful for comparing two datetimes that may have a negligible difference + to be considered equal. + """ + delta = abs(delta) + difference = dt1 - dt2 + return -delta <= difference <= delta diff --git a/dateutil/zoneinfo/__init__.py b/dateutil/zoneinfo/__init__.py new file mode 100644 index 0000000..34f11ad --- /dev/null +++ b/dateutil/zoneinfo/__init__.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +import warnings +import json + +from tarfile import TarFile +from pkgutil import get_data +from io import BytesIO + +from dateutil.tz import tzfile as _tzfile + +__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"] + +ZONEFILENAME = "dateutil-zoneinfo.tar.gz" +METADATA_FN = 'METADATA' + + +class tzfile(_tzfile): + def __reduce__(self): + return (gettz, (self._filename,)) + + +def getzoneinfofile_stream(): + try: + return BytesIO(get_data(__name__, ZONEFILENAME)) + except IOError as e: # TODO switch to FileNotFoundError? + warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror)) + return None + + +class ZoneInfoFile(object): + def __init__(self, zonefile_stream=None): + if zonefile_stream is not None: + with TarFile.open(fileobj=zonefile_stream) as tf: + self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name) + for zf in tf.getmembers() + if zf.isfile() and zf.name != METADATA_FN} + # deal with links: They'll point to their parent object. Less + # waste of memory + links = {zl.name: self.zones[zl.linkname] + for zl in tf.getmembers() if + zl.islnk() or zl.issym()} + self.zones.update(links) + try: + metadata_json = tf.extractfile(tf.getmember(METADATA_FN)) + metadata_str = metadata_json.read().decode('UTF-8') + self.metadata = json.loads(metadata_str) + except KeyError: + # no metadata in tar file + self.metadata = None + else: + self.zones = {} + self.metadata = None + + def get(self, name, default=None): + """ + Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method + for retrieving zones from the zone dictionary. + + :param name: + The name of the zone to retrieve. (Generally IANA zone names) + + :param default: + The value to return in the event of a missing key. + + .. versionadded:: 2.6.0 + + """ + return self.zones.get(name, default) + + +# The current API has gettz as a module function, although in fact it taps into +# a stateful class. So as a workaround for now, without changing the API, we +# will create a new "global" class instance the first time a user requests a +# timezone. Ugly, but adheres to the api. +# +# TODO: Remove after deprecation period. +_CLASS_ZONE_INSTANCE = [] + + +def get_zonefile_instance(new_instance=False): + """ + This is a convenience function which provides a :class:`ZoneInfoFile` + instance using the data provided by the ``dateutil`` package. By default, it + caches a single instance of the ZoneInfoFile object and returns that. + + :param new_instance: + If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and + used as the cached instance for the next call. Otherwise, new instances + are created only as necessary. + + :return: + Returns a :class:`ZoneInfoFile` object. + + .. versionadded:: 2.6 + """ + if new_instance: + zif = None + else: + zif = getattr(get_zonefile_instance, '_cached_instance', None) + + if zif is None: + zif = ZoneInfoFile(getzoneinfofile_stream()) + + get_zonefile_instance._cached_instance = zif + + return zif + + +def gettz(name): + """ + This retrieves a time zone from the local zoneinfo tarball that is packaged + with dateutil. + + :param name: + An IANA-style time zone name, as found in the zoneinfo file. + + :return: + Returns a :class:`dateutil.tz.tzfile` time zone object. + + .. warning:: + It is generally inadvisable to use this function, and it is only + provided for API compatibility with earlier versions. This is *not* + equivalent to ``dateutil.tz.gettz()``, which selects an appropriate + time zone based on the inputs, favoring system zoneinfo. This is ONLY + for accessing the dateutil-specific zoneinfo (which may be out of + date compared to the system zoneinfo). + + .. deprecated:: 2.6 + If you need to use a specific zoneinfofile over the system zoneinfo, + instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call + :func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead. + + Use :func:`get_zonefile_instance` to retrieve an instance of the + dateutil-provided zoneinfo. + """ + warnings.warn("zoneinfo.gettz() will be removed in future versions, " + "to use the dateutil-provided zoneinfo files, instantiate a " + "ZoneInfoFile object and use ZoneInfoFile.zones.get() " + "instead. See the documentation for details.", + DeprecationWarning) + + if len(_CLASS_ZONE_INSTANCE) == 0: + _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) + return _CLASS_ZONE_INSTANCE[0].zones.get(name) + + +def gettz_db_metadata(): + """ Get the zonefile metadata + + See `zonefile_metadata`_ + + :returns: + A dictionary with the database metadata + + .. deprecated:: 2.6 + See deprecation warning in :func:`zoneinfo.gettz`. To get metadata, + query the attribute ``zoneinfo.ZoneInfoFile.metadata``. + """ + warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future " + "versions, to use the dateutil-provided zoneinfo files, " + "ZoneInfoFile object and query the 'metadata' attribute " + "instead. See the documentation for details.", + DeprecationWarning) + + if len(_CLASS_ZONE_INSTANCE) == 0: + _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) + return _CLASS_ZONE_INSTANCE[0].metadata diff --git a/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..1461f8c862df8c3eae75e0ea376788b5b2602269 GIT binary patch literal 156400 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

    About This Content

    + +

    December 9, 2013

    +

    License

    + +

    The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise +indicated below, the Content is provided to you under the terms and conditions of the +Eclipse Public License Version 2.0 ("EPL") and Eclipse Distribution License Version 1.0 ("EDL"). +A copy of the EPL is available at +http://www.eclipse.org/legal/epl-v20.html +and a copy of the EDL is available at +http://www.eclipse.org/org/documents/edl-v10.php. +For purposes of the EPL, "Program" will mean the Content.

    + +

    If you did not receive this Content directly from the Eclipse Foundation, the Content is +being redistributed by another party ("Redistributor") and different terms and conditions may +apply to your use of any object code in the Content. Check the Redistributor's license that was +provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise +indicated below, the terms and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at http://www.eclipse.org.

    + + +

    Third Party Content

    +

    The Content includes items that have been sourced from third parties as set out below. If you + did not receive this Content directly from the Eclipse Foundation, the following is provided + for informational purposes only, and you should look to the Redistributor's license for + terms and conditions of use.

    +

    + None

    +

    +

    + + + + diff --git a/phaoUtils/edl-v10 b/phaoUtils/edl-v10 new file mode 100644 index 0000000..0d500b9 --- /dev/null +++ b/phaoUtils/edl-v10 @@ -0,0 +1,31 @@ +Eclipse Distribution License - v 1.0 + +Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + Neither the name of the Eclipse Foundation, Inc. nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/phaoUtils/epl-v20 b/phaoUtils/epl-v20 new file mode 100644 index 0000000..e48e096 --- /dev/null +++ b/phaoUtils/epl-v20 @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/phaoUtils/examples/aws_iot.py b/phaoUtils/examples/aws_iot.py new file mode 100644 index 0000000..6917c82 --- /dev/null +++ b/phaoUtils/examples/aws_iot.py @@ -0,0 +1,129 @@ +import base64 +import datetime +import functools +import hashlib +import hmac +import os +import uuid + +from paho.mqtt.client import Client, CallbackAPIVersion + + +def get_amazon_auth_headers(access_key, secret_key, region, host, port, headers=None): + """ Get the amazon auth headers for working with the amazon websockets + protocol + + Requires a lot of extra stuff: + + http://docs.aws.amazon.com/general/latest/gr//sigv4-create-canonical-request.html + http://docs.aws.amazon.com/general/latest/gr//signature-v4-examples.html#signature-v4-examples-pythonw + http://docs.aws.amazon.com/general/latest/gr//sigv4-signed-request-examples.html#sig-v4-examples-get-auth-header + + Args: + access_key (str): Amazon access key (AWS_ACCESS_KEY_ID) + secret_key (str): Amazon secret access key (AWS_SECRET_ACCESS_KEY) + region (str): aws region + host (str): iot endpoint (xxxxxxxxxxxxxx.iot..amazonaws.com) + headers (dict): a dictionary of the original headers- normally websocket headers + + Returns: + dict: A string containing the headers that amazon expects in the auth + request for the iot websocket service + """ + + # pylint: disable=unused-variable,unused-argument + + def sign(key, msg): + return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest() + + def getSignatureKey(key, dateStamp, regionName, serviceName): + kDate = sign(("AWS4" + key).encode("utf-8"), dateStamp) + kRegion = sign(kDate, regionName) + kService = sign(kRegion, serviceName) + kSigning = sign(kService, "aws4_request") + return kSigning + + service = "iotdevicegateway" + algorithm = "AWS4-HMAC-SHA256" + + t = datetime.datetime.utcnow() + amzdate = t.strftime('%Y%m%dT%H%M%SZ') + datestamp = t.strftime("%Y%m%d") # Date w/o time, used in credential scope + + if headers is None: + headers = { + "Host": "{0:s}:443".format(host), + "Upgrade": "websocket", + "Connection": "Upgrade", + "Origin": "https://{0:s}:443".format(host), + "Sec-WebSocket-Key": base64.b64encode(uuid.uuid4().bytes), + "Sec-Websocket-Version": "13", + "Sec-Websocket-Protocol": "mqtt", + } + + headers.update({ + "X-Amz-Date": amzdate, + }) + + # get into 'canonical' form - lowercase, sorted alphabetically + canonical_headers = "\n".join(sorted("{}:{}".format(i.lower(), j).strip() for i, j in headers.items())) + # Headers to sign - alphabetical order + signed_headers = ";".join(sorted(i.lower().strip() for i in headers.keys())) + + # No payload + payload_hash = hashlib.sha256("").hexdigest().lower() + + request_parts = [ + "GET", + "/mqtt", + # no query parameters + "", + canonical_headers + "\n", + signed_headers, + payload_hash, + ] + + canonical_request = "\n".join(request_parts) + + # now actually hash request and sign + hashed_request = hashlib.sha256(canonical_request).hexdigest() + + credential_scope = "{datestamp:s}/{region:s}/{service:s}/aws4_request".format(**locals()) + string_to_sign = "{algorithm:s}\n{amzdate:s}\n{credential_scope:s}\n{hashed_request:s}".format(**locals()) + + signing_key = getSignatureKey(secret_key, datestamp, region, service) + signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest() + + # create auth header + authorization_header = "{algorithm:s} Credential={access_key:s}/{credential_scope:s}, SignedHeaders={signed_headers:s}, Signature={signature:s}".format(**locals()) + + # get final header string + headers["Authorization"] = authorization_header + + return headers + + +def example_use(): + access_key = os.environ["AWS_ACCESS_KEY_ID"] + secret_key = os.environ["AWS_SECRET_ACCESS_KEY"] + port = 8883 + + region = "eu-west-1" + + # This is specific to your AWS account + host = "abc123def456.iot.{0:s}.amazonaws.com".format(region) + + extra_headers = functools.partial( + get_amazon_auth_headers, + access_key, + secret_key, + region, + host, + port, + ) + + client = Client(CallbackAPIVersion.VERSION2, transport="websockets") + + client.ws_set_options(headers=extra_headers) + + # Use client as normal from here diff --git a/phaoUtils/examples/client_logger.py b/phaoUtils/examples/client_logger.py new file mode 100644 index 0000000..6b984a7 --- /dev/null +++ b/phaoUtils/examples/client_logger.py @@ -0,0 +1,38 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 James Myatt +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# James Myatt - initial implementation + +# This shows a simple example of standard logging with an MQTT subscriber client. + +import logging + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt + +logging.basicConfig(level=logging.DEBUG) + +# If you want to use a specific client id, use +# mqttc = mqtt.Client("client-id") +# but note that the client id must be unique on the broker. Leaving the client +# id parameter empty will generate a random id for you. +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + +logger = logging.getLogger(__name__) +mqttc.enable_logger(logger) + +mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) +mqttc.subscribe("$SYS/#", 0) + +mqttc.loop_forever() diff --git a/phaoUtils/examples/client_mqtt_clear_retain.py b/phaoUtils/examples/client_mqtt_clear_retain.py new file mode 100644 index 0000000..996c7d6 --- /dev/null +++ b/phaoUtils/examples/client_mqtt_clear_retain.py @@ -0,0 +1,120 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2013 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation +# Copyright (c) 2010,2011 Roger Light +# All rights reserved. + +# This shows an example of an MQTT client that clears all of the retained messages it receives. + +import getopt +import sys + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt + +final_mid = 0 + + +def on_connect(mqttc, userdata, flags, reason_code, properties): + if userdata: + print(f"reason_code: {reason_code}") + + +def on_message(mqttc, userdata, msg): + global final_mid + if msg.retain == 0: + pass + # sys.exit() + else: + if userdata: + print("Clearing topic " + msg.topic) + (rc, final_mid) = mqttc.publish(msg.topic, None, 1, True) + + +def on_publish(mqttc, userdata, mid, reason_code, properties): + global final_mid + if mid == final_mid: + sys.exit() + + +def on_log(mqttc, userdata, level, string): + print(string) + + +def print_usage(): + print( + "mqtt_clear_retain.py [-d] [-h hostname] [-i clientid] [-k keepalive] [-p port] [-u username [-P password]] [-v] -t topic") + + +def main(argv): + debug = False + host = "mqtt.eclipseprojects.io" + client_id = None + keepalive = 60 + port = 1883 + password = None + topic = None + username = None + verbose = False + + try: + opts, args = getopt.getopt(argv, "dh:i:k:p:P:t:u:v", + ["debug", "id", "keepalive", "port", "password", "topic", "username", "verbose"]) + except getopt.GetoptError: + print_usage() + sys.exit(2) + for opt, arg in opts: + if opt in ("-d", "--debug"): + debug = True + elif opt in ("-h", "--host"): + host = arg + elif opt in ("-i", "--id"): + client_id = arg + elif opt in ("-k", "--keepalive"): + keepalive = int(arg) + elif opt in ("-p", "--port"): + port = int(arg) + elif opt in ("-P", "--password"): + password = arg + elif opt in ("-t", "--topic"): + topic = arg + print(topic) + elif opt in ("-u", "--username"): + username = arg + elif opt in ("-v", "--verbose"): + verbose = True + + if not topic: + print("You must provide a topic to clear.\n") + print_usage() + sys.exit(2) + + mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id) + mqttc._userdata = verbose + mqttc.on_message = on_message + mqttc.on_publish = on_publish + mqttc.on_connect = on_connect + if debug: + mqttc.on_log = on_log + + if username: + mqttc.username_pw_set(username, password) + mqttc.connect(host, port, keepalive) + mqttc.subscribe(topic) + mqttc.loop_forever() + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/phaoUtils/examples/client_pub-wait.py b/phaoUtils/examples/client_pub-wait.py new file mode 100644 index 0000000..729119d --- /dev/null +++ b/phaoUtils/examples/client_pub-wait.py @@ -0,0 +1,60 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2010-2013 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation +# Copyright (c) 2010,2011 Roger Light +# All rights reserved. + +# This shows a simple example of waiting for a message to be published. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt + + +def on_connect(mqttc, obj, flags, reason_code, properties): + print("reason_code: " + str(reason_code)) + + +def on_message(mqttc, obj, msg): + print(msg.topic + " " + str(msg.qos) + " " + str(msg.payload)) + + +def on_publish(mqttc, obj, mid, reason_code, properties): + print("mid: " + str(mid)) + + +def on_log(mqttc, obj, level, string): + print(string) + + +# If you want to use a specific client id, use +# mqttc = mqtt.Client("client-id") +# but note that the client id must be unique on the broker. Leaving the client +# id parameter empty will generate a random id for you. +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) +mqttc.on_message = on_message +mqttc.on_connect = on_connect +mqttc.on_publish = on_publish +# Uncomment to enable debug messages +# mqttc.on_log = on_log +mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) + +mqttc.loop_start() + +print("tuple") +(rc, mid) = mqttc.publish("tuple", "bar", qos=2) +print("class") +infot = mqttc.publish("class", "bar", qos=2) + +infot.wait_for_publish() diff --git a/phaoUtils/examples/client_pub_opts.py b/phaoUtils/examples/client_pub_opts.py new file mode 100644 index 0000000..40e49cd --- /dev/null +++ b/phaoUtils/examples/client_pub_opts.py @@ -0,0 +1,123 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 Jon Levell +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# All rights reserved. + +# This shows a example of an MQTT publisher with the ability to use +# user name, password CA certificates based on command line arguments + +import argparse +import os +import ssl +import time + +import paho.mqtt.client as mqtt + +parser = argparse.ArgumentParser() + +parser.add_argument('-H', '--host', required=False, default="mqtt.eclipseprojects.io") +parser.add_argument('-t', '--topic', required=False, default="paho/test/opts") +parser.add_argument('-q', '--qos', required=False, type=int,default=0) +parser.add_argument('-c', '--clientid', required=False, default=None) +parser.add_argument('-u', '--username', required=False, default=None) +parser.add_argument('-d', '--disable-clean-session', action='store_true', help="disable 'clean session' (sub + msgs not cleared when client disconnects)") +parser.add_argument('-p', '--password', required=False, default=None) +parser.add_argument('-P', '--port', required=False, type=int, default=None, help='Defaults to 8883 for TLS or 1883 for non-TLS') +parser.add_argument('-N', '--nummsgs', required=False, type=int, default=1, help='send this many messages before disconnecting') +parser.add_argument('-S', '--delay', required=False, type=float, default=1, help='number of seconds to sleep between msgs') +parser.add_argument('-k', '--keepalive', required=False, type=int, default=60) +parser.add_argument('-s', '--use-tls', action='store_true') +parser.add_argument('--insecure', action='store_true') +parser.add_argument('-F', '--cacerts', required=False, default=None) +parser.add_argument('--tls-version', required=False, default=None, help='TLS protocol version, can be one of tlsv1.2 tlsv1.1 or tlsv1\n') +parser.add_argument('-D', '--debug', action='store_true') + +args, unknown = parser.parse_known_args() + + +def on_connect(mqttc, obj, flags, reason_code, properties): + print("connect reason_code: " + str(reason_code)) + + +def on_message(mqttc, obj, msg): + print(msg.topic + " " + str(msg.qos) + " " + str(msg.payload)) + + +def on_publish(mqttc, obj, mid, reason_code, properties): + print("mid: " + str(mid)) + + +def on_log(mqttc, obj, level, string): + print(string) + +usetls = args.use_tls + +if args.cacerts: + usetls = True + +port = args.port +if port is None: + if usetls: + port = 8883 + else: + port = 1883 + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, args.clientid, clean_session = not args.disable_clean_session) + +if usetls: + if args.tls_version == "tlsv1.2": + tlsVersion = ssl.PROTOCOL_TLSv1_2 + elif args.tls_version == "tlsv1.1": + tlsVersion = ssl.PROTOCOL_TLSv1_1 + elif args.tls_version == "tlsv1": + tlsVersion = ssl.PROTOCOL_TLSv1 + elif args.tls_version is None: + tlsVersion = None + else: + print ("Unknown TLS version - ignoring") + tlsVersion = None + + if not args.insecure: + cert_required = ssl.CERT_REQUIRED + else: + cert_required = ssl.CERT_NONE + + mqttc.tls_set(ca_certs=args.cacerts, certfile=None, keyfile=None, cert_reqs=cert_required, tls_version=tlsVersion) + + if args.insecure: + mqttc.tls_insecure_set(True) + +if args.username or args.password: + mqttc.username_pw_set(args.username, args.password) + +mqttc.on_message = on_message +mqttc.on_connect = on_connect +mqttc.on_publish = on_publish + +if args.debug: + mqttc.on_log = on_log + +print("Connecting to "+args.host+" port: "+str(port)) +mqttc.connect(args.host, port, args.keepalive) + +mqttc.loop_start() + +for x in range (0, args.nummsgs): + msg_txt = '{"msgnum": "'+str(x)+'"}' + print("Publishing: "+msg_txt) + infot = mqttc.publish(args.topic, msg_txt, qos=args.qos) + infot.wait_for_publish() + + time.sleep(args.delay) + +mqttc.disconnect() + diff --git a/phaoUtils/examples/client_rpc_math.py b/phaoUtils/examples/client_rpc_math.py new file mode 100644 index 0000000..74a47d9 --- /dev/null +++ b/phaoUtils/examples/client_rpc_math.py @@ -0,0 +1,114 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Frank Pagliughi +# All rights reserved. +# +# This program and the accompanying materials are made available +# under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Frank Pagliughi - initial implementation +# + +# This shows an example of an MQTTv5 Remote Procedure Call (RPC) client. +# You should run the server_rpc_math.py before. + + +import json +import sys +import time + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt +from paho.mqtt.packettypes import PacketTypes + +# These will be updated with the server-assigned Client ID +client_id = "mathcli" +reply_to = "" + +# This correlates the outbound request with the returned reply +corr_id = b"1" + +# This is sent in the message callback when we get the response +reply = None + +# The MQTTv5 callback takes the additional 'props' parameter. +def on_connect(mqttc, userdata, flags, reason_code, props): + global client_id, reply_to + + print(f"Connected: '{flags}', '{reason_code}', '{props}'") + if hasattr(props, 'AssignedClientIdentifier'): + client_id = props.AssignedClientIdentifier + reply_to = "replies/math/" + client_id + mqttc.subscribe(reply_to) + + +# An incoming message should be the reply to our request +def on_message(mqttc, userdata, msg): + global reply + + print(msg.topic+" "+str(msg.payload)+" "+str(msg.properties)) + props = msg.properties + if not hasattr(props, 'CorrelationData'): + print("No correlation ID") + + # Match the response to the request correlation ID. + if props.CorrelationData == corr_id: + reply = msg.payload + + +if len(sys.argv) < 3: + print("USAGE: client_rpc_math.py [add|mult] n1 n2 ...") + sys.exit(1) + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="", protocol=mqtt.MQTTv5) +mqttc.on_message = on_message +mqttc.on_connect = on_connect + +mqttc.connect(host="mqtt.eclipseprojects.io", clean_start=True) +mqttc.loop_start() + +# Wait for connection to set `client_id`, etc. +while not mqttc.is_connected(): + time.sleep(0.1) + +# Properties for the request specify the ResponseTopic and CorrelationData +props = mqtt.Properties(PacketTypes.PUBLISH) +props.CorrelationData = corr_id +props.ResponseTopic = reply_to + +# Uncomment to see what got set +#print("Client ID: "+client_id) +#print("Reply To: "+reply_to) +#print(props) + +# The requested operation, 'add' or 'mult' +func = sys.argv[1] + +# Gather the numeric parameters as an array of numbers +# These can be int's or float's +args = [] +for s in sys.argv[2:]: + args.append(float(s)) + +# Send the request +topic = "requests/math/" + func +payload = json.dumps(args) +mqttc.publish(topic, payload, qos=1, properties=props) + +# Wait for the reply +while reply is None: + time.sleep(0.1) + +# Extract the response and print it. +rsp = json.loads(reply) +print("Response: "+str(rsp)) + +mqttc.loop_stop() + diff --git a/phaoUtils/examples/client_session_present.py b/phaoUtils/examples/client_session_present.py new file mode 100644 index 0000000..b4552cc --- /dev/null +++ b/phaoUtils/examples/client_session_present.py @@ -0,0 +1,62 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation +# Copyright (c) 2014 Roger Light +# All rights reserved. + +# This demonstrates the session present flag when connecting. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt + + +def on_connect(mqttc, obj, flags, reason_code, properties): + if obj == 0: + print("First connection:") + elif obj == 1: + print("Second connection:") + elif obj == 2: + print("Third connection (with clean session=True):") + print(" Session present: " + str(flags.session_present)) + print(" Connection result: " + str(reason_code)) + mqttc.disconnect() + + +def on_disconnect(mqttc, obj, flags, reason_code, properties): + mqttc.user_data_set(obj + 1) + if obj == 0: + mqttc.reconnect() + + +def on_log(mqttc, obj, level, string): + print(string) + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="asdfj", clean_session=False) +mqttc.on_connect = on_connect +mqttc.on_disconnect = on_disconnect +# Uncomment to enable debug messages +# mqttc.on_log = on_log +mqttc.user_data_set(0) +mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) + +mqttc.loop_forever() + +# Clear session +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="asdfj", clean_session=True) +mqttc.on_connect = on_connect +mqttc.user_data_set(2) +mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) +mqttc.loop_forever() diff --git a/phaoUtils/examples/client_sub-class.py b/phaoUtils/examples/client_sub-class.py new file mode 100644 index 0000000..9374006 --- /dev/null +++ b/phaoUtils/examples/client_sub-class.py @@ -0,0 +1,60 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2013 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation + +# This example shows how you can use the MQTT client in a class. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt + + +class MyMQTTClass(mqtt.Client): + + def on_connect(self, mqttc, obj, flags, reason_code, properties): + print("rc: "+str(reason_code)) + + def on_connect_fail(self, mqttc, obj): + print("Connect failed") + + def on_message(self, mqttc, obj, msg): + print(msg.topic+" "+str(msg.qos)+" "+str(msg.payload)) + + def on_publish(self, mqttc, obj, mid, reason_codes, properties): + print("mid: "+str(mid)) + + def on_subscribe(self, mqttc, obj, mid, reason_code_list, properties): + print("Subscribed: "+str(mid)+" "+str(reason_code_list)) + + def on_log(self, mqttc, obj, level, string): + print(string) + + def run(self): + self.connect("mqtt.eclipseprojects.io", 1883, 60) + self.subscribe("$SYS/#", 0) + + rc = 0 + while rc == 0: + rc = self.loop() + return rc + + +# If you want to use a specific client id, use +# mqttc = MyMQTTClass("client-id") +# but note that the client id must be unique on the broker. Leaving the client +# id parameter empty will generate a random id for you. +mqttc = MyMQTTClass(mqtt.CallbackAPIVersion.VERSION2) +rc = mqttc.run() + +print("rc: "+str(rc)) diff --git a/phaoUtils/examples/client_sub-multiple-callback.py b/phaoUtils/examples/client_sub-multiple-callback.py new file mode 100644 index 0000000..26a911e --- /dev/null +++ b/phaoUtils/examples/client_sub-multiple-callback.py @@ -0,0 +1,53 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation +# All rights reserved. + +# This shows a simple example of an MQTT subscriber using a per-subscription message handler. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt + + +def on_message_msgs(mosq, obj, msg): + # This callback will only be called for messages with topics that match + # $SYS/broker/messages/# + print("MESSAGES: " + msg.topic + " " + str(msg.qos) + " " + str(msg.payload)) + + +def on_message_bytes(mosq, obj, msg): + # This callback will only be called for messages with topics that match + # $SYS/broker/bytes/# + print("BYTES: " + msg.topic + " " + str(msg.qos) + " " + str(msg.payload)) + + +def on_message(mosq, obj, msg): + # This callback will be called for messages that we receive that do not + # match any patterns defined in topic specific callbacks, i.e. in this case + # those messages that do not have topics $SYS/broker/messages/# nor + # $SYS/broker/bytes/# + print(msg.topic + " " + str(msg.qos) + " " + str(msg.payload)) + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + +# Add message callbacks that will only trigger on a specific subscription match. +mqttc.message_callback_add("$SYS/broker/messages/#", on_message_msgs) +mqttc.message_callback_add("$SYS/broker/bytes/#", on_message_bytes) +mqttc.on_message = on_message +mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) +mqttc.subscribe("$SYS/#", 0) + +mqttc.loop_forever() diff --git a/phaoUtils/examples/client_sub-srv.py b/phaoUtils/examples/client_sub-srv.py new file mode 100644 index 0000000..df95c91 --- /dev/null +++ b/phaoUtils/examples/client_sub-srv.py @@ -0,0 +1,55 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2010-2013 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation +# Copyright (c) 2010,2011 Roger Light +# All rights reserved. + +# This shows a simple example of an MQTT subscriber using connect_srv method. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt + + +def on_connect(mqttc, obj, flags, reason_code, properties): + print("Connected to %s:%s" % (mqttc.host, mqttc.port)) + +def on_message(mqttc, obj, msg): + print(msg.topic+" "+str(msg.qos)+" "+str(msg.payload)) + +def on_subscribe(mqttc, obj, mid, reason_code_list, properties): + print("Subscribed: "+str(mid)+" "+str(reason_code_list)) + +def on_log(mqttc, obj, level, string): + print(string) + +# If you want to use a specific client id, use +# mqttc = mqtt.Client("client-id") +# but note that the client id must be unique on the broker. Leaving the client +# id parameter empty will generate a random id for you. +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) +mqttc.on_message = on_message +mqttc.on_connect = on_connect +mqttc.on_subscribe = on_subscribe +# Uncomment to enable debug messages +#mqttc.on_log = on_log +mqttc.connect_srv("eclipseprojects.io", 60) +mqttc.subscribe("$SYS/broker/version", 0) + + +rc = 0 +while rc == 0: + rc = mqttc.loop() + +print("rc: "+str(rc)) diff --git a/phaoUtils/examples/client_sub-ws.py b/phaoUtils/examples/client_sub-ws.py new file mode 100644 index 0000000..e295085 --- /dev/null +++ b/phaoUtils/examples/client_sub-ws.py @@ -0,0 +1,50 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2010-2013 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation +# Copyright (c) 2010,2011 Roger Light +# All rights reserved. + +# This shows a simple example of an MQTT subscriber. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt + + +def on_connect(mqttc, obj, flags, reason_code, properties): + print("reason_code: "+str(reason_code)) + +def on_message(mqttc, obj, msg): + print(msg.topic+" "+str(msg.qos)+" "+str(msg.payload)) + +def on_subscribe(mqttc, obj, mid, reason_code_list, properties): + print("Subscribed: "+str(mid)+" "+str(reason_code_list)) + +def on_log(mqttc, obj, level, string): + print(string) + +# If you want to use a specific client id, use +# mqttc = mqtt.Client("client-id") +# but note that the client id must be unique on the broker. Leaving the client +# id parameter empty will generate a random id for you. +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, transport="websockets") +mqttc.on_message = on_message +mqttc.on_connect = on_connect +mqttc.on_subscribe = on_subscribe +# Uncomment to enable debug messages +mqttc.on_log = on_log +mqttc.connect("mqtt.eclipseprojects.io", 80, 60) +mqttc.subscribe("$SYS/broker/version", 0) + +mqttc.loop_forever() diff --git a/phaoUtils/examples/client_sub.py b/phaoUtils/examples/client_sub.py new file mode 100644 index 0000000..739fa02 --- /dev/null +++ b/phaoUtils/examples/client_sub.py @@ -0,0 +1,54 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2010-2013 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation +# Copyright (c) 2010,2011 Roger Light +# All rights reserved. + +# This shows a simple example of an MQTT subscriber. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt + + +def on_connect(mqttc, obj, flags, reason_code, properties): + print("reason_code: " + str(reason_code)) + + +def on_message(mqttc, obj, msg): + print(msg.topic + " " + str(msg.qos) + " " + str(msg.payload)) + + +def on_subscribe(mqttc, obj, mid, reason_code_list, properties): + print("Subscribed: " + str(mid) + " " + str(reason_code_list)) + + +def on_log(mqttc, obj, level, string): + print(string) + + +# If you want to use a specific client id, use +# mqttc = mqtt.Client("client-id") +# but note that the client id must be unique on the broker. Leaving the client +# id parameter empty will generate a random id for you. +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) +mqttc.on_message = on_message +mqttc.on_connect = on_connect +mqttc.on_subscribe = on_subscribe +# Uncomment to enable debug messages +# mqttc.on_log = on_log +mqttc.connect("mqtt.eclipseprojects.io", 1883, 60) +mqttc.subscribe("$SYS/#") + +mqttc.loop_forever() diff --git a/phaoUtils/examples/client_sub_opts.py b/phaoUtils/examples/client_sub_opts.py new file mode 100644 index 0000000..2968ab5 --- /dev/null +++ b/phaoUtils/examples/client_sub_opts.py @@ -0,0 +1,115 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 Jon Levell +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# All rights reserved. + +# This shows a example of an MQTT subscriber with the ability to use +# user name, password CA certificates based on command line arguments + +import argparse +import os +import ssl + +import paho.mqtt.client as mqtt + +parser = argparse.ArgumentParser() + +parser.add_argument('-H', '--host', required=False, default="mqtt.eclipseprojects.io") +parser.add_argument('-t', '--topic', required=False, default="$SYS/#") +parser.add_argument('-q', '--qos', required=False, type=int, default=0) +parser.add_argument('-c', '--clientid', required=False, default=None) +parser.add_argument('-u', '--username', required=False, default=None) +parser.add_argument('-d', '--disable-clean-session', action='store_true', help="disable 'clean session' (sub + msgs not cleared when client disconnects)") +parser.add_argument('-p', '--password', required=False, default=None) +parser.add_argument('-P', '--port', required=False, type=int, default=None, help='Defaults to 8883 for TLS or 1883 for non-TLS') +parser.add_argument('-k', '--keepalive', required=False, type=int, default=60) +parser.add_argument('-s', '--use-tls', action='store_true') +parser.add_argument('--insecure', action='store_true') +parser.add_argument('-F', '--cacerts', required=False, default=None) +parser.add_argument('--tls-version', required=False, default=None, help='TLS protocol version, can be one of tlsv1.2 tlsv1.1 or tlsv1\n') +parser.add_argument('-D', '--debug', action='store_true') + +args, unknown = parser.parse_known_args() + + +def on_connect(mqttc, obj, flags, reason_code, properties): + print("reason_code: " + str(reason_code)) + + +def on_message(mqttc, obj, msg): + print(msg.topic + " " + str(msg.qos) + " " + str(msg.payload)) + + +def on_publish(mqttc, obj, mid): + print("mid: " + str(mid)) + + +def on_subscribe(mqttc, obj, mid, reason_code_list, properties): + print("Subscribed: " + str(mid) + " " + str(reason_code_list)) + + +def on_log(mqttc, obj, level, string): + print(string) + +usetls = args.use_tls + +if args.cacerts: + usetls = True + +port = args.port +if port is None: + if usetls: + port = 8883 + else: + port = 1883 + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, args.clientid,clean_session = not args.disable_clean_session) + +if usetls: + if args.tls_version == "tlsv1.2": + tlsVersion = ssl.PROTOCOL_TLSv1_2 + elif args.tls_version == "tlsv1.1": + tlsVersion = ssl.PROTOCOL_TLSv1_1 + elif args.tls_version == "tlsv1": + tlsVersion = ssl.PROTOCOL_TLSv1 + elif args.tls_version is None: + tlsVersion = None + else: + print ("Unknown TLS version - ignoring") + tlsVersion = None + + if not args.insecure: + cert_required = ssl.CERT_REQUIRED + else: + cert_required = ssl.CERT_NONE + + mqttc.tls_set(ca_certs=args.cacerts, certfile=None, keyfile=None, cert_reqs=cert_required, tls_version=tlsVersion) + + if args.insecure: + mqttc.tls_insecure_set(True) + +if args.username or args.password: + mqttc.username_pw_set(args.username, args.password) + +mqttc.on_message = on_message +mqttc.on_connect = on_connect +mqttc.on_publish = on_publish +mqttc.on_subscribe = on_subscribe + +if args.debug: + mqttc.on_log = on_log + +print("Connecting to "+args.host+" port: "+str(port)) +mqttc.connect(args.host, port, args.keepalive) +mqttc.subscribe(args.topic, args.qos) + +mqttc.loop_forever() diff --git a/phaoUtils/examples/context.py b/phaoUtils/examples/context.py new file mode 100644 index 0000000..faef26a --- /dev/null +++ b/phaoUtils/examples/context.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +# Ensure can import paho package +try: + import paho + +except ImportError: + # This part is only required to run the examples from within the examples + # directory when the module itself is not installed. + import inspect + import os + import sys + + cmd_subfolder = os.path.realpath( + os.path.abspath( + os.path.join( + os.path.split( + inspect.getfile(inspect.currentframe()) + )[0], + "..", + "src" + ) + ) + ) + if cmd_subfolder not in sys.path: + sys.path.insert(0, cmd_subfolder) + + import paho diff --git a/phaoUtils/examples/loop_asyncio.py b/phaoUtils/examples/loop_asyncio.py new file mode 100644 index 0000000..42ab04a --- /dev/null +++ b/phaoUtils/examples/loop_asyncio.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 + +import asyncio +import socket +import uuid + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt + +client_id = 'paho-mqtt-python/issue72/' + str(uuid.uuid4()) +topic = client_id +print("Using client_id / topic: " + client_id) + + +class AsyncioHelper: + def __init__(self, loop, client): + self.loop = loop + self.client = client + self.client.on_socket_open = self.on_socket_open + self.client.on_socket_close = self.on_socket_close + self.client.on_socket_register_write = self.on_socket_register_write + self.client.on_socket_unregister_write = self.on_socket_unregister_write + + def on_socket_open(self, client, userdata, sock): + print("Socket opened") + + def cb(): + print("Socket is readable, calling loop_read") + client.loop_read() + + self.loop.add_reader(sock, cb) + self.misc = self.loop.create_task(self.misc_loop()) + + def on_socket_close(self, client, userdata, sock): + print("Socket closed") + self.loop.remove_reader(sock) + self.misc.cancel() + + def on_socket_register_write(self, client, userdata, sock): + print("Watching socket for writability.") + + def cb(): + print("Socket is writable, calling loop_write") + client.loop_write() + + self.loop.add_writer(sock, cb) + + def on_socket_unregister_write(self, client, userdata, sock): + print("Stop watching socket for writability.") + self.loop.remove_writer(sock) + + async def misc_loop(self): + print("misc_loop started") + while self.client.loop_misc() == mqtt.MQTT_ERR_SUCCESS: + try: + await asyncio.sleep(1) + except asyncio.CancelledError: + break + print("misc_loop finished") + + +class AsyncMqttExample: + def __init__(self, loop): + self.loop = loop + + def on_connect(self, client, userdata, flags, reason_code, properties): + print("Subscribing") + client.subscribe(topic) + + def on_message(self, client, userdata, msg): + if not self.got_message: + print("Got unexpected message: {}".format(msg.decode())) + else: + self.got_message.set_result(msg.payload) + + def on_disconnect(self, client, userdata, flags, reason_code, properties): + self.disconnected.set_result(reason_code) + + async def main(self): + self.disconnected = self.loop.create_future() + self.got_message = None + + self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id) + self.client.on_connect = self.on_connect + self.client.on_message = self.on_message + self.client.on_disconnect = self.on_disconnect + + aioh = AsyncioHelper(self.loop, self.client) + + self.client.connect('mqtt.eclipseprojects.io', 1883, 60) + self.client.socket().setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 2048) + + for c in range(3): + await asyncio.sleep(5) + print("Publishing") + self.got_message = self.loop.create_future() + self.client.publish(topic, b'Hello' * 40000, qos=1) + msg = await self.got_message + print("Got response with {} bytes".format(len(msg))) + self.got_message = None + + self.client.disconnect() + print("Disconnected: {}".format(await self.disconnected)) + + +print("Starting") +loop = asyncio.get_event_loop() +loop.run_until_complete(AsyncMqttExample(loop).main()) +loop.close() +print("Finished") diff --git a/phaoUtils/examples/loop_select.py b/phaoUtils/examples/loop_select.py new file mode 100644 index 0000000..255c902 --- /dev/null +++ b/phaoUtils/examples/loop_select.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +import socket +import uuid +from select import select +from time import time + +import paho.mqtt.client as mqtt + +client_id = 'paho-mqtt-python/issue72/' + str(uuid.uuid4()) +topic = client_id +print("Using client_id / topic: " + client_id) + + +class SelectMqttExample: + def __init__(self): + pass + + def on_connect(self, client, userdata, flags, reason_code, properties): + print("Subscribing") + client.subscribe(topic) + + def on_message(self, client, userdata, msg): + if self.state not in {1, 3, 5}: + print("Got unexpected message: {}".format(msg.decode())) + return + + print("Got message with len {}".format(len(msg.payload))) + self.state += 1 + self.t = time() + + def on_disconnect(self, client, userdata, flags, reason_code, properties): + self.disconnected = True, reason_code + + def do_select(self): + sock = self.client.socket() + if not sock: + raise Exception("Socket is gone") + + print("Selecting for reading" + (" and writing" if self.client.want_write() else "")) + r, w, e = select( + [sock], + [sock] if self.client.want_write() else [], + [], + 1 + ) + + if sock in r: + print("Socket is readable, calling loop_read") + self.client.loop_read() + + if sock in w: + print("Socket is writable, calling loop_write") + self.client.loop_write() + + self.client.loop_misc() + + def main(self): + self.disconnected = (False, None) + self.t = time() + self.state = 0 + + self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id) + self.client.on_connect = self.on_connect + self.client.on_message = self.on_message + self.client.on_disconnect = self.on_disconnect + + self.client.connect('mqtt.eclipseprojects.io', 1883, 60) + print("Socket opened") + self.client.socket().setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 2048) + + while not self.disconnected[0]: + self.do_select() + + if self.state in {0, 2, 4}: + if time() - self.t >= 5: + print("Publishing") + self.client.publish(topic, b'Hello' * 40000) + self.state += 1 + + if self.state == 6: + self.state += 1 + self.client.disconnect() + + print("Disconnected: {}".format(self.disconnected[1])) + + +print("Starting") +SelectMqttExample().main() +print("Finished") diff --git a/phaoUtils/examples/loop_trio.py b/phaoUtils/examples/loop_trio.py new file mode 100644 index 0000000..aa71c3f --- /dev/null +++ b/phaoUtils/examples/loop_trio.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +import socket +import uuid + +import trio + +import paho.mqtt.client as mqtt + +client_id = 'paho-mqtt-python/issue72/' + str(uuid.uuid4()) +topic = client_id +print("Using client_id / topic: " + client_id) + + +class TrioAsyncHelper: + def __init__(self, client): + self.client = client + self.sock = None + self._event_large_write = trio.Event() + + self.client.on_socket_open = self.on_socket_open + self.client.on_socket_register_write = self.on_socket_register_write + self.client.on_socket_unregister_write = self.on_socket_unregister_write + + async def read_loop(self): + while True: + await trio.lowlevel.wait_readable(self.sock) + self.client.loop_read() + + async def write_loop(self): + while True: + await self._event_large_write.wait() + await trio.lowlevel.wait_writable(self.sock) + self.client.loop_write() + + async def misc_loop(self): + print("misc_loop started") + while self.client.loop_misc() == mqtt.MQTT_ERR_SUCCESS: + await trio.sleep(1) + print("misc_loop finished") + + def on_socket_open(self, client, userdata, sock): + print("Socket opened") + self.sock = sock + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 2048) + + def on_socket_register_write(self, client, userdata, sock): + print('large write request') + self._event_large_write.set() + + def on_socket_unregister_write(self, client, userdata, sock): + print("finished large write") + self._event_large_write = trio.Event() + + +class TrioAsyncMqttExample: + def on_connect(self, client, userdata, flags, reason_code, properties): + print("Subscribing") + client.subscribe(topic) + + def on_message(self, client, userdata, msg): + print("Got response with {} bytes".format(len(msg.payload))) + + def on_disconnect(self, client, userdata, flags, reason_code, properties): + print('Disconnect result {}'.format(reason_code)) + + async def test_write(self, cancel_scope: trio.CancelScope): + for c in range(3): + await trio.sleep(5) + print("Publishing") + self.client.publish(topic, b'Hello' * 40000, qos=1) + cancel_scope.cancel() + + async def main(self): + self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id) + self.client.on_connect = self.on_connect + self.client.on_message = self.on_message + self.client.on_disconnect = self.on_disconnect + + trio_helper = TrioAsyncHelper(self.client) + + self.client.connect('mqtt.eclipseprojects.io', 1883, 60) + + async with trio.open_nursery() as nursery: + nursery.start_soon(trio_helper.read_loop) + nursery.start_soon(trio_helper.write_loop) + nursery.start_soon(trio_helper.misc_loop) + nursery.start_soon(self.test_write, nursery.cancel_scope) + + self.client.disconnect() + print("Disconnected") + + +print("Starting") +trio.run(TrioAsyncMqttExample().main) +print("Finished") diff --git a/phaoUtils/examples/publish_multiple.py b/phaoUtils/examples/publish_multiple.py new file mode 100644 index 0000000..7c19a13 --- /dev/null +++ b/phaoUtils/examples/publish_multiple.py @@ -0,0 +1,23 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation + +# This shows an example of using the publish.multiple helper function. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.publish as publish + +msgs = [{'topic': "paho/test/multiple", 'payload': "multiple 1"}, ("paho/test/multiple", "multiple 2", 0, False)] +publish.multiple(msgs, hostname="mqtt.eclipseprojects.io") diff --git a/phaoUtils/examples/publish_single.py b/phaoUtils/examples/publish_single.py new file mode 100644 index 0000000..9e48279 --- /dev/null +++ b/phaoUtils/examples/publish_single.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation + +# This shows an example of using the publish.single helper function. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.publish as publish + +publish.single("paho/test/single", "boo", hostname="mqtt.eclipseprojects.io") diff --git a/phaoUtils/examples/publish_utf8-27.py b/phaoUtils/examples/publish_utf8-27.py new file mode 100644 index 0000000..74dee2b --- /dev/null +++ b/phaoUtils/examples/publish_utf8-27.py @@ -0,0 +1,24 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation + +# This shows an example of using the publish.single helper function with unicode topic and payload. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.publish as publish + +topic = u"paho/test/single/ô" +payload = u"bôô" +publish.single(topic, payload, hostname="mqtt.eclipseprojects.io") diff --git a/phaoUtils/examples/publish_utf8-3.py b/phaoUtils/examples/publish_utf8-3.py new file mode 100644 index 0000000..e7bb46c --- /dev/null +++ b/phaoUtils/examples/publish_utf8-3.py @@ -0,0 +1,24 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation + +# This shows an example of using the publish.single helper function with unicode topic and payload. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.publish as publish + +topic = u"paho/test/single/ô" +payload = u'German umlauts like "ä" ü"ö" are not supported' +publish.single(topic, payload, hostname="mqtt.eclipseprojects.io") diff --git a/phaoUtils/examples/server_rpc_math.py b/phaoUtils/examples/server_rpc_math.py new file mode 100644 index 0000000..2fba6d9 --- /dev/null +++ b/phaoUtils/examples/server_rpc_math.py @@ -0,0 +1,96 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Frank Pagliughi +# All rights reserved. +# +# This program and the accompanying materials are made available +# under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Frank Pagliughi - initial implementation +# + +# This shows an example of an MQTTv5 Remote Procedure Call (RPC) server. + +import json + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.client as mqtt +from paho.mqtt.packettypes import PacketTypes + +# The math functions exported + +def add(nums): + sum = 0 + for x in nums: + sum += x + return sum + +def mult(nums): + prod = 1 + for x in nums: + prod *= x + return prod + +# Remember that the MQTTv5 callback takes the additional 'props' parameter. +def on_connect(mqttc, userdata, flags, reason_code, props): + print(f"Connected: '{flags}', '{reason_code}', '{props}'") + if not flags.session_present: + print("Subscribing to math requests") + mqttc.subscribe("requests/math/#") + +# Each incoming message should be an RPC request on the +# 'requests/math/#' topic. +def on_message(mqttc, userdata, msg): + print(msg.topic + " " + str(msg.payload)) + + # Get the response properties, abort if they're not given + props = msg.properties + if not hasattr(props, 'ResponseTopic') or not hasattr(props, 'CorrelationData'): + print("No reply requested") + return + + corr_id = props.CorrelationData + reply_to = props.ResponseTopic + + # The command parameters are in the payload + nums = json.loads(msg.payload) + + # The requested command is at the end of the topic + res = 0 + if msg.topic.endswith("add"): + res = add(nums) + elif msg.topic.endswith("mult"): + res = mult(nums) + + # Now we have the result, res, so send it back on the 'reply_to' + # topic using the same correlation ID as the request. + print("Sending response "+str(res)+" on '"+reply_to+"': "+str(corr_id)) + props = mqtt.Properties(PacketTypes.PUBLISH) + props.CorrelationData = corr_id + + payload = json.dumps(res) + mqttc.publish(reply_to, payload, qos=1, properties=props) + +def on_log(mqttc, obj, level, string): + print(string) + + +# Typically with an RPC service, you want to make sure that you're the only +# client answering requests for specific topics. Using a known client ID +# might help. +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="paho_rpc_math_srvr", protocol=mqtt.MQTTv5) +mqttc.on_message = on_message +mqttc.on_connect = on_connect + +# Uncomment to enable debug messages +#mqttc.on_log = on_log + +mqttc.connect(host="mqtt.eclipseprojects.io", clean_start=False) +mqttc.loop_forever() diff --git a/phaoUtils/examples/subscribe_callback.py b/phaoUtils/examples/subscribe_callback.py new file mode 100644 index 0000000..f12bccc --- /dev/null +++ b/phaoUtils/examples/subscribe_callback.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation + +# This shows an example of using the subscribe.callback helper function. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.subscribe as subscribe + + +def print_msg(client, userdata, message): + print("%s : %s" % (message.topic, message.payload)) + +subscribe.callback(print_msg, "#", hostname="mqtt.eclipseprojects.io") diff --git a/phaoUtils/examples/subscribe_simple.py b/phaoUtils/examples/subscribe_simple.py new file mode 100644 index 0000000..87adb9f --- /dev/null +++ b/phaoUtils/examples/subscribe_simple.py @@ -0,0 +1,27 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 Roger Light +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Distribution License v1.0 +# which accompanies this distribution. +# +# The Eclipse Distribution License is available at +# http://www.eclipse.org/org/documents/edl-v10.php. +# +# Contributors: +# Roger Light - initial implementation + +# This shows an example of using the subscribe.simple helper function. + +import context # Ensures paho is in PYTHONPATH + +import paho.mqtt.subscribe as subscribe + +topics = ['#'] + +m = subscribe.simple(topics, hostname="mqtt.eclipseprojects.io", retained=False, msg_count=2) +for a in m: + print(a.topic) + print(a.payload) diff --git a/phaoUtils/notice.html b/phaoUtils/notice.html new file mode 100644 index 0000000..c201955 --- /dev/null +++ b/phaoUtils/notice.html @@ -0,0 +1,108 @@ + + + + + +Eclipse Foundation Software User Agreement + + + +

    Eclipse Foundation Software User Agreement

    +

    February 1, 2011

    + +

    Usage Of Content

    + +

    THE ECLIPSE FOUNDATION MAKES AVAILABLE SOFTWARE, DOCUMENTATION, INFORMATION AND/OR OTHER MATERIALS FOR OPEN SOURCE PROJECTS + (COLLECTIVELY "CONTENT"). USE OF THE CONTENT IS GOVERNED BY THE TERMS AND CONDITIONS OF THIS AGREEMENT AND/OR THE TERMS AND + CONDITIONS OF LICENSE AGREEMENTS OR NOTICES INDICATED OR REFERENCED BELOW. BY USING THE CONTENT, YOU AGREE THAT YOUR USE + OF THE CONTENT IS GOVERNED BY THIS AGREEMENT AND/OR THE TERMS AND CONDITIONS OF ANY APPLICABLE LICENSE AGREEMENTS OR + NOTICES INDICATED OR REFERENCED BELOW. IF YOU DO NOT AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT AND THE TERMS AND + CONDITIONS OF ANY APPLICABLE LICENSE AGREEMENTS OR NOTICES INDICATED OR REFERENCED BELOW, THEN YOU MAY NOT USE THE CONTENT.

    + +

    Applicable Licenses

    + +

    Unless otherwise indicated, all Content made available by the Eclipse Foundation is provided to you under the terms and conditions of the Eclipse Public License Version 2.0 + ("EPL"). A copy of the EPL is provided with this Content and is also available at http://www.eclipse.org/legal/epl-v20.html. + For purposes of the EPL, "Program" will mean the Content.

    + +

    Content includes, but is not limited to, source code, object code, documentation and other files maintained in the Eclipse Foundation source code + repository ("Repository") in software modules ("Modules") and made available as downloadable archives ("Downloads").

    + +
      +
    • Content may be structured and packaged into modules to facilitate delivering, extending, and upgrading the Content. Typical modules may include plug-ins ("Plug-ins"), plug-in fragments ("Fragments"), and features ("Features").
    • +
    • Each Plug-in or Fragment may be packaged as a sub-directory or JAR (Java™ ARchive) in a directory named "plugins".
    • +
    • A Feature is a bundle of one or more Plug-ins and/or Fragments and associated material. Each Feature may be packaged as a sub-directory in a directory named "features". Within a Feature, files named "feature.xml" may contain a list of the names and version numbers of the Plug-ins + and/or Fragments associated with that Feature.
    • +
    • Features may also include other Features ("Included Features"). Within a Feature, files named "feature.xml" may contain a list of the names and version numbers of Included Features.
    • +
    + +

    The terms and conditions governing Plug-ins and Fragments should be contained in files named "about.html" ("Abouts"). The terms and conditions governing Features and +Included Features should be contained in files named "license.html" ("Feature Licenses"). Abouts and Feature Licenses may be located in any directory of a Download or Module +including, but not limited to the following locations:

    + +
      +
    • The top-level (root) directory
    • +
    • Plug-in and Fragment directories
    • +
    • Inside Plug-ins and Fragments packaged as JARs
    • +
    • Sub-directories of the directory named "src" of certain Plug-ins
    • +
    • Feature directories
    • +
    + +

    Note: if a Feature made available by the Eclipse Foundation is installed using the Provisioning Technology (as defined below), you must agree to a license ("Feature Update License") during the +installation process. If the Feature contains Included Features, the Feature Update License should either provide you with the terms and conditions governing the Included Features or +inform you where you can locate them. Feature Update Licenses may be found in the "license" property of files named "feature.properties" found within a Feature. +Such Abouts, Feature Licenses, and Feature Update Licenses contain the terms and conditions (or references to such terms and conditions) that govern your use of the associated Content in +that directory.

    + +

    THE ABOUTS, FEATURE LICENSES, AND FEATURE UPDATE LICENSES MAY REFER TO THE EPL OR OTHER LICENSE AGREEMENTS, NOTICES OR TERMS AND CONDITIONS. SOME OF THESE +OTHER LICENSE AGREEMENTS MAY INCLUDE (BUT ARE NOT LIMITED TO):

    + + + +

    IT IS YOUR OBLIGATION TO READ AND ACCEPT ALL SUCH TERMS AND CONDITIONS PRIOR TO USE OF THE CONTENT. If no About, Feature License, or Feature Update License is provided, please +contact the Eclipse Foundation to determine what terms and conditions govern that particular Content.

    + + +

    Use of Provisioning Technology

    + +

    The Eclipse Foundation makes available provisioning software, examples of which include, but are not limited to, p2 and the Eclipse + Update Manager ("Provisioning Technology") for the purpose of allowing users to install software, documentation, information and/or + other materials (collectively "Installable Software"). This capability is provided with the intent of allowing such users to + install, extend and update Eclipse-based products. Information about packaging Installable Software is available at http://eclipse.org/equinox/p2/repository_packaging.html + ("Specification").

    + +

    You may use Provisioning Technology to allow other parties to install Installable Software. You shall be responsible for enabling the + applicable license agreements relating to the Installable Software to be presented to, and accepted by, the users of the Provisioning Technology + in accordance with the Specification. By using Provisioning Technology in such a manner and making it available in accordance with the + Specification, you further acknowledge your agreement to, and the acquisition of all necessary rights to permit the following:

    + +
      +
    1. A series of actions may occur ("Provisioning Process") in which a user may execute the Provisioning Technology + on a machine ("Target Machine") with the intent of installing, extending or updating the functionality of an Eclipse-based + product.
    2. +
    3. During the Provisioning Process, the Provisioning Technology may cause third party Installable Software or a portion thereof to be + accessed and copied to the Target Machine.
    4. +
    5. Pursuant to the Specification, you will provide to the user the terms and conditions that govern the use of the Installable + Software ("Installable Software Agreement") and such Installable Software Agreement shall be accessed from the Target + Machine in accordance with the Specification. Such Installable Software Agreement must inform the user of the terms and conditions that govern + the Installable Software and must solicit acceptance by the end user in the manner prescribed in such Installable Software Agreement. Upon such + indication of agreement by the user, the provisioning Technology will complete installation of the Installable Software.
    6. +
    + +

    Cryptography

    + +

    Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to + another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, + possession, or use, and re-export of encryption software, to see if this is permitted.

    + +

    Java and all Java-based trademarks are trademarks of Oracle Corporation in the United States, other countries, or both.

    + + diff --git a/phaoUtils/pyproject.toml b/phaoUtils/pyproject.toml new file mode 100644 index 0000000..e990812 --- /dev/null +++ b/phaoUtils/pyproject.toml @@ -0,0 +1,146 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "paho-mqtt" +dynamic = ["version"] +description = "MQTT version 5.0/3.1.1 client class" +readme = "README.rst" +# see https://lists.spdx.org/g/Spdx-legal/topic/request_for_adding_eclipse/67981884 +# for why Eclipse Distribution License v1.0 is listed as BSD-3-Clause +license = { text = "EPL-2.0 OR BSD-3-Clause" } +requires-python = ">=3.7" +authors = [ + { name = "Roger Light", email = "roger@atchoo.org" }, +] +keywords = [ + "paho", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Communications", + "Topic :: Internet", +] +dependencies = [] + +[project.optional-dependencies] +proxy = [ + "PySocks", +] + +[project.urls] +Homepage = "http://eclipse.org/paho" + +[tool.coverage.report] +exclude_also = [ + "@(abc\\.)?abstractmethod", + "def __repr__", + "except ImportError:", + "if TYPE_CHECKING:", + "raise AssertionError", + "raise NotImplementedError", +] + +[tool.hatch.build.targets.sdist] +include = [ + "src/paho", + "/examples", + "/tests", + "about.html", + "CONTRIBUTING.md", + "edl-v10", + "epl-v20", + "LICENSE.txt", + "notice.html", + "README.rst", +] + +[tool.hatch.build.targets.wheel] +sources = ["src"] +include = [ + "src/paho", +] + +[tool.hatch.version] +path = "src/paho/mqtt/__init__.py" + + +[tool.mypy] + +[[tool.mypy.overrides]] +module = "paho.mqtt.client" +# check_untyped_defs = true +# disallow_untyped_calls = true +# disallow_incomplete_defs = true +disallow_untyped_defs = true + +[tool.pytest.ini_options] +addopts = ["-r", "xs"] +testpaths = "tests src" +filterwarnings = [ + "ignore:Callback API version 1 is deprecated, update to latest version" +] + +[tool.ruff] +exclude = ["test/lib/python/*"] +extend-select = [ + "B", + "C4", + "E", + "E9", + "F63", + "F7", + "F82", + "FLY", # flynt + "I", + "ISC", + "PERF", + "S", # Bandit + "UP", + "RUF", + "W", +] +ignore = [] +line-length = 167 + +[tool.ruff.per-file-ignores] +"examples/**/*.py" = [ + "B", + "E402", + "E711", + "E741", + "F401", + "F811", + "F841", + "I", + "PERF", + "S", + "UP", +] +"tests/**/*.py" = [ + "F811", + "PERF203", + "S101", + "S105", + "S106", +] + +[tool.typos.default.extend-words] +Mosquitto = "Mosquitto" + +[tool.typos.type.sh.extend-words] +# gen.sh use the openssl option pass(word) in +passin = "passin" diff --git a/phaoUtils/tests/__init__.py b/phaoUtils/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/phaoUtils/tests/consts.py b/phaoUtils/tests/consts.py new file mode 100644 index 0000000..c9e7eb4 --- /dev/null +++ b/phaoUtils/tests/consts.py @@ -0,0 +1,5 @@ +import pathlib + +tests_path = pathlib.Path(__file__).parent +lib_path = tests_path.parent +ssl_path = tests_path / "ssl" diff --git a/phaoUtils/tests/debug_helpers.py b/phaoUtils/tests/debug_helpers.py new file mode 100644 index 0000000..54b9636 --- /dev/null +++ b/phaoUtils/tests/debug_helpers.py @@ -0,0 +1,223 @@ +import binascii +import struct +from typing import Tuple + + +def dump_packet(prefix: str, data: bytes) -> None: + try: + data = to_string(data) + print(prefix, ": ", data, sep="") + except struct.error: + data = binascii.b2a_hex(data).decode('utf8') + print(prefix, " (not decoded): 0x", data, sep="") + + +def remaining_length(packet: bytes) -> Tuple[bytes, int]: + l = min(5, len(packet)) # noqa: E741 + all_bytes = struct.unpack("!" + "B" * l, packet[:l]) + mult = 1 + rl = 0 + for i in range(1, l - 1): + byte = all_bytes[i] + + rl += (byte & 127) * mult + mult *= 128 + if byte & 128 == 0: + packet = packet[i + 1:] + break + + return (packet, rl) + + +def to_hex_string(packet: bytes) -> str: + if not packet: + return "" + + s = "" + while len(packet) > 0: + packet0 = struct.unpack("!B", packet[0]) + s = s+hex(packet0[0]) + " " + packet = packet[1:] + + return s + + +def to_string(packet: bytes) -> str: + if not packet: + return "" + + packet0 = struct.unpack("!B%ds" % (len(packet)-1), bytes(packet)) + packet0 = packet0[0] + cmd = packet0 & 0xF0 + if cmd == 0x00: + # Reserved + return "0x00" + elif cmd == 0x10: + # CONNECT + (packet, rl) = remaining_length(packet) + pack_format = "!H" + str(len(packet) - 2) + 's' + (slen, packet) = struct.unpack(pack_format, packet) + pack_format = "!" + str(slen) + 'sBBH' + str(len(packet) - slen - 4) + 's' + (protocol, proto_ver, flags, keepalive, packet) = struct.unpack(pack_format, packet) + kind = ("clean-session" if flags & 2 else "durable") + s = f"CONNECT, proto={protocol}{proto_ver}, keepalive={keepalive}, {kind}" + + pack_format = "!H" + str(len(packet) - 2) + 's' + (slen, packet) = struct.unpack(pack_format, packet) + pack_format = "!" + str(slen) + 's' + str(len(packet) - slen) + 's' + (client_id, packet) = struct.unpack(pack_format, packet) + s = s + ", id=" + str(client_id) + + if flags & 4: + pack_format = "!H" + str(len(packet) - 2) + 's' + (slen, packet) = struct.unpack(pack_format, packet) + pack_format = "!" + str(slen) + 's' + str(len(packet) - slen) + 's' + (will_topic, packet) = struct.unpack(pack_format, packet) + s = s + ", will-topic=" + str(will_topic) + + pack_format = "!H" + str(len(packet) - 2) + 's' + (slen, packet) = struct.unpack(pack_format, packet) + pack_format = "!" + str(slen) + 's' + str(len(packet) - slen) + 's' + (will_message, packet) = struct.unpack(pack_format, packet) + s = s + ", will-message=" + will_message + + s = s + ", will-qos=" + str((flags & 24) >> 3) + s = s + ", will-retain=" + str((flags & 32) >> 5) + + if flags & 128: + pack_format = "!H" + str(len(packet) - 2) + 's' + (slen, packet) = struct.unpack(pack_format, packet) + pack_format = "!" + str(slen) + 's' + str(len(packet) - slen) + 's' + (username, packet) = struct.unpack(pack_format, packet) + s = s + ", username=" + str(username) + + if flags & 64: + pack_format = "!H" + str(len(packet) - 2) + 's' + (slen, packet) = struct.unpack(pack_format, packet) + pack_format = "!" + str(slen) + 's' + str(len(packet) - slen) + 's' + (password, packet) = struct.unpack(pack_format, packet) + s = s + ", password=" + str(password) + + if flags & 1: + s = s + ", reserved=1" + + return s + elif cmd == 0x20: + # CONNACK + if len(packet) == 4: + (cmd, rl, resv, rc) = struct.unpack('!BBBB', packet) + return "CONNACK, rl="+str(rl)+", res="+str(resv)+", rc="+str(rc) + elif len(packet) == 5: + (cmd, rl, flags, reason_code, proplen) = struct.unpack('!BBBBB', packet) + return "CONNACK, rl="+str(rl)+", flags="+str(flags)+", rc="+str(reason_code)+", proplen="+str(proplen) + else: + return "CONNACK, (not decoded)" + + elif cmd == 0x30: + # PUBLISH + dup = (packet0 & 0x08) >> 3 + qos = (packet0 & 0x06) >> 1 + retain = (packet0 & 0x01) + (packet, rl) = remaining_length(packet) + pack_format = "!H" + str(len(packet) - 2) + 's' + (tlen, packet) = struct.unpack(pack_format, packet) + pack_format = "!" + str(tlen) + 's' + str(len(packet) - tlen) + 's' + (topic, packet) = struct.unpack(pack_format, packet) + s = "PUBLISH, rl=" + str(rl) + ", topic=" + str(topic) + ", qos=" + str(qos) + ", retain=" + str(retain) + ", dup=" + str(dup) + if qos > 0: + pack_format = "!H" + str(len(packet) - 2) + 's' + (mid, packet) = struct.unpack(pack_format, packet) + s = s + ", mid=" + str(mid) + + s = s + ", payload=" + str(packet) + return s + elif cmd == 0x40: + # PUBACK + if len(packet) == 5: + (cmd, rl, mid, reason_code) = struct.unpack('!BBHB', packet) + return "PUBACK, rl="+str(rl)+", mid="+str(mid)+", reason_code="+str(reason_code) + else: + (cmd, rl, mid) = struct.unpack('!BBH', packet) + return "PUBACK, rl="+str(rl)+", mid="+str(mid) + elif cmd == 0x50: + # PUBREC + if len(packet) == 5: + (cmd, rl, mid, reason_code) = struct.unpack('!BBHB', packet) + return "PUBREC, rl="+str(rl)+", mid="+str(mid)+", reason_code="+str(reason_code) + else: + (cmd, rl, mid) = struct.unpack('!BBH', packet) + return "PUBREC, rl="+str(rl)+", mid="+str(mid) + elif cmd == 0x60: + # PUBREL + dup = (packet0 & 0x08) >> 3 + (cmd, rl, mid) = struct.unpack('!BBH', packet) + return "PUBREL, rl=" + str(rl) + ", mid=" + str(mid) + ", dup=" + str(dup) + elif cmd == 0x70: + # PUBCOMP + (cmd, rl, mid) = struct.unpack('!BBH', packet) + return "PUBCOMP, rl=" + str(rl) + ", mid=" + str(mid) + elif cmd == 0x80: + # SUBSCRIBE + (packet, rl) = remaining_length(packet) + pack_format = "!H" + str(len(packet) - 2) + 's' + (mid, packet) = struct.unpack(pack_format, packet) + s = "SUBSCRIBE, rl=" + str(rl) + ", mid=" + str(mid) + topic_index = 0 + while len(packet) > 0: + pack_format = "!H" + str(len(packet) - 2) + 's' + (tlen, packet) = struct.unpack(pack_format, packet) + pack_format = "!" + str(tlen) + 'sB' + str(len(packet) - tlen - 1) + 's' + (topic, qos, packet) = struct.unpack(pack_format, packet) + s = s + ", topic" + str(topic_index) + "=" + str(topic) + "," + str(qos) + return s + elif cmd == 0x90: + # SUBACK + (packet, rl) = remaining_length(packet) + pack_format = "!H" + str(len(packet) - 2) + 's' + (mid, packet) = struct.unpack(pack_format, packet) + pack_format = "!" + "B" * len(packet) + granted_qos = struct.unpack(pack_format, packet) + + s = "SUBACK, rl=" + str(rl) + ", mid=" + str(mid) + ", granted_qos=" + str(granted_qos[0]) + for i in range(1, len(granted_qos) - 1): + s = s + ", " + str(granted_qos[i]) + return s + elif cmd == 0xA0: + # UNSUBSCRIBE + (packet, rl) = remaining_length(packet) + pack_format = "!H" + str(len(packet) - 2) + 's' + (mid, packet) = struct.unpack(pack_format, packet) + s = "UNSUBSCRIBE, rl=" + str(rl) + ", mid=" + str(mid) + topic_index = 0 + while len(packet) > 0: + pack_format = "!H" + str(len(packet) - 2) + 's' + (tlen, packet) = struct.unpack(pack_format, packet) + pack_format = "!" + str(tlen) + 's' + str(len(packet) - tlen) + 's' + (topic, packet) = struct.unpack(pack_format, packet) + s = s + ", topic" + str(topic_index) + "=" + str(topic) + return s + elif cmd == 0xB0: + # UNSUBACK + (cmd, rl, mid) = struct.unpack('!BBH', packet) + return "UNSUBACK, rl=" + str(rl) + ", mid=" + str(mid) + elif cmd == 0xC0: + # PINGREQ + (cmd, rl) = struct.unpack('!BB', packet) + return "PINGREQ, rl=" + str(rl) + elif cmd == 0xD0: + # PINGRESP + (cmd, rl) = struct.unpack('!BB', packet) + return "PINGRESP, rl=" + str(rl) + elif cmd == 0xE0: + # DISCONNECT + if len(packet) == 3: + (cmd, rl, reason_code) = struct.unpack('!BBB', packet) + return "DISCONNECT, rl="+str(rl)+", reason_code="+str(reason_code) + else: + (cmd, rl) = struct.unpack('!BB', packet) + return "DISCONNECT, rl="+str(rl) + elif cmd == 0xF0: + # AUTH + (cmd, rl) = struct.unpack('!BB', packet) + return "AUTH, rl="+str(rl) + raise ValueError(f"Unknown packet type {cmd}") diff --git a/phaoUtils/tests/lib/__init__.py b/phaoUtils/tests/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/phaoUtils/tests/lib/clients/01-asyncio.py b/phaoUtils/tests/lib/clients/01-asyncio.py new file mode 100644 index 0000000..a8ba928 --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-asyncio.py @@ -0,0 +1,89 @@ +import asyncio +import socket + +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port + +client_id = 'asyncio-test' + + +class AsyncioHelper: + def __init__(self, loop, client): + self.loop = loop + self.client = client + self.client.on_socket_open = self.on_socket_open + self.client.on_socket_close = self.on_socket_close + self.client.on_socket_register_write = self.on_socket_register_write + self.client.on_socket_unregister_write = self.on_socket_unregister_write + + def on_socket_open(self, client, userdata, sock): + def cb(): + client.loop_read() + + self.loop.add_reader(sock, cb) + self.misc = self.loop.create_task(self.misc_loop()) + + def on_socket_close(self, client, userdata, sock): + self.loop.remove_reader(sock) + self.misc.cancel() + + def on_socket_register_write(self, client, userdata, sock): + def cb(): + client.loop_write() + + self.loop.add_writer(sock, cb) + + def on_socket_unregister_write(self, client, userdata, sock): + self.loop.remove_writer(sock) + + async def misc_loop(self): + while self.client.loop_misc() == mqtt.MQTT_ERR_SUCCESS: + try: + await asyncio.sleep(1) + except asyncio.CancelledError: + break + + +async def main(): + loop = asyncio.get_event_loop() + payload = "" + + def on_connect(client, obj, flags, rc): + client.subscribe("sub-test", 1) + + def on_subscribe(client, obj, mid, granted_qos): + client.unsubscribe("unsub-test") + + def on_unsubscribe(client, obj, mid): + nonlocal payload + payload = "message" + + def on_message(client, obj, msg): + client.publish("asyncio", qos=1, payload=payload) + + def on_publish(client, obj, mid): + client.disconnect() + + def on_disconnect(client, userdata, rc): + disconnected.set_result(rc) + + disconnected = loop.create_future() + + client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION1, client_id=client_id) + client.on_connect = on_connect + client.on_message = on_message + client.on_publish = on_publish + client.on_subscribe = on_subscribe + client.on_unsubscribe = on_unsubscribe + client.on_disconnect = on_disconnect + + _aioh = AsyncioHelper(loop, client) + + client.connect('localhost', get_test_server_port(), 60) + client.socket().setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 2048) + + await disconnected + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/phaoUtils/tests/lib/clients/01-decorators.py b/phaoUtils/tests/lib/clients/01-decorators.py new file mode 100644 index 0000000..55e6d48 --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-decorators.py @@ -0,0 +1,42 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "decorators-test", clean_session=True) +payload = b"" + + +@mqttc.connect_callback() +def on_connect(mqttc, obj, flags, rc): + mqttc.subscribe("sub-test", 1) + + +@mqttc.subscribe_callback() +def on_subscribe(mqttc, obj, mid, granted_qos): + mqttc.unsubscribe("unsub-test") + + +@mqttc.unsubscribe_callback() +def on_unsubscribe(mqttc, obj, mid): + global payload + payload = "message" + + +@mqttc.message_callback() +def on_message(mqttc, obj, msg): + global payload + mqttc.publish("decorators", qos=1, payload=payload) + + +@mqttc.publish_callback() +def on_publish(mqttc, obj, mid): + mqttc.disconnect() + + +@mqttc.disconnect_callback() +def on_disconnect(mqttc, obj, rc): + pass # TODO: should probably test that this gets called + + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/01-keepalive-pingreq.py b/phaoUtils/tests/lib/clients/01-keepalive-pingreq.py new file mode 100644 index 0000000..ffa6383 --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-keepalive-pingreq.py @@ -0,0 +1,14 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "01-keepalive-pingreq") +mqttc.on_connect = on_connect + +mqttc.connect("localhost", get_test_server_port(), keepalive=4) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/01-no-clean-session.py b/phaoUtils/tests/lib/clients/01-no-clean-session.py new file mode 100644 index 0000000..6296613 --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-no-clean-session.py @@ -0,0 +1,8 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "01-no-clean-session", clean_session=False) + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/01-reconnect-on-failure.py b/phaoUtils/tests/lib/clients/01-reconnect-on-failure.py new file mode 100644 index 0000000..907a2f1 --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-reconnect-on-failure.py @@ -0,0 +1,16 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, wait_for_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + mqttc.publish("reconnect/test", "message") + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "01-reconnect-on-failure", reconnect_on_failure=False) +mqttc.on_connect = on_connect + +with wait_for_keyboard_interrupt(): + mqttc.connect("localhost", get_test_server_port()) + mqttc.loop_forever() + exit(42) # this is expected by the test case diff --git a/phaoUtils/tests/lib/clients/01-unpwd-empty-password-set.py b/phaoUtils/tests/lib/clients/01-unpwd-empty-password-set.py new file mode 100644 index 0000000..40a1a43 --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-unpwd-empty-password-set.py @@ -0,0 +1,9 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "01-unpwd-set") + +mqttc.username_pw_set("uname", "") +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/01-unpwd-empty-set.py b/phaoUtils/tests/lib/clients/01-unpwd-empty-set.py new file mode 100644 index 0000000..b65d679 --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-unpwd-empty-set.py @@ -0,0 +1,9 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "01-unpwd-set") + +mqttc.username_pw_set("", "") +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/01-unpwd-set.py b/phaoUtils/tests/lib/clients/01-unpwd-set.py new file mode 100644 index 0000000..763297f --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-unpwd-set.py @@ -0,0 +1,9 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "01-unpwd-set") + +mqttc.username_pw_set("uname", ";'[08gn=#") +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/01-unpwd-unicode-set.py b/phaoUtils/tests/lib/clients/01-unpwd-unicode-set.py new file mode 100644 index 0000000..5844541 --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-unpwd-unicode-set.py @@ -0,0 +1,12 @@ + +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "01-unpwd-unicode-set") + +username = "\u00fas\u00e9rn\u00e1m\u00e9-h\u00e9ll\u00f3" +password = "h\u00e9ll\u00f3" +mqttc.username_pw_set(username, password) +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/01-will-set.py b/phaoUtils/tests/lib/clients/01-will-set.py new file mode 100644 index 0000000..c3310a4 --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-will-set.py @@ -0,0 +1,9 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "01-will-set") + +mqttc.will_set("topic/on/unexpected/disconnect", "will message", 1, True) +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/01-will-unpwd-set.py b/phaoUtils/tests/lib/clients/01-will-unpwd-set.py new file mode 100644 index 0000000..31bb976 --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-will-unpwd-set.py @@ -0,0 +1,10 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "01-will-unpwd-set") + +mqttc.username_pw_set("oibvvwqw", "#'^2hg9a&nm38*us") +mqttc.will_set("will-topic", "will message", 2, False) +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/01-zero-length-clientid.py b/phaoUtils/tests/lib/clients/01-zero-length-clientid.py new file mode 100644 index 0000000..992efed --- /dev/null +++ b/phaoUtils/tests/lib/clients/01-zero-length-clientid.py @@ -0,0 +1,20 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + mqttc.disconnect() + + +def on_disconnect(mqttc, obj, rc): + mqttc.loop() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "", clean_session=True, protocol=mqtt.MQTTv311) +mqttc.on_connect = on_connect +mqttc.on_disconnect = on_disconnect + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/02-subscribe-qos0.py b/phaoUtils/tests/lib/clients/02-subscribe-qos0.py new file mode 100644 index 0000000..2444ffb --- /dev/null +++ b/phaoUtils/tests/lib/clients/02-subscribe-qos0.py @@ -0,0 +1,20 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + mqttc.subscribe("qos0/test", 0) + + +def on_subscribe(mqttc, obj, mid, granted_qos): + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "subscribe-qos0-test", clean_session=True) +mqttc.on_connect = on_connect +mqttc.on_subscribe = on_subscribe + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/02-subscribe-qos1.py b/phaoUtils/tests/lib/clients/02-subscribe-qos1.py new file mode 100644 index 0000000..2079de6 --- /dev/null +++ b/phaoUtils/tests/lib/clients/02-subscribe-qos1.py @@ -0,0 +1,20 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + mqttc.subscribe("qos1/test", 1) + + +def on_subscribe(mqttc, obj, mid, granted_qos): + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "subscribe-qos1-test", clean_session=True) +mqttc.on_connect = on_connect +mqttc.on_subscribe = on_subscribe + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/02-subscribe-qos2.py b/phaoUtils/tests/lib/clients/02-subscribe-qos2.py new file mode 100644 index 0000000..4776a76 --- /dev/null +++ b/phaoUtils/tests/lib/clients/02-subscribe-qos2.py @@ -0,0 +1,20 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + mqttc.subscribe("qos2/test", 2) + + +def on_subscribe(mqttc, obj, mid, granted_qos): + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "subscribe-qos2-test", clean_session=True) +mqttc.on_connect = on_connect +mqttc.on_subscribe = on_subscribe + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/02-unsubscribe.py b/phaoUtils/tests/lib/clients/02-unsubscribe.py new file mode 100644 index 0000000..de36bfb --- /dev/null +++ b/phaoUtils/tests/lib/clients/02-unsubscribe.py @@ -0,0 +1,20 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + mqttc.unsubscribe("unsubscribe/test") + + +def on_unsubscribe(mqttc, obj, mid): + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "unsubscribe-test", clean_session=True) +mqttc.on_connect = on_connect +mqttc.on_unsubscribe = on_unsubscribe + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/03-publish-b2c-qos1.py b/phaoUtils/tests/lib/clients/03-publish-b2c-qos1.py new file mode 100644 index 0000000..71ac950 --- /dev/null +++ b/phaoUtils/tests/lib/clients/03-publish-b2c-qos1.py @@ -0,0 +1,25 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +expected_payload = b"message" + + +def on_message(mqttc, obj, msg): + assert msg.mid == 123, f"Invalid mid: ({msg.mid})" + assert msg.topic == "pub/qos1/receive", f"Invalid topic: ({msg.topic})" + assert msg.payload == expected_payload, f"Invalid payload: ({msg.payload})" + assert msg.qos == 1, f"Invalid qos: ({msg.qos})" + assert not msg.retain, f"Invalid retain: ({msg.retain})" + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "publish-qos1-test") +mqttc.on_connect = on_connect +mqttc.on_message = on_message + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/03-publish-b2c-qos2.py b/phaoUtils/tests/lib/clients/03-publish-b2c-qos2.py new file mode 100644 index 0000000..e3b1492 --- /dev/null +++ b/phaoUtils/tests/lib/clients/03-publish-b2c-qos2.py @@ -0,0 +1,28 @@ +import logging + +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +expected_payload = b"message" + + +def on_message(mqttc, obj, msg): + assert msg.mid == 13423, f"Invalid mid: ({msg.mid})" + assert msg.topic == "pub/qos2/receive", f"Invalid topic: ({msg.topic})" + assert msg.payload == expected_payload, f"Invalid payload: ({msg.payload})" + assert msg.qos == 2, f"Invalid qos: ({msg.qos})" + assert not msg.retain, f"Invalid retain: ({msg.retain})" + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + +logging.basicConfig(level=logging.DEBUG) +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "publish-qos2-test", clean_session=True) +mqttc.enable_logger() +mqttc.on_connect = on_connect +mqttc.on_message = on_message + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/03-publish-c2b-qos1-disconnect.py b/phaoUtils/tests/lib/clients/03-publish-c2b-qos1-disconnect.py new file mode 100644 index 0000000..52fa516 --- /dev/null +++ b/phaoUtils/tests/lib/clients/03-publish-c2b-qos1-disconnect.py @@ -0,0 +1,33 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +sent_mid = -1 + + +def on_connect(mqttc, obj, flags, rc): + global sent_mid + assert rc == 0, f"Connect failed ({rc})" + if sent_mid == -1: + res = mqttc.publish("pub/qos1/test", "message", 1) + sent_mid = res[1] + + +def on_disconnect(mqttc, obj, rc): + if rc != mqtt.MQTT_ERR_SUCCESS: + mqttc.reconnect() + + +def on_publish(mqttc, obj, mid): + global sent_mid + assert mid == sent_mid, f"Invalid mid: ({mid})" + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "publish-qos1-test", clean_session=False) +mqttc.on_connect = on_connect +mqttc.on_disconnect = on_disconnect +mqttc.on_publish = on_publish + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/03-publish-c2b-qos2-disconnect.py b/phaoUtils/tests/lib/clients/03-publish-c2b-qos2-disconnect.py new file mode 100644 index 0000000..fe7c2ae --- /dev/null +++ b/phaoUtils/tests/lib/clients/03-publish-c2b-qos2-disconnect.py @@ -0,0 +1,31 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +first_connection = 1 + + +def on_connect(mqttc, obj, flags, rc): + global first_connection + assert rc == 0, f"Connect failed ({rc})" + if first_connection == 1: + mqttc.publish("pub/qos2/test", "message", 2) + first_connection = 0 + + +def on_disconnect(mqttc, obj, rc): + if rc != 0: + mqttc.reconnect() + + +def on_publish(mqttc, obj, mid): + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "publish-qos2-test", clean_session=False) +mqttc.on_connect = on_connect +mqttc.on_disconnect = on_disconnect +mqttc.on_publish = on_publish + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/03-publish-fill-inflight.py b/phaoUtils/tests/lib/clients/03-publish-fill-inflight.py new file mode 100644 index 0000000..0a7eb85 --- /dev/null +++ b/phaoUtils/tests/lib/clients/03-publish-fill-inflight.py @@ -0,0 +1,39 @@ +import logging + +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def expected_payload(i: int) -> bytes: + return f"message{i}".encode() + + +def on_message(mqttc, obj, msg): + assert msg.mid == 123, f"Invalid mid: ({msg.mid})" + assert msg.topic == "pub/qos1/receive", f"Invalid topic: ({msg.topic})" + assert msg.payload == expected_payload, f"Invalid payload: ({msg.payload})" + assert msg.qos == 1, f"Invalid qos: ({msg.qos})" + assert msg.retain is not False, f"Invalid retain: ({msg.retain})" + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + for i in range(12): + mqttc.publish("topic", expected_payload(i), qos=1) + +def on_disconnect(mqttc, rc, properties): + logging.info("disconnected") + mqttc.reconnect() + +logging.basicConfig(level=logging.DEBUG) +logging.info(str(mqtt)) +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "publish-qos1-test") +mqttc.max_inflight_messages_set(10) +mqttc.on_connect = on_connect +mqttc.on_disconnect = on_disconnect +mqttc.on_message = on_message +mqttc.enable_logger() + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/03-publish-helper-qos0-v5.py b/phaoUtils/tests/lib/clients/03-publish-helper-qos0-v5.py new file mode 100644 index 0000000..1b59113 --- /dev/null +++ b/phaoUtils/tests/lib/clients/03-publish-helper-qos0-v5.py @@ -0,0 +1,15 @@ +import paho.mqtt.client +import paho.mqtt.publish + +from tests.paho_test import get_test_server_port, wait_for_keyboard_interrupt + +with wait_for_keyboard_interrupt(): + paho.mqtt.publish.single( + "pub/qos0/test", + "message", + qos=0, + hostname="localhost", + port=get_test_server_port(), + client_id="publish-helper-qos0-test", + protocol=paho.mqtt.client.MQTTv5, + ) diff --git a/phaoUtils/tests/lib/clients/03-publish-helper-qos0.py b/phaoUtils/tests/lib/clients/03-publish-helper-qos0.py new file mode 100644 index 0000000..a7b696e --- /dev/null +++ b/phaoUtils/tests/lib/clients/03-publish-helper-qos0.py @@ -0,0 +1,13 @@ +import paho.mqtt.publish + +from tests.paho_test import get_test_server_port, wait_for_keyboard_interrupt + +with wait_for_keyboard_interrupt(): + paho.mqtt.publish.single( + "pub/qos0/test", + "message", + qos=0, + hostname="localhost", + port=get_test_server_port(), + client_id="publish-helper-qos0-test", + ) diff --git a/phaoUtils/tests/lib/clients/03-publish-helper-qos1-disconnect.py b/phaoUtils/tests/lib/clients/03-publish-helper-qos1-disconnect.py new file mode 100644 index 0000000..b07166f --- /dev/null +++ b/phaoUtils/tests/lib/clients/03-publish-helper-qos1-disconnect.py @@ -0,0 +1,13 @@ +import paho.mqtt.publish + +from tests.paho_test import get_test_server_port, wait_for_keyboard_interrupt + +with wait_for_keyboard_interrupt(): + paho.mqtt.publish.single( + "pub/qos1/test", + "message", + qos=1, + hostname="localhost", + port=get_test_server_port(), + client_id="publish-helper-qos1-disconnect-test", + ) diff --git a/phaoUtils/tests/lib/clients/03-publish-qos0-no-payload.py b/phaoUtils/tests/lib/clients/03-publish-qos0-no-payload.py new file mode 100644 index 0000000..7ac8e35 --- /dev/null +++ b/phaoUtils/tests/lib/clients/03-publish-qos0-no-payload.py @@ -0,0 +1,24 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +sent_mid = -1 + + +def on_connect(mqttc, obj, flags, rc): + global sent_mid + assert rc == 0, f"Connect failed ({rc})" + (res, sent_mid) = mqttc.publish("pub/qos0/no-payload/test") + + +def on_publish(mqttc, obj, mid): + if sent_mid == mid: + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "publish-qos0-test-np", clean_session=True) +mqttc.on_connect = on_connect +mqttc.on_publish = on_publish + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/03-publish-qos0.py b/phaoUtils/tests/lib/clients/03-publish-qos0.py new file mode 100644 index 0000000..dc6ac74 --- /dev/null +++ b/phaoUtils/tests/lib/clients/03-publish-qos0.py @@ -0,0 +1,26 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + +sent_mid = -1 + + +def on_connect(mqttc, obj, flags, rc): + global sent_mid + assert rc == 0, f"Connect failed ({rc})" + res = mqttc.publish("pub/qos0/test", "message") + sent_mid = res[1] + + +def on_publish(mqttc, obj, mid): + global sent_mid, run + if sent_mid == mid: + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "publish-qos0-test", clean_session=True) +mqttc.on_connect = on_connect +mqttc.on_publish = on_publish + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/04-retain-qos0.py b/phaoUtils/tests/lib/clients/04-retain-qos0.py new file mode 100644 index 0000000..bdfa660 --- /dev/null +++ b/phaoUtils/tests/lib/clients/04-retain-qos0.py @@ -0,0 +1,15 @@ +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + mqttc.publish("retain/qos0/test", "retained message", 0, True) + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "retain-qos0-test", clean_session=True) +mqttc.on_connect = on_connect + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/08-ssl-connect-alpn.py b/phaoUtils/tests/lib/clients/08-ssl-connect-alpn.py new file mode 100644 index 0000000..f830e50 --- /dev/null +++ b/phaoUtils/tests/lib/clients/08-ssl-connect-alpn.py @@ -0,0 +1,23 @@ +import os + +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "08-ssl-connect-alpn", clean_session=True) +mqttc.tls_set( + os.path.join(os.environ["PAHO_SSL_PATH"], "all-ca.crt"), + os.path.join(os.environ["PAHO_SSL_PATH"], "client.crt"), + os.path.join(os.environ["PAHO_SSL_PATH"], "client.key"), + alpn_protocols=["paho-test-protocol"], +) +mqttc.on_connect = on_connect + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/08-ssl-connect-cert-auth-pw.py b/phaoUtils/tests/lib/clients/08-ssl-connect-cert-auth-pw.py new file mode 100644 index 0000000..4681dc4 --- /dev/null +++ b/phaoUtils/tests/lib/clients/08-ssl-connect-cert-auth-pw.py @@ -0,0 +1,23 @@ +import os + +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "08-ssl-connect-crt-auth-pw") +mqttc.tls_set( + os.path.join(os.environ["PAHO_SSL_PATH"], "all-ca.crt"), + os.path.join(os.environ["PAHO_SSL_PATH"], "client-pw.crt"), + os.path.join(os.environ["PAHO_SSL_PATH"], "client-pw.key"), + keyfile_password="password", +) +mqttc.on_connect = on_connect + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/08-ssl-connect-cert-auth.py b/phaoUtils/tests/lib/clients/08-ssl-connect-cert-auth.py new file mode 100644 index 0000000..a34409a --- /dev/null +++ b/phaoUtils/tests/lib/clients/08-ssl-connect-cert-auth.py @@ -0,0 +1,22 @@ +import os + +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "08-ssl-connect-crt-auth") +mqttc.tls_set( + os.path.join(os.environ["PAHO_SSL_PATH"], "all-ca.crt"), + os.path.join(os.environ["PAHO_SSL_PATH"], "client.crt"), + os.path.join(os.environ["PAHO_SSL_PATH"], "client.key"), +) +mqttc.on_connect = on_connect + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/08-ssl-connect-no-auth.py b/phaoUtils/tests/lib/clients/08-ssl-connect-no-auth.py new file mode 100644 index 0000000..9d86f28 --- /dev/null +++ b/phaoUtils/tests/lib/clients/08-ssl-connect-no-auth.py @@ -0,0 +1,18 @@ +import os + +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, loop_until_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + assert rc == 0, f"Connect failed ({rc})" + mqttc.disconnect() + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "08-ssl-connect-no-auth") +mqttc.tls_set(os.path.join(os.environ["PAHO_SSL_PATH"], "all-ca.crt")) +mqttc.on_connect = on_connect + +mqttc.connect("localhost", get_test_server_port()) +loop_until_keyboard_interrupt(mqttc) diff --git a/phaoUtils/tests/lib/clients/08-ssl-fake-cacert.py b/phaoUtils/tests/lib/clients/08-ssl-fake-cacert.py new file mode 100644 index 0000000..ffa5364 --- /dev/null +++ b/phaoUtils/tests/lib/clients/08-ssl-fake-cacert.py @@ -0,0 +1,27 @@ +import os +import ssl + +import paho.mqtt.client as mqtt + +from tests.paho_test import get_test_server_port, wait_for_keyboard_interrupt + + +def on_connect(mqttc, obj, flags, rc): + raise RuntimeError("Connection should have failed!") + + +mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "08-ssl-fake-cacert") +mqttc.tls_set( + os.path.join(os.environ["PAHO_SSL_PATH"], "test-fake-root-ca.crt"), + os.path.join(os.environ["PAHO_SSL_PATH"], "client.crt"), + os.path.join(os.environ["PAHO_SSL_PATH"], "client.key"), +) +mqttc.on_connect = on_connect + +with wait_for_keyboard_interrupt(): + try: + mqttc.connect("localhost", get_test_server_port()) + except ssl.SSLError as msg: + assert msg.errno == 1 and "certificate verify failed" in msg.strerror + else: + raise Exception("Expected SSLError") diff --git a/phaoUtils/tests/lib/conftest.py b/phaoUtils/tests/lib/conftest.py new file mode 100644 index 0000000..30ab6fd --- /dev/null +++ b/phaoUtils/tests/lib/conftest.py @@ -0,0 +1,80 @@ +import os +import signal +import subprocess +import sys + +import pytest + +from tests.consts import ssl_path, tests_path +from tests.paho_test import create_server_socket, create_server_socket_ssl, ssl + +clients_path = tests_path / "lib" / "clients" + + +def _yield_server(monkeypatch, sockport): + sock, port = sockport + monkeypatch.setenv("PAHO_SERVER_PORT", str(port)) + try: + yield sock + finally: + sock.close() + + +@pytest.fixture() +def server_socket(monkeypatch): + yield from _yield_server(monkeypatch, create_server_socket()) + + +@pytest.fixture() +def ssl_server_socket(monkeypatch): + if ssl is None: + pytest.skip("no ssl module") + yield from _yield_server(monkeypatch, create_server_socket_ssl()) + + +@pytest.fixture() +def alpn_ssl_server_socket(monkeypatch): + if ssl is None: + pytest.skip("no ssl module") + if not getattr(ssl, "HAS_ALPN", False): + pytest.skip("ALPN not supported in this version of Python") + yield from _yield_server(monkeypatch, create_server_socket_ssl(alpn_protocols=["paho-test-protocol"])) + + +def stop_process(proc: subprocess.Popen) -> None: + if sys.platform == "win32": + proc.send_signal(signal.CTRL_C_EVENT) + else: + proc.send_signal(signal.SIGINT) + try: + proc.wait(5) + except subprocess.TimeoutExpired: + proc.terminate() + + +@pytest.fixture() +def start_client(request: pytest.FixtureRequest): + def starter(name: str, expected_returncode: int = 0) -> None: + client_path = clients_path / name + if not client_path.exists(): + raise FileNotFoundError(client_path) + env = dict( + os.environ, + PAHO_SSL_PATH=str(ssl_path), + PYTHONPATH=f"{tests_path}{os.pathsep}{os.environ.get('PYTHONPATH', '')}", + ) + assert 'PAHO_SERVER_PORT' in env, "PAHO_SERVER_PORT must be set in the environment when starting a client" + proc = subprocess.Popen([ # noqa: S603 + sys.executable, + str(client_path), + ], env=env) + + def fin(): + stop_process(proc) + if proc.returncode != expected_returncode: + raise RuntimeError(f"Client {name} exited with code {proc.returncode}, expected {expected_returncode}") + + request.addfinalizer(fin) + return proc + + return starter diff --git a/phaoUtils/tests/lib/test_01_asyncio.py b/phaoUtils/tests/lib/test_01_asyncio.py new file mode 100644 index 0000000..1496737 --- /dev/null +++ b/phaoUtils/tests/lib/test_01_asyncio.py @@ -0,0 +1,45 @@ +# Test whether asyncio works + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("asyncio-test", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +subscribe_packet = paho_test.gen_subscribe(mid=1, topic="sub-test", qos=1) +suback_packet = paho_test.gen_suback(mid=1, qos=1) + +unsubscribe_packet = paho_test.gen_unsubscribe(mid=2, topic="unsub-test") +unsuback_packet = paho_test.gen_unsuback(mid=2) + +publish_packet = paho_test.gen_publish("b2c", qos=0, payload="msg") + +publish_packet_in = paho_test.gen_publish("asyncio", qos=1, mid=3, payload="message") +puback_packet_in = paho_test.gen_puback(mid=3) + +disconnect_packet = paho_test.gen_disconnect() + + +def test_01_asyncio(server_socket, start_client): + proc = start_client("01-asyncio.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "subscribe", subscribe_packet) + conn.send(suback_packet) + + paho_test.expect_packet(conn, "unsubscribe", unsubscribe_packet) + conn.send(unsuback_packet) + conn.send(publish_packet) + + paho_test.expect_packet(conn, "publish", publish_packet_in) + conn.send(puback_packet_in) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() + assert proc.wait() == 0 diff --git a/phaoUtils/tests/lib/test_01_decorators.py b/phaoUtils/tests/lib/test_01_decorators.py new file mode 100644 index 0000000..7ae66ef --- /dev/null +++ b/phaoUtils/tests/lib/test_01_decorators.py @@ -0,0 +1,44 @@ +# Test whether callback decorators work + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("decorators-test", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +subscribe_packet = paho_test.gen_subscribe(mid=1, topic="sub-test", qos=1) +suback_packet = paho_test.gen_suback(mid=1, qos=1) + +unsubscribe_packet = paho_test.gen_unsubscribe(mid=2, topic="unsub-test") +unsuback_packet = paho_test.gen_unsuback(mid=2) + +publish_packet = paho_test.gen_publish("b2c", qos=0, payload="msg") + +publish_packet_in = paho_test.gen_publish("decorators", qos=1, mid=3, payload="message") +puback_packet_in = paho_test.gen_puback(mid=3) + +disconnect_packet = paho_test.gen_disconnect() + + +def test_01_decorators(server_socket, start_client): + start_client("01-decorators.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "subscribe", subscribe_packet) + conn.send(suback_packet) + + paho_test.expect_packet(conn, "unsubscribe", unsubscribe_packet) + conn.send(unsuback_packet) + conn.send(publish_packet) + + paho_test.expect_packet(conn, "publish", publish_packet_in) + conn.send(puback_packet_in) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_01_keepalive_pingreq.py b/phaoUtils/tests/lib/test_01_keepalive_pingreq.py new file mode 100644 index 0000000..4c327db --- /dev/null +++ b/phaoUtils/tests/lib/test_01_keepalive_pingreq.py @@ -0,0 +1,32 @@ +# Test whether a client sends a pingreq after the keepalive time + +# The client should connect with keepalive=4, clean session set, +# and client id 01-keepalive-pingreq +# The client should send a PINGREQ message after the appropriate amount of time +# (4 seconds after no traffic). + +import time + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("01-keepalive-pingreq", keepalive=4) +connack_packet = paho_test.gen_connack(rc=0) + +pingreq_packet = paho_test.gen_pingreq() +pingresp_packet = paho_test.gen_pingresp() + + +def test_01_keepalive_pingreq(server_socket, start_client): + start_client("01-keepalive-pingreq.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "pingreq", pingreq_packet) + time.sleep(1.0) + conn.send(pingresp_packet) + + paho_test.expect_packet(conn, "pingreq", pingreq_packet) diff --git a/phaoUtils/tests/lib/test_01_no_clean_session.py b/phaoUtils/tests/lib/test_01_no_clean_session.py new file mode 100644 index 0000000..7f00e54 --- /dev/null +++ b/phaoUtils/tests/lib/test_01_no_clean_session.py @@ -0,0 +1,20 @@ +# Test whether a client produces a correct connect with clean session not set. + +# The client should connect with keepalive=60, clean session not +# set, and client id 01-no-clean-session. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("01-no-clean-session", clean_session=False, keepalive=60) + + +def test_01_no_clean_session(server_socket, start_client): + start_client("01-no-clean-session.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_01_reconnect_on_failure.py b/phaoUtils/tests/lib/test_01_reconnect_on_failure.py new file mode 100644 index 0000000..8deb653 --- /dev/null +++ b/phaoUtils/tests/lib/test_01_reconnect_on_failure.py @@ -0,0 +1,31 @@ +# Test the reconnect_on_failure = False mode +import pytest + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("01-reconnect-on-failure", keepalive=60) +connack_packet_ok = paho_test.gen_connack(rc=0) +connack_packet_failure = paho_test.gen_connack(rc=1) # CONNACK_REFUSED_PROTOCOL_VERSION + +publish_packet = paho_test.gen_publish( + "reconnect/test", qos=0, payload="message") + + +@pytest.mark.parametrize("ok_code", [False, True]) +def test_01_reconnect_on_failure(server_socket, start_client, ok_code): + client = start_client("01-reconnect-on-failure.py", expected_returncode=42) + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + if ok_code: + conn.send(connack_packet_ok) + # Connection is a success, so we expect a publish + paho_test.expect_packet(conn, "publish", publish_packet) + else: + conn.send(connack_packet_failure) + conn.close() + # Expect the client to quit here due to socket being closed + client.wait(1) + assert client.returncode == 42 diff --git a/phaoUtils/tests/lib/test_01_unpwd_empty_password_set.py b/phaoUtils/tests/lib/test_01_unpwd_empty_password_set.py new file mode 100644 index 0000000..225d72b --- /dev/null +++ b/phaoUtils/tests/lib/test_01_unpwd_empty_password_set.py @@ -0,0 +1,21 @@ +# Test whether a client produces a correct connect with a username and password. + +# The client should connect with keepalive=60, clean session set, +# client id 01-unpwd-set, username set to uname and password set to empty string + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect( + "01-unpwd-set", keepalive=60, username="uname", password="") + + +def test_01_unpwd_empty_password_set(server_socket, start_client): + start_client("01-unpwd-empty-password-set.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_01_unpwd_empty_set.py b/phaoUtils/tests/lib/test_01_unpwd_empty_set.py new file mode 100644 index 0000000..8c51c22 --- /dev/null +++ b/phaoUtils/tests/lib/test_01_unpwd_empty_set.py @@ -0,0 +1,21 @@ +# Test whether a client produces a correct connect with a username and password. + +# The client should connect with keepalive=60, clean session set, +# client id 01-unpwd-set, username and password set to empty string. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect( + "01-unpwd-set", keepalive=60, username="", password='') + + +def test_01_unpwd_empty_set(server_socket, start_client): + start_client("01-unpwd-empty-set.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_01_unpwd_set.py b/phaoUtils/tests/lib/test_01_unpwd_set.py new file mode 100644 index 0000000..38834ab --- /dev/null +++ b/phaoUtils/tests/lib/test_01_unpwd_set.py @@ -0,0 +1,21 @@ +# Test whether a client produces a correct connect with a username and password. + +# The client should connect with keepalive=60, clean session set, +# client id 01-unpwd-set, username set to uname and password set to ;'[08gn=# + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect( + "01-unpwd-set", keepalive=60, username="uname", password=";'[08gn=#") + + +def test_01_unpwd_set(server_socket, start_client): + start_client("01-unpwd-set.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_01_unpwd_unicode_set.py b/phaoUtils/tests/lib/test_01_unpwd_unicode_set.py new file mode 100644 index 0000000..64e49af --- /dev/null +++ b/phaoUtils/tests/lib/test_01_unpwd_unicode_set.py @@ -0,0 +1,25 @@ +# Test whether a client produces a correct connect with a unicode username and password. + +# The client should connect with keepalive=60, clean session set, +# client id 01-unpwd-unicode-set, username and password from corresponding variables + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect( + "01-unpwd-unicode-set", + keepalive=60, + username="\u00fas\u00e9rn\u00e1m\u00e9-h\u00e9ll\u00f3", + password="h\u00e9ll\u00f3", +) + + +def test_01_unpwd_unicode_set(server_socket, start_client): + start_client("01-unpwd-unicode-set.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_01_will_set.py b/phaoUtils/tests/lib/test_01_will_set.py new file mode 100644 index 0000000..55b55a2 --- /dev/null +++ b/phaoUtils/tests/lib/test_01_will_set.py @@ -0,0 +1,21 @@ +# Test whether a client produces a correct connect with a will. +# Will QoS=1, will retain=1. + +# The client should connect with keepalive=60, clean session set, +# client id 01-will-set will topic set to topic/on/unexpected/disconnect , will +# payload set to "will message", will qos set to 1 and will retain set. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect( + "01-will-set", keepalive=60, will_topic="topic/on/unexpected/disconnect", + will_qos=1, will_retain=True, will_payload="will message") + + +def test_01_will_set(server_socket, start_client): + start_client("01-will-set.py") + (conn, address) = server_socket.accept() + conn.settimeout(10) + paho_test.expect_packet(conn, "connect", connect_packet) + conn.close() diff --git a/phaoUtils/tests/lib/test_01_will_unpwd_set.py b/phaoUtils/tests/lib/test_01_will_unpwd_set.py new file mode 100644 index 0000000..95c0517 --- /dev/null +++ b/phaoUtils/tests/lib/test_01_will_unpwd_set.py @@ -0,0 +1,26 @@ +# Test whether a client produces a correct connect with a will, username and password. + +# The client should connect with keepalive=60, clean session set, +# client id 01-will-unpwd-set , will topic set to "will-topic", will payload +# set to "will message", will qos=2, will retain not set, username set to +# "oibvvwqw" and password set to "#'^2hg9a&nm38*us". + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect( + "01-will-unpwd-set", + keepalive=60, username="oibvvwqw", password="#'^2hg9a&nm38*us", + will_topic="will-topic", will_qos=2, will_payload="will message", +) + + +def test_01_will_unpwd_set(server_socket, start_client): + start_client("01-will-unpwd-set.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_01_zero_length_clientid.py b/phaoUtils/tests/lib/test_01_zero_length_clientid.py new file mode 100644 index 0000000..f993408 --- /dev/null +++ b/phaoUtils/tests/lib/test_01_zero_length_clientid.py @@ -0,0 +1,23 @@ +# Test whether a client connects correctly with a zero length clientid. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("", keepalive=60, proto_ver=4) +connack_packet = paho_test.gen_connack(rc=0) + +disconnect_packet = paho_test.gen_disconnect() + + +def test_01_zero_length_clientid(server_socket, start_client): + start_client("01-zero-length-clientid.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_02_subscribe_qos0.py b/phaoUtils/tests/lib/test_02_subscribe_qos0.py new file mode 100644 index 0000000..d1ff422 --- /dev/null +++ b/phaoUtils/tests/lib/test_02_subscribe_qos0.py @@ -0,0 +1,40 @@ +# Test whether a client sends a correct SUBSCRIBE to a topic with QoS 0. + +# The client should connect with keepalive=60, clean session set, +# and client id subscribe-qos0-test +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a SUBSCRIBE +# message to subscribe to topic "qos0/test" with QoS=0. If rc!=0, the client +# should exit with an error. +# Upon receiving the correct SUBSCRIBE message, the test will reply with a +# SUBACK message with the accepted QoS set to 0. On receiving the SUBACK +# message, the client should send a DISCONNECT message. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("subscribe-qos0-test", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +disconnect_packet = paho_test.gen_disconnect() + +mid = 1 +subscribe_packet = paho_test.gen_subscribe(mid, "qos0/test", 0) +suback_packet = paho_test.gen_suback(mid, 0) + + +def test_02_subscribe_qos0(server_socket, start_client): + start_client("02-subscribe-qos0.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "subscribe", subscribe_packet) + conn.send(suback_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_02_subscribe_qos1.py b/phaoUtils/tests/lib/test_02_subscribe_qos1.py new file mode 100644 index 0000000..1e96d49 --- /dev/null +++ b/phaoUtils/tests/lib/test_02_subscribe_qos1.py @@ -0,0 +1,40 @@ +# Test whether a client sends a correct SUBSCRIBE to a topic with QoS 1. + +# The client should connect with keepalive=60, clean session set, +# and client id subscribe-qos1-test +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a SUBSCRIBE +# message to subscribe to topic "qos1/test" with QoS=1. If rc!=0, the client +# should exit with an error. +# Upon receiving the correct SUBSCRIBE message, the test will reply with a +# SUBACK message with the accepted QoS set to 1. On receiving the SUBACK +# message, the client should send a DISCONNECT message. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("subscribe-qos1-test", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +disconnect_packet = paho_test.gen_disconnect() + +mid = 1 +subscribe_packet = paho_test.gen_subscribe(mid, "qos1/test", 1) +suback_packet = paho_test.gen_suback(mid, 1) + + +def test_02_subscribe_qos1(server_socket, start_client): + start_client("02-subscribe-qos1.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "subscribe", subscribe_packet) + conn.send(suback_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_02_subscribe_qos2.py b/phaoUtils/tests/lib/test_02_subscribe_qos2.py new file mode 100644 index 0000000..5d04aff --- /dev/null +++ b/phaoUtils/tests/lib/test_02_subscribe_qos2.py @@ -0,0 +1,40 @@ +# Test whether a client sends a correct SUBSCRIBE to a topic with QoS 2. + +# The client should connect with keepalive=60, clean session set, +# and client id subscribe-qos2-test +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a SUBSCRIBE +# message to subscribe to topic "qos2/test" with QoS=2. If rc!=0, the client +# should exit with an error. +# Upon receiving the correct SUBSCRIBE message, the test will reply with a +# SUBACK message with the accepted QoS set to 2. On receiving the SUBACK +# message, the client should send a DISCONNECT message. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("subscribe-qos2-test", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +disconnect_packet = paho_test.gen_disconnect() + +mid = 1 +subscribe_packet = paho_test.gen_subscribe(mid, "qos2/test", 2) +suback_packet = paho_test.gen_suback(mid, 2) + + +def test_02_subscribe_qos2(server_socket, start_client): + start_client("02-subscribe-qos2.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "subscribe", subscribe_packet) + conn.send(suback_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_02_unsubscribe.py b/phaoUtils/tests/lib/test_02_unsubscribe.py new file mode 100644 index 0000000..92346b6 --- /dev/null +++ b/phaoUtils/tests/lib/test_02_unsubscribe.py @@ -0,0 +1,30 @@ +# Test whether a client sends a correct UNSUBSCRIBE packet. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("unsubscribe-test", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +disconnect_packet = paho_test.gen_disconnect() + +mid = 1 +unsubscribe_packet = paho_test.gen_unsubscribe(mid, "unsubscribe/test") +unsuback_packet = paho_test.gen_unsuback(mid) + + +def test_02_unsubscribe(server_socket, start_client): + start_client("02-unsubscribe.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "unsubscribe", unsubscribe_packet) + conn.send(unsuback_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_03_publish_b2c_qos1.py b/phaoUtils/tests/lib/test_03_publish_b2c_qos1.py new file mode 100644 index 0000000..32578dd --- /dev/null +++ b/phaoUtils/tests/lib/test_03_publish_b2c_qos1.py @@ -0,0 +1,36 @@ +# Test whether a client responds correctly to a PUBLISH with QoS 1. + +# The client should connect with keepalive=60, clean session set, +# and client id publish-qos1-test +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK the client should verify that rc==0. +# The test will send the client a PUBLISH message with topic +# "pub/qos1/receive", payload of "message", QoS=1 and mid=123. The client +# should handle this as per the spec by sending a PUBACK message. +# The client should then exit with return code==0. +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("publish-qos1-test", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +disconnect_packet = paho_test.gen_disconnect() + +mid = 123 +publish_packet = paho_test.gen_publish( + "pub/qos1/receive", qos=1, mid=mid, payload="message") +puback_packet = paho_test.gen_puback(mid) + + +def test_03_publish_b2c_qos1(server_socket, start_client): + start_client("03-publish-b2c-qos1.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + conn.send(publish_packet) + + paho_test.expect_packet(conn, "puback", puback_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_03_publish_b2c_qos2.py b/phaoUtils/tests/lib/test_03_publish_b2c_qos2.py new file mode 100644 index 0000000..781938f --- /dev/null +++ b/phaoUtils/tests/lib/test_03_publish_b2c_qos2.py @@ -0,0 +1,41 @@ +# Test whether a client responds correctly to a PUBLISH with QoS 1. + +# The client should connect with keepalive=60, clean session set, +# and client id publish-qos1-test +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK the client should verify that rc==0. +# The test will send the client a PUBLISH message with topic +# "pub/qos1/receive", payload of "message", QoS=1 and mid=123. The client +# should handle this as per the spec by sending a PUBACK message. +# The client should then exit with return code==0. +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("publish-qos2-test", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +disconnect_packet = paho_test.gen_disconnect() + +mid = 13423 +publish_packet = paho_test.gen_publish( + "pub/qos2/receive", qos=2, mid=mid, payload="message") +pubrec_packet = paho_test.gen_pubrec(mid=mid) +pubrel_packet = paho_test.gen_pubrel(mid=mid) +pubcomp_packet = paho_test.gen_pubcomp(mid) + + +def test_03_publish_b2c_qos2(server_socket, start_client): + start_client("03-publish-b2c-qos2.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + conn.send(publish_packet) + + paho_test.expect_packet(conn, "pubrec", pubrec_packet) + conn.send(pubrel_packet) + + paho_test.expect_packet(conn, "pubcomp", pubcomp_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_03_publish_c2b_qos1_disconnect.py b/phaoUtils/tests/lib/test_03_publish_c2b_qos1_disconnect.py new file mode 100644 index 0000000..10daca6 --- /dev/null +++ b/phaoUtils/tests/lib/test_03_publish_c2b_qos1_disconnect.py @@ -0,0 +1,45 @@ +# Test whether a client sends a correct PUBLISH to a topic with QoS 1, then responds correctly to a disconnect. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect( + "publish-qos1-test", keepalive=60, clean_session=False, +) +connack_packet = paho_test.gen_connack(rc=0) + +disconnect_packet = paho_test.gen_disconnect() + +mid = 1 +publish_packet = paho_test.gen_publish( + "pub/qos1/test", qos=1, mid=mid, payload="message") +publish_packet_dup = paho_test.gen_publish( + "pub/qos1/test", qos=1, mid=mid, payload="message", dup=True) +puback_packet = paho_test.gen_puback(mid) + + +def test_03_publish_c2b_qos1_disconnect(server_socket, start_client): + start_client("03-publish-c2b-qos1-disconnect.py") + + (conn, address) = server_socket.accept() + conn.settimeout(15) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "publish", publish_packet) + # Disconnect client. It should reconnect. + conn.close() + + (conn, address) = server_socket.accept() + conn.settimeout(15) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "retried publish", publish_packet_dup) + conn.send(puback_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_03_publish_c2b_qos2_disconnect.py b/phaoUtils/tests/lib/test_03_publish_c2b_qos2_disconnect.py new file mode 100644 index 0000000..15b1d49 --- /dev/null +++ b/phaoUtils/tests/lib/test_03_publish_c2b_qos2_disconnect.py @@ -0,0 +1,61 @@ +# Test whether a client sends a correct PUBLISH to a topic with QoS 2 and responds to a disconnect. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect( + "publish-qos2-test", keepalive=60, clean_session=False, +) +connack_packet = paho_test.gen_connack(rc=0) + +disconnect_packet = paho_test.gen_disconnect() + +mid = 1 +publish_packet = paho_test.gen_publish( + "pub/qos2/test", qos=2, mid=mid, payload="message") +publish_dup_packet = paho_test.gen_publish( + "pub/qos2/test", qos=2, mid=mid, payload="message", dup=True) +pubrec_packet = paho_test.gen_pubrec(mid) +pubrel_packet = paho_test.gen_pubrel(mid) +pubcomp_packet = paho_test.gen_pubcomp(mid) + + +def test_03_publish_c2b_qos2_disconnect(server_socket, start_client): + start_client("03-publish-c2b-qos2-disconnect.py") + + (conn, address) = server_socket.accept() + conn.settimeout(5) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "publish", publish_packet) + # Disconnect client. It should reconnect. + conn.close() + + (conn, address) = server_socket.accept() + conn.settimeout(15) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "retried publish", publish_dup_packet) + conn.send(pubrec_packet) + + paho_test.expect_packet(conn, "pubrel", pubrel_packet) + # Disconnect client. It should reconnect. + conn.close() + + (conn, address) = server_socket.accept() + conn.settimeout(15) + + # Complete connection and message flow. + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "retried pubrel", pubrel_packet) + conn.send(pubcomp_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_03_publish_fill_inflight.py b/phaoUtils/tests/lib/test_03_publish_fill_inflight.py new file mode 100644 index 0000000..697c896 --- /dev/null +++ b/phaoUtils/tests/lib/test_03_publish_fill_inflight.py @@ -0,0 +1,90 @@ +# Test whether a client responds to max-inflight and reconnect when max-inflight is reached + +# The client should connect with keepalive=60, clean session set, +# and client id publish-fill-inflight +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK the client should verify that rc==0. +# Then client should send 10 PUBLISH with QoS == 1. On client side 12 message will be +# submitted, so 2 will be queued. +# The test will wait 0.5 seconds after received the 10 PUBLISH. After this wait, it will +# disconnect the client. +# The client should re-connect and re-sent the first 10 messages. +# The test will PUBACK one message, it should receive another PUBLISH. +# The test will wait 0.5 seconds and expect no PUBLISH. +# The test will then PUBACK all message. +# The client should disconnect once everything is acked. + +import pytest + +import tests.paho_test as paho_test + + +def expected_payload(i: int) -> bytes: + return f"message{i}" + +connect_packet = paho_test.gen_connect("publish-qos1-test", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +disconnect_packet = paho_test.gen_disconnect() + +first_connection_publishs = [ + paho_test.gen_publish( + "topic", qos=1, mid=i+1, payload=expected_payload(i), + ) + for i in range(10) +] +second_connection_publishs = [ + paho_test.gen_publish( + # I'm not sure we should have the mid+13. + # Currently on reconnection client will do two wrong thing: + # * it sent more than max_inflight packet + # * it re-send message both with mid = old_mid + 12 AND with mid = old_mid & dup=1 + "topic", qos=1, mid=i+13, payload=expected_payload(i), + ) + for i in range(12) +] +second_connection_pubacks = [ + paho_test.gen_puback(i+13) + for i in range(12) +] + +@pytest.mark.xfail +def test_03_publish_fill_inflight(server_socket, start_client): + start_client("03-publish-fill-inflight.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + for packet in first_connection_publishs: + paho_test.expect_packet(conn, "publish", packet) + + paho_test.expect_no_packet(conn, 0.5) + + conn.close() + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + for packet in second_connection_publishs[:10]: + paho_test.expect_packet(conn, "publish", packet) + + paho_test.expect_no_packet(conn, 0.2) + + conn.send(second_connection_pubacks[0]) + paho_test.expect_packet(conn, "publish", second_connection_publishs[10]) + + paho_test.expect_no_packet(conn, 0.5) + + for packet in second_connection_pubacks[1:11]: + conn.send(packet) + + paho_test.expect_packet(conn, "publish", second_connection_publishs[11]) + + paho_test.expect_no_packet(conn, 0.5) + diff --git a/phaoUtils/tests/lib/test_03_publish_helper_qos0.py b/phaoUtils/tests/lib/test_03_publish_helper_qos0.py new file mode 100644 index 0000000..b1c57d9 --- /dev/null +++ b/phaoUtils/tests/lib/test_03_publish_helper_qos0.py @@ -0,0 +1,40 @@ +# Test whether a client sends a correct PUBLISH to a topic with QoS 0. +# Use paho.mqtt.publish helper for that. + +# The client should connect with keepalive=60, clean session set, +# and client id publish-helper-qos0-test +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a PUBLISH message +# to topic "pub/qos0/test" with payload "message" and QoS=0. If rc!=0, the +# client should exit with an error. +# After sending the PUBLISH message, the client should send a +# DISCONNECT message. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect( + "publish-helper-qos0-test", keepalive=60, +) +connack_packet = paho_test.gen_connack(rc=0) + +publish_packet = paho_test.gen_publish( + "pub/qos0/test", qos=0, payload="message" +) + +disconnect_packet = paho_test.gen_disconnect() + + +def test_03_publish_helper_qos0(server_socket, start_client): + start_client("03-publish-helper-qos0.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "publish", publish_packet) + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_03_publish_helper_qos0_v5.py b/phaoUtils/tests/lib/test_03_publish_helper_qos0_v5.py new file mode 100644 index 0000000..ab95077 --- /dev/null +++ b/phaoUtils/tests/lib/test_03_publish_helper_qos0_v5.py @@ -0,0 +1,40 @@ +# Test whether a client sends a correct PUBLISH to a topic with QoS 0. +# Use paho.mqtt.publish helper for that. + +# The client should connect with keepalive=60, clean session set, +# and client id publish-helper-qos0-test +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a PUBLISH message +# to topic "pub/qos0/test" with payload "message" and QoS=0. If rc!=0, the +# client should exit with an error. +# After sending the PUBLISH message, the client should send a +# DISCONNECT message. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect( + "publish-helper-qos0-test", keepalive=60, proto_ver=5, properties=None +) +connack_packet = paho_test.gen_connack(rc=0, proto_ver=5) + +publish_packet = paho_test.gen_publish( + "pub/qos0/test", qos=0, payload="message", proto_ver=5 +) + +disconnect_packet = paho_test.gen_disconnect() + + +def test_03_publish_helper_qos0_v5(server_socket, start_client): + start_client("03-publish-helper-qos0-v5.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "publish", publish_packet) + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_03_publish_helper_qos1_disconnect.py b/phaoUtils/tests/lib/test_03_publish_helper_qos1_disconnect.py new file mode 100644 index 0000000..f73462c --- /dev/null +++ b/phaoUtils/tests/lib/test_03_publish_helper_qos1_disconnect.py @@ -0,0 +1,50 @@ +# Test whether a client sends a correct PUBLISH to a topic with QoS 1, +# then responds correctly to a disconnect. +# Use paho.mqtt.publish helper for that. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect( + "publish-helper-qos1-disconnect-test", keepalive=60, +) +connack_packet = paho_test.gen_connack(rc=0) + +mid = 1 +publish_packet = paho_test.gen_publish( + "pub/qos1/test", qos=1, mid=mid, payload="message" +) +publish_packet_dup = paho_test.gen_publish( + "pub/qos1/test", qos=1, mid=mid, payload="message", + dup=True, +) +puback_packet = paho_test.gen_puback(mid) + +disconnect_packet = paho_test.gen_disconnect() + + +def test_03_publish_helper_qos1_disconnect(server_socket, start_client): + start_client("03-publish-helper-qos1-disconnect.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "publish", publish_packet) + # Disconnect client. It should reconnect. + conn.close() + + (conn, address) = server_socket.accept() + conn.settimeout(15) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "retried publish", publish_packet_dup) + conn.send(puback_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_03_publish_qos0.py b/phaoUtils/tests/lib/test_03_publish_qos0.py new file mode 100644 index 0000000..82f312e --- /dev/null +++ b/phaoUtils/tests/lib/test_03_publish_qos0.py @@ -0,0 +1,33 @@ +# Test whether a client sends a correct PUBLISH to a topic with QoS 0. + +# The client should connect with keepalive=60, clean session set, +# and client id publish-qos0-test +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a PUBLISH message +# to topic "pub/qos0/test" with payload "message" and QoS=0. If rc!=0, the +# client should exit with an error. +# After sending the PUBLISH message, the client should send a DISCONNECT message. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("publish-qos0-test", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +publish_packet = paho_test.gen_publish("pub/qos0/test", qos=0, payload="message") + +disconnect_packet = paho_test.gen_disconnect() + + +def test_03_publish_qos0(server_socket, start_client): + start_client("03-publish-qos0.py") + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "publish", publish_packet) + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_03_publish_qos0_no_payload.py b/phaoUtils/tests/lib/test_03_publish_qos0_no_payload.py new file mode 100644 index 0000000..69b9aef --- /dev/null +++ b/phaoUtils/tests/lib/test_03_publish_qos0_no_payload.py @@ -0,0 +1,34 @@ +# Test whether a client sends a correct PUBLISH to a topic with QoS 0 and no payload. + +# The client should connect with keepalive=60, clean session set, +# and client id publish-qos0-test-np +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a PUBLISH message +# to topic "pub/qos0/no-payload/test" with zero length payload and QoS=0. If +# rc!=0, the client should exit with an error. +# After sending the PUBLISH message, the client should send a DISCONNECT message. + + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("publish-qos0-test-np", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +publish_packet = paho_test.gen_publish("pub/qos0/no-payload/test", qos=0) + +disconnect_packet = paho_test.gen_disconnect() + + +def test_03_publish_qos0_no_payload(server_socket, start_client): + start_client("03-publish-qos0-no-payload.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "publish", publish_packet) + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_04_retain_qos0.py b/phaoUtils/tests/lib/test_04_retain_qos0.py new file mode 100644 index 0000000..dee6b09 --- /dev/null +++ b/phaoUtils/tests/lib/test_04_retain_qos0.py @@ -0,0 +1,25 @@ +# Test whether a client sends a correct retained PUBLISH to a topic with QoS 0. + + +import tests.paho_test as paho_test + +mid = 16 +connect_packet = paho_test.gen_connect("retain-qos0-test", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) + +publish_packet = paho_test.gen_publish( + "retain/qos0/test", qos=0, payload="retained message", retain=True) + + +def test_04_retain_qos0(server_socket, start_client): + start_client("04-retain-qos0.py") + + (conn, address) = server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "publish", publish_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_08_ssl_bad_cacert.py b/phaoUtils/tests/lib/test_08_ssl_bad_cacert.py new file mode 100644 index 0000000..0031a2f --- /dev/null +++ b/phaoUtils/tests/lib/test_08_ssl_bad_cacert.py @@ -0,0 +1,8 @@ +import paho.mqtt.client as mqtt +import pytest + + +def test_08_ssl_bad_cacert(): + with pytest.raises(IOError): + mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "08-ssl-bad-cacert") + mqttc.tls_set("this/file/doesnt/exist") diff --git a/phaoUtils/tests/lib/test_08_ssl_connect_alpn.py b/phaoUtils/tests/lib/test_08_ssl_connect_alpn.py new file mode 100644 index 0000000..af1ecc4 --- /dev/null +++ b/phaoUtils/tests/lib/test_08_ssl_connect_alpn.py @@ -0,0 +1,38 @@ +# Test whether a client produces a correct connect and subsequent disconnect when using SSL. +# Client must provide a certificate. +# +# The client should connect with keepalive=60, clean session set, +# and client id 08-ssl-connect-alpn +# It should use the CA certificate ssl/all-ca.crt for verifying the server. +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a DISCONNECT +# message. If rc!=0, the client should exit with an error. +# +# Additionally, the secure socket must have been negotiated with the "paho-test-protocol" + + +from tests import paho_test +from tests.paho_test import ssl + + +def test_08_ssl_connect_alpn(alpn_ssl_server_socket, start_client): + connect_packet = paho_test.gen_connect("08-ssl-connect-alpn", keepalive=60) + connack_packet = paho_test.gen_connack(rc=0) + disconnect_packet = paho_test.gen_disconnect() + + start_client("08-ssl-connect-alpn.py") + + (conn, address) = alpn_ssl_server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + if ssl.HAS_ALPN: + negotiated_protocol = conn.selected_alpn_protocol() + if negotiated_protocol != "paho-test-protocol": + raise Exception(f"Unexpected protocol '{negotiated_protocol}'") + + conn.close() diff --git a/phaoUtils/tests/lib/test_08_ssl_connect_cert_auth.py b/phaoUtils/tests/lib/test_08_ssl_connect_cert_auth.py new file mode 100644 index 0000000..630773a --- /dev/null +++ b/phaoUtils/tests/lib/test_08_ssl_connect_cert_auth.py @@ -0,0 +1,29 @@ +# Test whether a client produces a correct connect and subsequent disconnect when using SSL. +# Client must provide a certificate. +# +# The client should connect with keepalive=60, clean session set, +# and client id 08-ssl-connect-crt-auth +# It should use the CA certificate ssl/all-ca.crt for verifying the server. +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a DISCONNECT +# message. If rc!=0, the client should exit with an error. + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("08-ssl-connect-crt-auth", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) +disconnect_packet = paho_test.gen_disconnect() + + +def test_08_ssl_connect_crt_auth(ssl_server_socket, start_client): + start_client("08-ssl-connect-cert-auth.py") + + (conn, address) = ssl_server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_08_ssl_connect_cert_auth_pw.py b/phaoUtils/tests/lib/test_08_ssl_connect_cert_auth_pw.py new file mode 100644 index 0000000..333d6f7 --- /dev/null +++ b/phaoUtils/tests/lib/test_08_ssl_connect_cert_auth_pw.py @@ -0,0 +1,29 @@ +# Test whether a client produces a correct connect and subsequent disconnect when using SSL. +# Client must provide a certificate - the private key is encrypted with a password. +# +# The client should connect with keepalive=60, clean session set, +# and client id 08-ssl-connect-crt-auth +# It should use the CA certificate ssl/all-ca.crt for verifying the server. +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a DISCONNECT +# message. If rc!=0, the client should exit with an error. + +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("08-ssl-connect-crt-auth-pw", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) +disconnect_packet = paho_test.gen_disconnect() + + +def test_08_ssl_connect_crt_auth_pw(ssl_server_socket, start_client): + start_client("08-ssl-connect-cert-auth-pw.py") + + (conn, address) = ssl_server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_08_ssl_connect_no_auth.py b/phaoUtils/tests/lib/test_08_ssl_connect_no_auth.py new file mode 100644 index 0000000..d284658 --- /dev/null +++ b/phaoUtils/tests/lib/test_08_ssl_connect_no_auth.py @@ -0,0 +1,26 @@ +# Test whether a client produces a correct connect and subsequent disconnect when using SSL. +# +# The client should connect with keepalive=60, clean session set,# and client id 08-ssl-connect-no-auth +# It should use the CA certificate ssl/all-ca.crt for verifying the server. +# The test will send a CONNACK message to the client with rc=0. Upon receiving +# the CONNACK and verifying that rc=0, the client should send a DISCONNECT +# message. If rc!=0, the client should exit with an error. +import tests.paho_test as paho_test + +connect_packet = paho_test.gen_connect("08-ssl-connect-no-auth", keepalive=60) +connack_packet = paho_test.gen_connack(rc=0) +disconnect_packet = paho_test.gen_disconnect() + + +def test_08_ssl_connect_no_auth(ssl_server_socket, start_client): + start_client("08-ssl-connect-no-auth.py") + + (conn, address) = ssl_server_socket.accept() + conn.settimeout(10) + + paho_test.expect_packet(conn, "connect", connect_packet) + conn.send(connack_packet) + + paho_test.expect_packet(conn, "disconnect", disconnect_packet) + + conn.close() diff --git a/phaoUtils/tests/lib/test_08_ssl_fake_cacert.py b/phaoUtils/tests/lib/test_08_ssl_fake_cacert.py new file mode 100644 index 0000000..09b0e3c --- /dev/null +++ b/phaoUtils/tests/lib/test_08_ssl_fake_cacert.py @@ -0,0 +1,10 @@ +import pytest + +from tests.paho_test import ssl + + +def test_08_ssl_fake_cacert(ssl_server_socket, start_client): + start_client("08-ssl-fake-cacert.py") + with pytest.raises(ssl.SSLError): + (conn, address) = ssl_server_socket.accept() + conn.close() diff --git a/phaoUtils/tests/mqtt5_props.py b/phaoUtils/tests/mqtt5_props.py new file mode 100644 index 0000000..f9be0d6 --- /dev/null +++ b/phaoUtils/tests/mqtt5_props.py @@ -0,0 +1,76 @@ +import struct + +PROP_PAYLOAD_FORMAT_INDICATOR = 1 +PROP_MESSAGE_EXPIRY_INTERVAL = 2 +PROP_CONTENT_TYPE = 3 +PROP_RESPONSE_TOPIC = 8 +PROP_CORRELATION_DATA = 9 +PROP_SUBSCRIPTION_IDENTIFIER = 11 +PROP_SESSION_EXPIRY_INTERVAL = 17 +PROP_ASSIGNED_CLIENT_IDENTIFIER = 18 +PROP_SERVER_KEEP_ALIVE = 19 +PROP_AUTHENTICATION_METHOD = 21 +PROP_AUTHENTICATION_DATA = 22 +PROP_REQUEST_PROBLEM_INFO = 23 +PROP_WILL_DELAY_INTERVAL = 24 +PROP_REQUEST_RESPONSE_INFO = 25 +PROP_RESPONSE_INFO = 26 +PROP_SERVER_REFERENCE = 28 +PROP_REASON_STRING = 31 +PROP_RECEIVE_MAXIMUM = 33 +PROP_TOPIC_ALIAS_MAXIMUM = 34 +PROP_TOPIC_ALIAS = 35 +PROP_MAXIMUM_QOS = 36 +PROP_RETAIN_AVAILABLE = 37 +PROP_USER_PROPERTY = 38 +PROP_MAXIMUM_PACKET_SIZE = 39 +PROP_WILDCARD_SUB_AVAILABLE = 40 +PROP_SUBSCRIPTION_ID_AVAILABLE = 41 +PROP_SHARED_SUB_AVAILABLE = 42 + +def gen_byte_prop(identifier, byte): + prop = struct.pack('BB', identifier, byte) + return prop + +def gen_uint16_prop(identifier, word): + prop = struct.pack('!BH', identifier, word) + return prop + +def gen_uint32_prop(identifier, word): + prop = struct.pack('!BI', identifier, word) + return prop + +def gen_string_prop(identifier, s): + s = s.encode("utf-8") + prop = struct.pack(f'!BH{len(s)}s', identifier, len(s), s) + return prop + +def gen_string_pair_prop(identifier, s1, s2): + s1 = s1.encode("utf-8") + s2 = s2.encode("utf-8") + prop = struct.pack(f'!BH{len(s1)}sH{len(s2)}s', identifier, len(s1), s1, len(s2), s2) + return prop + +def gen_varint_prop(identifier, val): + v = pack_varint(val) + return struct.pack(f"!B{len(v)}s", identifier, v) + +def pack_varint(varint): + s = b"" + while True: + byte = varint % 128 + varint = varint // 128 + # If there are more digits to encode, set the top bit of this digit + if varint > 0: + byte = byte | 0x80 + + s = s + struct.pack("!B", byte) + if varint == 0: + return s + +def prop_finalise(props): + if props is None: + return pack_varint(0) + else: + return pack_varint(len(props)) + props + diff --git a/phaoUtils/tests/paho_test.py b/phaoUtils/tests/paho_test.py new file mode 100644 index 0000000..40e950a --- /dev/null +++ b/phaoUtils/tests/paho_test.py @@ -0,0 +1,444 @@ +import contextlib +import os +import socket +import struct +import time + +from tests.consts import ssl_path +from tests.debug_helpers import dump_packet + +try: + import ssl +except ImportError: + ssl = None + +from tests import mqtt5_props + + +def bind_to_any_free_port(sock) -> int: + """ + Bind a socket to an available port on localhost, + and return the port number. + """ + sock.bind(('localhost', 0)) + return sock.getsockname()[1] + + +def create_server_socket(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(10) + port = bind_to_any_free_port(sock) + sock.listen(5) + return (sock, port) + + +def create_server_socket_ssl(*, verify_mode=None, alpn_protocols=None): + assert ssl, "SSL not available" + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_verify_locations(str(ssl_path / "all-ca.crt")) + context.load_cert_chain( + str(ssl_path / "server.crt"), + str(ssl_path / "server.key"), + ) + if verify_mode: + context.verify_mode = verify_mode + + if alpn_protocols is not None: + context.set_alpn_protocols(alpn_protocols) + + ssock = context.wrap_socket(sock, server_side=True) + ssock.settimeout(10) + port = bind_to_any_free_port(ssock) + ssock.listen(5) + return (ssock, port) + + +def expect_packet(sock, name, expected): + rlen = len(expected) if len(expected) > 0 else 1 + + packet_recvd = b"" + try: + while len(packet_recvd) < rlen: + data = sock.recv(rlen-len(packet_recvd)) + if len(data) == 0: + break + packet_recvd += data + except socket.timeout: # pragma: no cover + pass + + assert packet_matches(name, packet_recvd, expected) + return True + + +def expect_no_packet(sock, delay=1): + """ expect that nothing is received within given delay + """ + try: + previous_timeout = sock.gettimeout() + sock.settimeout(delay) + data = sock.recv(1024) + except socket.timeout: + data = None + finally: + sock.settimeout(previous_timeout) + + if data is not None: + dump_packet("Received unexpected", data) + + assert data is None, "shouldn't receive any data" + + +def packet_matches(name, recvd, expected): + if recvd != expected: # pragma: no cover + print(f"FAIL: Received incorrect {name}.") + dump_packet("Received", recvd) + dump_packet("Expected", expected) + return False + else: + return True + + +def gen_connect( + client_id, + clean_session=True, + keepalive=60, + username=None, + password=None, + will_topic=None, + will_qos=0, + will_retain=False, + will_payload=b"", + proto_ver=4, + connect_reserved=False, + properties=b"", + will_properties=b"", + session_expiry=-1, +): + if (proto_ver&0x7F) == 3 or proto_ver == 0: + remaining_length = 12 + elif (proto_ver&0x7F) == 4 or proto_ver == 5: + remaining_length = 10 + else: + raise ValueError + + if client_id is not None: + client_id = client_id.encode("utf-8") + remaining_length = remaining_length + 2+len(client_id) + else: + remaining_length = remaining_length + 2 + + connect_flags = 0 + + if connect_reserved: + connect_flags = connect_flags | 0x01 + + if clean_session: + connect_flags = connect_flags | 0x02 + + if proto_ver == 5: + if properties == b"": + properties += mqtt5_props.gen_uint16_prop(mqtt5_props.PROP_RECEIVE_MAXIMUM, 20) + + if session_expiry != -1: + properties += mqtt5_props.gen_uint32_prop(mqtt5_props.PROP_SESSION_EXPIRY_INTERVAL, session_expiry) + + properties = mqtt5_props.prop_finalise(properties) + remaining_length += len(properties) + + if will_topic is not None: + will_topic = will_topic.encode('utf-8') + remaining_length = remaining_length + 2 + len(will_topic) + 2 + len(will_payload) + connect_flags = connect_flags | 0x04 | ((will_qos & 0x03) << 3) + if will_retain: + connect_flags = connect_flags | 32 + if proto_ver == 5: + will_properties = mqtt5_props.prop_finalise(will_properties) + remaining_length += len(will_properties) + + if username is not None: + username = username.encode('utf-8') + remaining_length = remaining_length + 2 + len(username) + connect_flags = connect_flags | 0x80 + if password is not None: + password = password.encode('utf-8') + connect_flags = connect_flags | 0x40 + remaining_length = remaining_length + 2 + len(password) + + rl = pack_remaining_length(remaining_length) + packet = struct.pack("!B" + str(len(rl)) + "s", 0x10, rl) + if (proto_ver&0x7F) == 3 or proto_ver == 0: + packet = packet + struct.pack("!H6sBBH", len(b"MQIsdp"), b"MQIsdp", proto_ver, connect_flags, keepalive) + elif (proto_ver&0x7F) == 4 or proto_ver == 5: + packet = packet + struct.pack("!H4sBBH", len(b"MQTT"), b"MQTT", proto_ver, connect_flags, keepalive) + + if proto_ver == 5: + packet += properties + + if client_id is not None: + packet = packet + struct.pack("!H" + str(len(client_id)) + "s", len(client_id), bytes(client_id)) + else: + packet = packet + struct.pack("!H", 0) + + if will_topic is not None: + packet += will_properties + packet = packet + struct.pack("!H" + str(len(will_topic)) + "s", len(will_topic), will_topic) + if len(will_payload) > 0: + packet = packet + struct.pack("!H" + str(len(will_payload)) + "s", len(will_payload), will_payload.encode('utf8')) + else: + packet = packet + struct.pack("!H", 0) + + if username is not None: + packet = packet + struct.pack("!H" + str(len(username)) + "s", len(username), username) + if password is not None: + packet = packet + struct.pack("!H" + str(len(password)) + "s", len(password), password) + return packet + +def gen_connack(flags=0, rc=0, proto_ver=4, properties=b"", property_helper=True): + if proto_ver == 5: + if property_helper: + if properties is not None: + properties = mqtt5_props.gen_uint16_prop(mqtt5_props.PROP_TOPIC_ALIAS_MAXIMUM, 10) \ + + properties + mqtt5_props.gen_uint16_prop(mqtt5_props.PROP_RECEIVE_MAXIMUM, 20) + else: + properties = b"" + properties = mqtt5_props.prop_finalise(properties) + + packet = struct.pack('!BBBB', 32, 2+len(properties), flags, rc) + properties + else: + packet = struct.pack('!BBBB', 32, 2, flags, rc) + + return packet + +def gen_publish(topic, qos, payload=None, retain=False, dup=False, mid=0, proto_ver=4, properties=b""): + if isinstance(topic, str): + topic = topic.encode("utf-8") + rl = 2+len(topic) + pack_format = "H"+str(len(topic))+"s" + if qos > 0: + rl = rl + 2 + pack_format = pack_format + "H" + + if proto_ver == 5: + properties = mqtt5_props.prop_finalise(properties) + rl += len(properties) + # This will break if len(properties) > 127 + pack_format = pack_format + "%ds"%(len(properties)) + + if payload is not None: + if isinstance(payload, str): + payload = payload.encode("utf-8") + rl = rl + len(payload) + pack_format = pack_format + str(len(payload)) + "s" + else: + payload = b"" + pack_format = pack_format + "0s" + + rlpacked = pack_remaining_length(rl) + cmd = 48 | (qos << 1) + if retain: + cmd = cmd + 1 + if dup: + cmd = cmd + 8 + + if proto_ver == 5: + if qos > 0: + return struct.pack("!B" + str(len(rlpacked))+"s" + pack_format, cmd, rlpacked, len(topic), topic, mid, properties, payload) + else: + return struct.pack("!B" + str(len(rlpacked))+"s" + pack_format, cmd, rlpacked, len(topic), topic, properties, payload) + else: + if qos > 0: + return struct.pack("!B" + str(len(rlpacked))+"s" + pack_format, cmd, rlpacked, len(topic), topic, mid, payload) + else: + return struct.pack("!B" + str(len(rlpacked))+"s" + pack_format, cmd, rlpacked, len(topic), topic, payload) + +def _gen_command_with_mid(cmd, mid, proto_ver=4, reason_code=-1, properties=None): + if proto_ver == 5 and (reason_code != -1 or properties is not None): + if reason_code == -1: + reason_code = 0 + + if properties is None: + return struct.pack('!BBHB', cmd, 3, mid, reason_code) + elif properties == "": + return struct.pack('!BBHBB', cmd, 4, mid, reason_code, 0) + else: + properties = mqtt5_props.prop_finalise(properties) + pack_format = "!BBHB"+str(len(properties))+"s" + return struct.pack(pack_format, cmd, 2+1+len(properties), mid, reason_code, properties) + else: + return struct.pack('!BBH', cmd, 2, mid) + +def gen_puback(mid, proto_ver=4, reason_code=-1, properties=None): + return _gen_command_with_mid(64, mid, proto_ver, reason_code, properties) + +def gen_pubrec(mid, proto_ver=4, reason_code=-1, properties=None): + return _gen_command_with_mid(80, mid, proto_ver, reason_code, properties) + +def gen_pubrel(mid, dup=False, proto_ver=4, reason_code=-1, properties=None): + if dup: + cmd = 96+8+2 + else: + cmd = 96+2 + return _gen_command_with_mid(cmd, mid, proto_ver, reason_code, properties) + +def gen_pubcomp(mid, proto_ver=4, reason_code=-1, properties=None): + return _gen_command_with_mid(112, mid, proto_ver, reason_code, properties) + + +def gen_subscribe(mid, topic, qos, cmd=130, proto_ver=4, properties=b""): + topic = topic.encode("utf-8") + packet = struct.pack("!B", cmd) + if proto_ver == 5: + if properties == b"": + packet += pack_remaining_length(2+1+2+len(topic)+1) + pack_format = "!HBH"+str(len(topic))+"sB" + return packet + struct.pack(pack_format, mid, 0, len(topic), topic, qos) + else: + properties = mqtt5_props.prop_finalise(properties) + packet += pack_remaining_length(2+1+2+len(topic)+len(properties)) + pack_format = "!H"+str(len(properties))+"s"+"H"+str(len(topic))+"sB" + return packet + struct.pack(pack_format, mid, properties, len(topic), topic, qos) + else: + packet += pack_remaining_length(2+2+len(topic)+1) + pack_format = "!HH"+str(len(topic))+"sB" + return packet + struct.pack(pack_format, mid, len(topic), topic, qos) + + +def gen_suback(mid, qos, proto_ver=4): + if proto_ver == 5: + return struct.pack('!BBHBB', 144, 2+1+1, mid, 0, qos) + else: + return struct.pack('!BBHB', 144, 2+1, mid, qos) + +def gen_unsubscribe(mid, topic, cmd=162, proto_ver=4, properties=b""): + topic = topic.encode("utf-8") + if proto_ver == 5: + if properties == b"": + pack_format = "!BBHBH"+str(len(topic))+"s" + return struct.pack(pack_format, cmd, 2+2+len(topic)+1, mid, 0, len(topic), topic) + else: + properties = mqtt5_props.prop_finalise(properties) + packet = struct.pack("!B", cmd) + l = 2+2+len(topic)+1+len(properties) # noqa: E741 + packet += pack_remaining_length(l) + pack_format = "!HB"+str(len(properties))+"sH"+str(len(topic))+"s" + packet += struct.pack(pack_format, mid, len(properties), properties, len(topic), topic) + return packet + else: + pack_format = "!BBHH"+str(len(topic))+"s" + return struct.pack(pack_format, cmd, 2+2+len(topic), mid, len(topic), topic) + +def gen_unsubscribe_multiple(mid, topics, proto_ver=4): + packet = b"" + remaining_length = 0 + for t in topics: + t = t.encode("utf-8") + remaining_length += 2+len(t) + packet += struct.pack("!H"+str(len(t))+"s", len(t), t) + + if proto_ver == 5: + remaining_length += 2+1 + + return struct.pack("!BBHB", 162, remaining_length, mid, 0) + packet + else: + remaining_length += 2 + + return struct.pack("!BBH", 162, remaining_length, mid) + packet + +def gen_unsuback(mid, reason_code=0, proto_ver=4): + if proto_ver == 5: + if isinstance(reason_code, list): + reason_code_count = len(reason_code) + p = struct.pack('!BBHB', 176, 3+reason_code_count, mid, 0) + for r in reason_code: + p += struct.pack('B', r) + return p + else: + return struct.pack('!BBHBB', 176, 4, mid, 0, reason_code) + else: + return struct.pack('!BBH', 176, 2, mid) + +def gen_pingreq(): + return struct.pack('!BB', 192, 0) + +def gen_pingresp(): + return struct.pack('!BB', 208, 0) + + +def _gen_short(cmd, reason_code=-1, proto_ver=5, properties=None): + if proto_ver == 5 and (reason_code != -1 or properties is not None): + if reason_code == -1: + reason_code = 0 + + if properties is None: + return struct.pack('!BBB', cmd, 1, reason_code) + elif properties == "": + return struct.pack('!BBBB', cmd, 2, reason_code, 0) + else: + properties = mqtt5_props.prop_finalise(properties) + return struct.pack("!BBB", cmd, 1+len(properties), reason_code) + properties + else: + return struct.pack('!BB', cmd, 0) + +def gen_disconnect(reason_code=-1, proto_ver=4, properties=None): + return _gen_short(0xE0, reason_code, proto_ver, properties) + +def gen_auth(reason_code=-1, properties=None): + return _gen_short(0xF0, reason_code, 5, properties) + + +def pack_remaining_length(remaining_length): + s = b"" + while True: + byte = remaining_length % 128 + remaining_length = remaining_length // 128 + # If there are more digits to encode, set the top bit of this digit + if remaining_length > 0: + byte = byte | 0x80 + + s = s + struct.pack("!B", byte) + if remaining_length == 0: + return s + + +def loop_until_keyboard_interrupt(mqttc): + """ + Call loop() in a loop until KeyboardInterrupt is received. + + This is used by the test clients in `lib/clients`; + the client spawner will send a SIGINT to the client process + when it wants the client to stop, so we should catch that + and stop the client gracefully. + """ + try: + while True: + mqttc.loop() + except KeyboardInterrupt: + pass + + +@contextlib.contextmanager +def wait_for_keyboard_interrupt(): + """ + Run the code in the context manager, then wait for a KeyboardInterrupt. + + This is used by the test clients in `lib/clients`; + the client spawner will send a SIGINT to the client process + when it wants the client to stop, so we should catch that + and stop the client gracefully. + """ + yield # If we get a KeyboardInterrupt during the block, it's too soon! + try: + while True: + time.sleep(0.1) + except KeyboardInterrupt: + pass + + +def get_test_server_port() -> int: + """ + Get the port number for the test server. + """ + return int(os.environ['PAHO_SERVER_PORT']) diff --git a/phaoUtils/tests/ssl/all-ca.crt b/phaoUtils/tests/ssl/all-ca.crt new file mode 100644 index 0000000..06b6593 --- /dev/null +++ b/phaoUtils/tests/ssl/all-ca.crt @@ -0,0 +1,101 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=GB, ST=Derbyshire, L=Derby, O=Paho Project, OU=Testing, CN=Root CA + Validity + Not Before: Jul 7 11:14:42 2021 GMT + Not After : Jul 6 11:14:42 2026 GMT + Subject: C=GB, ST=Derbyshire, O=Paho Project, OU=Testing, CN=Signing CA + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:cb:32:6c:8c:48:e8:44:58:36:18:70:36:42:3d: + 2d:29:47:3c:69:12:9e:7b:f7:45:62:ef:91:44:46: + 97:a0:ea:5f:da:fd:9f:98:d4:bf:43:02:e3:39:90: + 33:7b:13:13:d5:31:30:9c:07:fc:ca:1b:a9:e4:89: + 42:e5:d0:6e:f4:a2:e0:23:ee:9d:9a:cc:80:3b:78: + bf:7e:27:a8:46:1b:28:9f:4a:64:53:7a:89:3e:ab: + 65:6f:af:0b:29:fa:4d:4f:04:f1:1e:10:2c:bf:2b: + ea:fc:c5:fa:77:c9:1a:7a:78:29:f5:a2:cb:25:7c: + 02:bb:91:8d:76:4d:23:bc:9c:19:da:be:c5:20:04: + ad:fe:bd:b9:d4:bb:29:2a:c3:e4:fc:4c:84:db:a3: + 55:9f:f0:70:7f:40:38:b5:c3:78:a5:db:06:36:b7: + 10:8e:ca:6c:1a:92:66:be:0e:1a:97:59:6b:18:f4: + c2:b8:c9:31:7b:d1:b1:a1:00:78:7f:c0:09:f6:ef: + b2:8f:94:87:5d:b1:a2:23:93:4d:ec:fa:95:09:a9: + 90:c4:02:f0:1e:d9:ab:a2:8b:7f:7f:54:95:e7:da: + c3:c9:7d:a7:d7:04:89:59:db:88:9d:57:16:5d:b9: + 66:b0:d6:88:bb:e0:ee:43:e9:ab:02:78:fc:bd:e8: + 98:d9 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + C2:8F:09:9B:D5:F1:BA:C4:74:5E:97:50:BB:86:9D:A1:F1:FA:C4:36 + X509v3 Authority Key Identifier: + keyid:13:A0:B6:1F:F5:C7:64:C2:F9:FD:2E:08:F2:19:01:77:54:19:73:7F + + X509v3 Basic Constraints: + CA:TRUE + Signature Algorithm: sha256WithRSAEncryption + 3e:70:76:69:37:e4:6e:e0:08:c6:8e:5b:2e:aa:26:fe:e9:ed: + ac:02:ce:2c:37:08:6a:8a:c3:0d:c0:ef:43:51:01:2e:e0:96: + 76:23:1b:1f:75:98:df:7c:d1:b7:c1:67:aa:62:c1:bd:ef:84: + eb:d9:28:47:50:f2:1b:54:7f:ed:cb:52:f7:fc:c3:f8:62:22: + 0c:b3:95:ed:bb:3f:74:91:bc:d2:eb:c0:81:7d:74:12:85:61: + a3:7e:fb:22:4a:25:99:0b:5d:ef:69:f2:5a:e6:d5:12:a3:95: + 38:30:0c:c7:d9:da:28:30:10:b4:3d:3e:ad:20:85:31:e0:bf: + 30:33:2e:0b:e3:07:3d:ed:22:dc:67:f8:93:64:89:ed:e7:08: + 74:b5:0a:7a:01:3d:f9:44:62:71:cf:60:12:92:c3:95:9a:e5: + a5:f2:24:6a:22:64:d5:76:22:c9:03:1c:c5:d1:a5:85:4d:55: + f9:80:47:ca:12:20:df:05:fb:82:12:45:6f:e8:c0:20:a8:ae: + f7:17:c5:c3:b6:9c:51:bd:d8:84:e4:db:c7:03:44:d2:cb:75: + 51:79:3f:86:33:3c:e4:34:1d:77:b2:60:24:5c:21:c5:c3:53: + 36:08:2f:a7:14:0b:68:78:67:95:90:b9:06:0e:85:04:65:57: + b4:34:31:cf +-----BEGIN CERTIFICATE----- +MIIDmDCCAoCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQGEwJHQjET +MBEGA1UECAwKRGVyYnlzaGlyZTEOMAwGA1UEBwwFRGVyYnkxFTATBgNVBAoMDFBh +aG8gUHJvamVjdDEQMA4GA1UECwwHVGVzdGluZzEQMA4GA1UEAwwHUm9vdCBDQTAe +Fw0yMTA3MDcxMTE0NDJaFw0yNjA3MDYxMTE0NDJaMGAxCzAJBgNVBAYTAkdCMRMw +EQYDVQQIDApEZXJieXNoaXJlMRUwEwYDVQQKDAxQYWhvIFByb2plY3QxEDAOBgNV +BAsMB1Rlc3RpbmcxEzARBgNVBAMMClNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDLMmyMSOhEWDYYcDZCPS0pRzxpEp5790Vi75FERpeg +6l/a/Z+Y1L9DAuM5kDN7ExPVMTCcB/zKG6nkiULl0G70ouAj7p2azIA7eL9+J6hG +GyifSmRTeok+q2Vvrwsp+k1PBPEeECy/K+r8xfp3yRp6eCn1osslfAK7kY12TSO8 +nBnavsUgBK3+vbnUuykqw+T8TITbo1Wf8HB/QDi1w3il2wY2txCOymwakma+DhqX +WWsY9MK4yTF70bGhAHh/wAn277KPlIddsaIjk03s+pUJqZDEAvAe2auii39/VJXn +2sPJfafXBIlZ24idVxZduWaw1oi74O5D6asCePy96JjZAgMBAAGjUDBOMB0GA1Ud +DgQWBBTCjwmb1fG6xHRel1C7hp2h8frENjAfBgNVHSMEGDAWgBQToLYf9cdkwvn9 +LgjyGQF3VBlzfzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA+cHZp +N+Ru4AjGjlsuqib+6e2sAs4sNwhqisMNwO9DUQEu4JZ2IxsfdZjffNG3wWeqYsG9 +74Tr2ShHUPIbVH/ty1L3/MP4YiIMs5Xtuz90kbzS68CBfXQShWGjfvsiSiWZC13v +afJa5tUSo5U4MAzH2dooMBC0PT6tIIUx4L8wMy4L4wc97SLcZ/iTZInt5wh0tQp6 +AT35RGJxz2ASksOVmuWl8iRqImTVdiLJAxzF0aWFTVX5gEfKEiDfBfuCEkVv6MAg +qK73F8XDtpxRvdiE5NvHA0TSy3VReT+GMzzkNB13smAkXCHFw1M2CC+nFAtoeGeV +kLkGDoUEZVe0NDHP +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIUS1Q+E18/+trcKfhT+xz8ghGukmYwDQYJKoZIhvcNAQEL +BQAwbTELMAkGA1UEBhMCR0IxEzARBgNVBAgMCkRlcmJ5c2hpcmUxDjAMBgNVBAcM +BURlcmJ5MRUwEwYDVQQKDAxQYWhvIFByb2plY3QxEDAOBgNVBAsMB1Rlc3Rpbmcx +EDAOBgNVBAMMB1Jvb3QgQ0EwHhcNMjEwNzA3MTExNDQyWhcNMzEwNzA1MTExNDQy +WjBtMQswCQYDVQQGEwJHQjETMBEGA1UECAwKRGVyYnlzaGlyZTEOMAwGA1UEBwwF +RGVyYnkxFTATBgNVBAoMDFBhaG8gUHJvamVjdDEQMA4GA1UECwwHVGVzdGluZzEQ +MA4GA1UEAwwHUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKpCq45dCrroNa+y3zgdBglQOtw4og3MD/3Rn6ZftyL0dv1rSMkCFU8lCtZ4bIpz +iNSJKau79owCudX3qQTPfiX2pmR5uuYjvMzRiZohZtz5uqXByy/CMS8dPRI3po6i +kfNx9n7EQqOlxdwkY1kae2j5ybkAld2MNci93BH4P8qqaQckVRKpv6cKq33KsXK7 +jHgjAYMGrihTAwxgP1JX9NS8yxxjMUYvFqeEOLARoeWc6Nl7oDbGLs2fr0j2Yssm +cz0AMu7LWcbhnfs2S8Troksztnq38yHu+YTs6hX4NhANBgon5CAdyzmmE/b2OwOX +p8rQepUfG7wO5QaS0OrAEXsCAwEAAaNQME4wHQYDVR0OBBYEFBOgth/1x2TC+f0u +CPIZAXdUGXN/MB8GA1UdIwQYMBaAFBOgth/1x2TC+f0uCPIZAXdUGXN/MAwGA1Ud +EwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHgE1oMwIcilQFN4xPYCf8jbsa5o +zA5ljTbxv7fU3Zd+7KdlDFYroGjgHb7o3r0//b8+ZarxBqn1274u4KPs39Ow7h6m +YJo7IM2Z2fC6IWZroqeidfFx5SwejAP1j7coYLblTIbNF+P08sJG5nSQ+Yx0gams +6C1x0mETaaglDwllU1KXHTm8fUpEwpISc/VfKABYgScODMpdsDghyHANvnFjmvp4 +ktABnasliZYTmdl0t3szNm7zIk+bntiK4KunFea8GqgslWqGPwtNxxJFHzPjMCxK +EHgubLgp1lNZzH13XSO6ZpiNRDJ6IVed3Zq+yn+24uKH+1Hqp6Bt20ZFB4E= +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/client-expired.crt b/phaoUtils/tests/ssl/client-expired.crt new file mode 100644 index 0000000..435c94c --- /dev/null +++ b/phaoUtils/tests/ssl/client-expired.crt @@ -0,0 +1,82 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 3 (0x3) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=GB, ST=Derbyshire, O=Paho Project, OU=Testing, CN=Signing CA + Validity + Not Before: Aug 20 00:00:00 2012 GMT + Not After : Aug 21 00:00:00 2012 GMT + Subject: C=GB, ST=Nottinghamshire, L=Nottingham, O=Server, OU=Production, CN=test client expired + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:d0:58:ed:ad:44:b4:f8:30:16:27:d9:b4:2c:b4: + 24:67:9a:19:fe:32:04:0d:9a:7e:75:97:12:d6:2c: + d4:97:33:fb:30:8c:ef:a2:b1:ef:e5:92:d6:56:05: + 75:c8:19:82:92:4e:4b:13:8e:25:90:b9:21:72:f4: + a4:bf:7a:e2:0f:75:52:08:04:4c:e8:6a:35:7e:7d: + 78:d9:b8:f7:2b:3d:8e:4e:b5:f3:7a:9a:06:10:50: + ca:95:63:2c:bd:3a:89:d0:8a:84:12:32:9b:00:a7: + 25:33:70:d2:18:0a:43:94:12:62:e7:77:db:b8:0f: + dc:23:48:95:5c:77:c6:11:4f:0f:d6:6e:73:59:7c: + ed:6a:fd:ba:24:f0:b2:59:c3:a2:16:65:ad:19:7f: + 92:87:8c:ea:b5:e5:0f:26:f8:b1:74:98:c3:fd:ed: + 4d:74:d0:58:ce:d9:9c:24:34:9b:75:79:25:d0:aa: + 6c:03:03:0c:3a:4a:4c:9a:36:50:ab:55:74:1e:8b: + de:41:a7:14:b9:57:ee:8b:31:90:5c:00:af:31:9d: + e0:55:07:8d:05:ed:c9:5f:e1:79:b7:96:be:d9:5b: + cf:a7:5c:cd:48:fc:bd:a4:34:bf:e0:49:d5:25:60: + 7a:4c:32:37:97:e4:f8:64:24:a6:79:c1:62:8d:93: + 52:53 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 61:62:90:E6:BB:8A:BB:06:6C:8A:66:9F:A5:C7:85:12:43:5C:94:6F + X509v3 Authority Key Identifier: + keyid:C2:8F:09:9B:D5:F1:BA:C4:74:5E:97:50:BB:86:9D:A1:F1:FA:C4:36 + + Signature Algorithm: sha256WithRSAEncryption + 8d:7b:2a:16:2a:2e:50:db:5d:6e:20:ec:4e:5f:2e:d0:f4:9a: + a9:c8:b3:f0:73:02:9f:2f:32:a2:2a:a5:a7:83:1e:e3:36:6b: + 99:d2:4c:6a:ea:09:0b:73:5e:7f:69:da:50:69:5b:dc:0f:4d: + 59:ec:d2:c7:ca:0e:a8:55:c0:5a:f6:67:e8:a0:0b:4b:0a:9a: + a8:1f:b3:f0:e7:e6:10:4b:db:1b:5a:18:7a:ee:52:16:93:2e: + 70:1c:4f:7d:c6:eb:4a:11:35:92:db:8c:f0:86:1b:f7:64:4f: + f5:1b:31:d6:da:89:97:c6:46:4b:c9:df:7f:80:c4:77:5e:c6: + a8:b7:47:12:48:b5:2b:f2:73:80:e4:dd:5b:cf:a1:20:3c:3b: + b3:37:34:d1:72:37:e1:a6:06:d4:22:cc:65:d3:af:0f:aa:ea: + ad:dd:e9:21:c5:1e:86:81:94:33:6c:ca:68:c2:48:ed:ea:0e: + c4:be:38:a5:4f:bb:0b:2b:7f:e7:63:e1:9f:e1:c8:6a:c4:4c: + 7b:43:a2:56:c9:ff:56:88:2e:e3:4f:d6:d0:69:59:96:6e:26: + d9:3d:f3:62:4e:c3:a3:79:8f:f9:e4:82:11:52:f0:a2:c7:79: + b6:54:50:21:31:e6:4a:8c:2c:df:23:e9:2e:50:6e:9d:a8:61: + 5b:e1:cb:51 +-----BEGIN CERTIFICATE----- +MIID1zCCAr+gAwIBAgIBAzANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJHQjET +MBEGA1UECAwKRGVyYnlzaGlyZTEVMBMGA1UECgwMUGFobyBQcm9qZWN0MRAwDgYD +VQQLDAdUZXN0aW5nMRMwEQYDVQQDDApTaWduaW5nIENBMB4XDTEyMDgyMDAwMDAw +MFoXDTEyMDgyMTAwMDAwMFowgYAxCzAJBgNVBAYTAkdCMRgwFgYDVQQIDA9Ob3R0 +aW5naGFtc2hpcmUxEzARBgNVBAcMCk5vdHRpbmdoYW0xDzANBgNVBAoMBlNlcnZl +cjETMBEGA1UECwwKUHJvZHVjdGlvbjEcMBoGA1UEAwwTdGVzdCBjbGllbnQgZXhw +aXJlZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANBY7a1EtPgwFifZ +tCy0JGeaGf4yBA2afnWXEtYs1Jcz+zCM76Kx7+WS1lYFdcgZgpJOSxOOJZC5IXL0 +pL964g91UggETOhqNX59eNm49ys9jk6183qaBhBQypVjLL06idCKhBIymwCnJTNw +0hgKQ5QSYud327gP3CNIlVx3xhFPD9Zuc1l87Wr9uiTwslnDohZlrRl/koeM6rXl +Dyb4sXSYw/3tTXTQWM7ZnCQ0m3V5JdCqbAMDDDpKTJo2UKtVdB6L3kGnFLlX7osx +kFwArzGd4FUHjQXtyV/hebeWvtlbz6dczUj8vaQ0v+BJ1SVgekwyN5fk+GQkpnnB +Yo2TUlMCAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNT +TCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFGFikOa7irsGbIpmn6XH +hRJDXJRvMB8GA1UdIwQYMBaAFMKPCZvV8brEdF6XULuGnaHx+sQ2MA0GCSqGSIb3 +DQEBCwUAA4IBAQCNeyoWKi5Q211uIOxOXy7Q9JqpyLPwcwKfLzKiKqWngx7jNmuZ +0kxq6gkLc15/adpQaVvcD01Z7NLHyg6oVcBa9mfooAtLCpqoH7Pw5+YQS9sbWhh6 +7lIWky5wHE99xutKETWS24zwhhv3ZE/1GzHW2omXxkZLyd9/gMR3Xsaot0cSSLUr +8nOA5N1bz6EgPDuzNzTRcjfhpgbUIsxl068Pquqt3ekhxR6GgZQzbMpowkjt6g7E +vjilT7sLK3/nY+Gf4chqxEx7Q6JWyf9WiC7jT9bQaVmWbibZPfNiTsOjeY/55IIR +UvCix3m2VFAhMeZKjCzfI+kuUG6dqGFb4ctR +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/client-pw.crt b/phaoUtils/tests/ssl/client-pw.crt new file mode 100644 index 0000000..daf7b86 --- /dev/null +++ b/phaoUtils/tests/ssl/client-pw.crt @@ -0,0 +1,82 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 4 (0x4) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=GB, ST=Derbyshire, O=Paho Project, OU=Testing, CN=Signing CA + Validity + Not Before: Jul 7 11:14:42 2021 GMT + Not After : Jul 6 11:14:42 2026 GMT + Subject: C=GB, ST=Nottinghamshire, L=Nottingham, O=Server, OU=Production, CN=test client with password + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:ca:cb:93:49:45:28:95:5b:c4:51:b4:2b:d0:e7: + e4:b7:3b:37:34:f6:5c:ec:f8:7d:3a:8b:b8:da:3c: + 94:38:85:5f:41:ea:2b:08:d7:3e:97:12:50:09:1f: + 37:4f:e4:25:a1:59:b6:98:63:22:8d:80:7e:b1:b4: + 24:03:2e:5e:5d:45:a4:4c:76:e8:ac:2c:5f:ca:9d: + ed:6e:0a:7b:6f:2b:34:d1:4e:6a:e1:b6:72:66:42: + ec:fd:b8:97:bf:40:4b:24:9c:47:6c:8c:4a:73:aa: + e0:3a:db:ac:45:65:23:df:8f:4a:30:ed:d6:ad:5c: + eb:a9:e9:83:da:39:d1:eb:98:31:74:98:bd:99:6b: + 85:0e:1d:f8:93:cf:e2:bd:59:77:fe:b2:a0:c4:e5: + 63:ae:92:10:13:47:14:55:22:a0:30:b6:f0:cb:17: + b6:2d:f9:7d:f9:82:50:b2:64:88:dd:5a:3b:b6:81: + 67:8c:e3:de:89:76:63:82:af:b7:ba:83:5c:3b:bc: + cf:1f:8e:fe:25:04:6f:f2:70:bf:2f:b0:6b:4f:77: + d2:2d:e4:37:20:84:f3:94:c3:12:80:ae:bc:c3:2b: + 93:d2:fa:92:a3:1a:33:8d:d7:4a:eb:23:04:c0:38: + 51:73:fb:7a:9f:f5:3a:ca:7e:2e:c7:b6:22:3e:68: + 69:0f + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 8E:E7:D7:66:D5:0C:10:B5:7A:4F:7F:83:C3:43:94:E9:BC:E2:88:D0 + X509v3 Authority Key Identifier: + keyid:C2:8F:09:9B:D5:F1:BA:C4:74:5E:97:50:BB:86:9D:A1:F1:FA:C4:36 + + Signature Algorithm: sha256WithRSAEncryption + 17:54:5a:5e:9e:7d:fe:3f:6d:82:c5:e8:42:6b:61:91:13:5f: + 07:d5:25:4b:3c:05:e6:4c:99:a5:ff:20:ff:d3:e8:a4:25:08: + 8c:82:1b:2f:25:73:79:97:12:5e:e9:30:a1:b2:16:36:51:e2: + a5:41:bf:1c:c1:db:d2:9a:36:67:75:da:e7:36:9a:b4:65:17: + 74:af:73:02:b0:09:b3:ac:29:e7:ca:cd:01:12:7f:ba:39:29: + 90:d4:7c:3f:99:89:66:e7:eb:79:80:77:91:e4:3d:7e:87:69: + 7b:da:b5:68:07:26:ab:30:20:49:2b:46:33:3f:f7:4b:4e:e7: + a0:13:19:53:7d:73:ff:4a:95:86:35:d2:cd:ff:3c:b1:14:b4: + d8:d4:ca:de:b7:8d:2e:e3:47:f8:5d:2e:e7:b1:5b:b9:23:d3: + 54:11:89:8e:98:12:a8:10:2a:da:bb:d0:0c:07:c7:d7:21:7e: + f0:88:91:31:07:2a:a6:42:84:4a:61:9e:68:72:d4:7c:3f:59: + b2:02:e1:a6:11:9b:d2:90:73:39:13:07:e1:6b:57:2a:78:b4: + b4:f0:75:7c:6d:48:9d:33:cd:3f:d0:ff:43:a4:7e:3a:8d:fe: + 98:10:df:ab:ee:c0:58:82:cb:23:7a:b7:f5:5c:29:29:af:d0: + 40:fc:42:a3 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBBDANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJHQjET +MBEGA1UECAwKRGVyYnlzaGlyZTEVMBMGA1UECgwMUGFobyBQcm9qZWN0MRAwDgYD +VQQLDAdUZXN0aW5nMRMwEQYDVQQDDApTaWduaW5nIENBMB4XDTIxMDcwNzExMTQ0 +MloXDTI2MDcwNjExMTQ0MlowgYYxCzAJBgNVBAYTAkdCMRgwFgYDVQQIDA9Ob3R0 +aW5naGFtc2hpcmUxEzARBgNVBAcMCk5vdHRpbmdoYW0xDzANBgNVBAoMBlNlcnZl +cjETMBEGA1UECwwKUHJvZHVjdGlvbjEiMCAGA1UEAwwZdGVzdCBjbGllbnQgd2l0 +aCBwYXNzd29yZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMrLk0lF +KJVbxFG0K9Dn5Lc7NzT2XOz4fTqLuNo8lDiFX0HqKwjXPpcSUAkfN0/kJaFZtphj +Io2AfrG0JAMuXl1FpEx26KwsX8qd7W4Ke28rNNFOauG2cmZC7P24l79ASyScR2yM +SnOq4DrbrEVlI9+PSjDt1q1c66npg9o50euYMXSYvZlrhQ4d+JPP4r1Zd/6yoMTl +Y66SEBNHFFUioDC28MsXti35ffmCULJkiN1aO7aBZ4zj3ol2Y4Kvt7qDXDu8zx+O +/iUEb/Jwvy+wa0930i3kNyCE85TDEoCuvMMrk9L6kqMaM43XSusjBMA4UXP7ep/1 +Osp+Lse2Ij5oaQ8CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYd +T3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFI7n12bVDBC1 +ek9/g8NDlOm84ojQMB8GA1UdIwQYMBaAFMKPCZvV8brEdF6XULuGnaHx+sQ2MA0G +CSqGSIb3DQEBCwUAA4IBAQAXVFpenn3+P22CxehCa2GRE18H1SVLPAXmTJml/yD/ +0+ikJQiMghsvJXN5lxJe6TChshY2UeKlQb8cwdvSmjZnddrnNpq0ZRd0r3MCsAmz +rCnnys0BEn+6OSmQ1Hw/mYlm5+t5gHeR5D1+h2l72rVoByarMCBJK0YzP/dLTueg +ExlTfXP/SpWGNdLN/zyxFLTY1Mret40u40f4XS7nsVu5I9NUEYmOmBKoECrau9AM +B8fXIX7wiJExByqmQoRKYZ5octR8P1myAuGmEZvSkHM5Ewfha1cqeLS08HV8bUid +M80/0P9DpH46jf6YEN+r7sBYgssjerf1XCkpr9BA/EKj +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/client-pw.key b/phaoUtils/tests/ssl/client-pw.key new file mode 100644 index 0000000..468b275 --- /dev/null +++ b/phaoUtils/tests/ssl/client-pw.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,84549D95979482A29416CC3DBA507BC0 + +tAbMy3JP4/R53W9E8sB+fFFvGQOb+QKSW0vtjb8Z3GlIW+9wdGJ89GhXcspP7+HN +eQZy6trDmPHJ++m4sVEwBGjngLdDaajRvQVlqCIXgLvjCwIIaE2gHo3yF37umunR +0dg4NVjfEgNS3luPu7DzCv9pwIQl2YOsoPK7TuHOTGVGHCovHAr/IJ7fdaT5N9G8 +ypC7rHZhneLUs02J/L0FZUPlztOXRuiqnJisZr0pPs6ZoVEoNvR4D3VgU0nOFMcF +UmcWA+vJsXaPzmC0HDErYqfr0Mwc/7mZURCGm/+A6Q79PAAAR7pPoMczaRHP4szS +BxegO6XyKYs8a52q/XojKTcb0FESNjX0syW3+OjZusCYpuBmK4MofSdDmXVNxBLD +iBqUDLGSfU2W5H/UkHWh7O0VW1DT0RvqqFV1p1WI0dvixM80wx12rPiJ79RYLG8D +HMD7lR2iODDibCXePMg0XeCz+zf8OSfqzw6YeAMxmapZWBd8cJJ4eaUq6ziZO0hE +kvj6tUZk7d/nTSissQR2Tx6xlSm0AuHWdKx1s5gKnVg6xLKNKIVyeHnXdXkgCwSq +dICwmtP/1iYrslWCrhnB4MLA6R2vgpglwBfh7h7rW59K2untdIzr/td+h/xkynHQ +wMKJ5xZ2oRQc9oZrV0PXHQKdukniLr3owBPiu+i+QbqpzGtvhyt1wUs+NjDROrim +kritxoz6SXSIH6Wv9ae1crdhK1YTaMt1YOJT4tPjTdZyhMXqYszAH25L3ar2HaEv +Cv2YU9VqPno45/ZSVSA8xZ+E74AoZsgDMOWKFJimJv+P9CGNbm8d9SGlHDAsyj0U ++cTeyH6AWWHuAdEtVNA36qDdWOJOwhH2vT2iLuqdDySX5EJRszoxFo2RdGF3lRuQ +lVFo1v41tnvB89i9g8ZVqqkfs1IjybU2Aq+hpnqRVThRrbN75o2s2BC0K2sUvgu5 +gKUzXBl2B5CX6kWrUZ9llTSi2nH6zFAMtKvvuRQx+r+qrjJbxiPkRm9HFXlRRKYG +NZbYyrB0ovuNgL5mwraNBL8Ytzx/nGvnaJsxWqhNiDENEziGcjTiA0/gh/mtru7K +xTAQt7vVgrsynAK7c5Yhu3BBJspjgq2S9mKNpgXadcYcKQRcJnsYR9VCNWy4f7dD +sTDy9NPttZM24ayC8OjUtyusk/DxXubuqRf4mF7jKsmoTqZR/yW5/NGPOpYRs0If +9ysiBP8ctyti+snS8jSzb7PVCBJzKgEDthjLvXmV2AIeuiXTFvqTmKOlTYT8mkev +ZaXl4tS+3GGJgrSmPweAJsGFo58oQA8skExrXBW0w2rRSQDqzEo/GAbmd65IDGXh +YMOwsdvjiu9Ug4E7icxB2w5bKmhEsIh/Vj3Np/h5xcJ9W9O8zq0oYhjle3GLDGro +yPA1g0wWLeuxwPhPk7cNHFdF2Yr2CXFVug3Q9WkGTABKbZd2zV/7kk6YMRFmieV0 +h4nQBSFqR6qkF9CiUFkKS2dod0zKLPJiBRqgiopEur5QdHg1PpdEQAj6fLQo0Zfw +LoQyVi3ta9IMO1wZU8fy9w+bXoo8c76VD5jzfXw1ig0bK3iu1ozZc2UjnNlBmpYp +-----END RSA PRIVATE KEY----- diff --git a/phaoUtils/tests/ssl/client-revoked.crt b/phaoUtils/tests/ssl/client-revoked.crt new file mode 100644 index 0000000..9a1a461 --- /dev/null +++ b/phaoUtils/tests/ssl/client-revoked.crt @@ -0,0 +1,82 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 5 (0x5) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=GB, ST=Derbyshire, O=Paho Project, OU=Testing, CN=Signing CA + Validity + Not Before: Jul 7 11:14:43 2021 GMT + Not After : Jul 6 11:14:43 2026 GMT + Subject: C=GB, ST=Nottinghamshire, L=Nottingham, O=Server, OU=Production, CN=test client revoked + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:a5:7a:5f:a2:55:1f:50:22:cd:92:0d:9a:69:fa: + 47:d4:d1:2f:6f:e5:3e:22:06:2f:f4:ed:a8:85:9b: + 2a:0d:e7:e2:81:f4:23:08:68:e2:75:5b:ba:58:f5: + 57:61:5a:5c:c4:e5:27:5e:9c:8e:82:77:72:25:c2: + 2e:1d:e0:61:dc:32:f0:3b:be:7d:26:e3:a0:bb:5d: + 75:7f:87:d8:a1:26:2f:7f:01:7b:1e:2f:25:cb:bd: + 15:6c:43:12:6a:a6:02:1d:fd:7b:34:e2:1e:6c:06: + 13:de:39:e8:ee:ae:ed:cd:cc:bd:1e:48:d5:e6:11: + 95:12:08:61:88:13:d6:88:40:cc:9d:18:1c:c6:30: + 5e:8f:e8:a4:2a:c8:62:78:19:f6:95:6a:f0:ce:27: + e3:af:aa:fd:46:41:9d:83:32:f6:8e:a4:1f:32:00: + c3:ca:5f:a5:3e:bc:74:6e:96:3e:50:cd:12:ca:81: + 5a:ab:cf:a1:f8:3a:2f:fe:91:73:79:14:b3:fb:e6: + 6b:c3:57:a9:8c:2d:f6:6c:53:4f:2e:e9:4c:25:67: + 88:ac:ce:bc:84:ac:b8:d8:f5:6a:a4:ae:24:10:ea: + 4e:2c:ef:90:f5:a6:68:c3:5c:a7:e0:40:99:06:6a: + ec:b1:63:f5:7a:0b:a9:f1:81:26:95:12:9c:02:20: + 77:df + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + C7:6A:63:58:7D:DB:19:38:77:1F:41:E8:67:38:78:9D:0B:BE:51:92 + X509v3 Authority Key Identifier: + keyid:C2:8F:09:9B:D5:F1:BA:C4:74:5E:97:50:BB:86:9D:A1:F1:FA:C4:36 + + Signature Algorithm: sha256WithRSAEncryption + 5c:b9:89:ad:18:b6:0b:93:f1:b0:0a:3e:aa:f3:d0:ad:d1:6f: + 04:31:29:5a:b8:74:81:3c:4b:bb:98:8a:ca:c8:95:fa:8f:3e: + 89:ba:f2:c3:83:a2:18:1c:7c:c6:56:4b:52:83:ef:fe:87:23: + 7f:2d:6f:a2:36:22:46:04:ed:bf:ad:34:e6:9d:87:7e:92:72: + 80:b2:5d:b1:b3:23:f6:f2:bd:74:c5:34:ef:8f:50:89:8b:64: + 77:95:9d:ec:72:09:a6:c4:74:da:1d:2a:57:60:38:8f:6c:22: + ff:9e:40:73:98:ac:1f:bc:b6:e4:1b:1d:2d:73:a2:9a:ad:53: + 95:d2:17:b3:c5:8a:6c:5a:5a:be:e2:80:e4:f5:d6:99:06:61: + ec:66:44:1a:ec:ac:86:36:ef:84:4b:c5:b3:a0:c5:d7:0d:be: + 51:8c:95:46:03:e4:74:61:bf:7c:10:68:91:12:46:b8:38:94: + 9f:a2:68:77:4d:92:57:43:ff:a1:c2:67:43:33:01:1d:fd:29: + 13:8d:04:ed:7e:2d:4c:ed:8c:2f:f6:6f:44:33:3c:71:4d:f6: + 51:04:c5:c0:cb:2c:ea:95:6e:22:32:03:37:0b:32:87:89:c0: + e5:bc:72:d2:8f:73:db:40:a9:4d:f2:15:bd:c4:0d:aa:ea:2e: + 0c:ce:77:9d +-----BEGIN CERTIFICATE----- +MIID1zCCAr+gAwIBAgIBBTANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJHQjET +MBEGA1UECAwKRGVyYnlzaGlyZTEVMBMGA1UECgwMUGFobyBQcm9qZWN0MRAwDgYD +VQQLDAdUZXN0aW5nMRMwEQYDVQQDDApTaWduaW5nIENBMB4XDTIxMDcwNzExMTQ0 +M1oXDTI2MDcwNjExMTQ0M1owgYAxCzAJBgNVBAYTAkdCMRgwFgYDVQQIDA9Ob3R0 +aW5naGFtc2hpcmUxEzARBgNVBAcMCk5vdHRpbmdoYW0xDzANBgNVBAoMBlNlcnZl +cjETMBEGA1UECwwKUHJvZHVjdGlvbjEcMBoGA1UEAwwTdGVzdCBjbGllbnQgcmV2 +b2tlZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKV6X6JVH1AizZIN +mmn6R9TRL2/lPiIGL/TtqIWbKg3n4oH0Iwho4nVbulj1V2FaXMTlJ16cjoJ3ciXC +Lh3gYdwy8Du+fSbjoLtddX+H2KEmL38Bex4vJcu9FWxDEmqmAh39ezTiHmwGE945 +6O6u7c3MvR5I1eYRlRIIYYgT1ohAzJ0YHMYwXo/opCrIYngZ9pVq8M4n46+q/UZB +nYMy9o6kHzIAw8pfpT68dG6WPlDNEsqBWqvPofg6L/6Rc3kUs/vma8NXqYwt9mxT +Ty7pTCVniKzOvISsuNj1aqSuJBDqTizvkPWmaMNcp+BAmQZq7LFj9XoLqfGBJpUS +nAIgd98CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNT +TCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFMdqY1h92xk4dx9B6Gc4 +eJ0LvlGSMB8GA1UdIwQYMBaAFMKPCZvV8brEdF6XULuGnaHx+sQ2MA0GCSqGSIb3 +DQEBCwUAA4IBAQBcuYmtGLYLk/GwCj6q89Ct0W8EMSlauHSBPEu7mIrKyJX6jz6J +uvLDg6IYHHzGVktSg+/+hyN/LW+iNiJGBO2/rTTmnYd+knKAsl2xsyP28r10xTTv +j1CJi2R3lZ3scgmmxHTaHSpXYDiPbCL/nkBzmKwfvLbkGx0tc6KarVOV0hezxYps +Wlq+4oDk9daZBmHsZkQa7KyGNu+ES8WzoMXXDb5RjJVGA+R0Yb98EGiREka4OJSf +omh3TZJXQ/+hwmdDMwEd/SkTjQTtfi1M7Ywv9m9EMzxxTfZRBMXAyyzqlW4iMgM3 +CzKHicDlvHLSj3PbQKlN8hW9xA2q6i4Mzned +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/client-revoked.key b/phaoUtils/tests/ssl/client-revoked.key new file mode 100644 index 0000000..9939572 --- /dev/null +++ b/phaoUtils/tests/ssl/client-revoked.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEApXpfolUfUCLNkg2aafpH1NEvb+U+IgYv9O2ohZsqDefigfQj +CGjidVu6WPVXYVpcxOUnXpyOgndyJcIuHeBh3DLwO759JuOgu111f4fYoSYvfwF7 +Hi8ly70VbEMSaqYCHf17NOIebAYT3jno7q7tzcy9HkjV5hGVEghhiBPWiEDMnRgc +xjBej+ikKshieBn2lWrwzifjr6r9RkGdgzL2jqQfMgDDyl+lPrx0bpY+UM0SyoFa +q8+h+Dov/pFzeRSz++Zrw1epjC32bFNPLulMJWeIrM68hKy42PVqpK4kEOpOLO+Q +9aZow1yn4ECZBmrssWP1egup8YEmlRKcAiB33wIDAQABAoIBACVlqZVLPX9jzieS +0XHf8TnkaJ8WJNuVoGLvDuXa8j8gR61s2jn9UiiJqWyPTccfn9WToDkekopjqjVk +U/3Ghvc3v9kQrMIMMXgGoBZJQijxM0y1rfhdWWJZAi1sXw4hJFtYvO5vp8Zr/TN8 +zOqcN/wJqDfe6BBNqu3fXQNe0F4MQeiLVYi7c1Q0ZupiALZDFPJ1u03xFiIzlMrN +QLghfUoq4pFgqC08wR31XncvcWQ/iOggznxjy16Ezx1ubqGf7cMWZmXEWtdD0fsP +8P3x/VS+MBzVtX9hhTaS3pVUAZKriCLF8kQiCUgtzlbUvKNrRb6gLan+OcD+J2sE +0wopLZkCgYEA0BsLbbf0pc+PT+oCFUUueyADSl3SuH8j1YCpSU0nl0h6lE90cOen +HSPRa4WHhhQm9lgCtTLXlHxJsZym0Lau7Nd90MIrjuvgRv0uOk07tRyThoGGuSCL +2fBnD+a8NGjh+s/KTxmBDdWPkRpaZ5MVl8ZSGSM5zUZzplwPLx/J+wUCgYEAy4/W +nd+rU4oh3Hm6cVp4YaktZ9cU/YB0yDd/sIm7bJ+kDyz0LSw7l/FSxMzQ+Dqybqht +We+jn07BOh89r70vbcxx+4kMtHbg5Ii1u/R74AgGK6JSiKKpybLi7LA+3vcW/Hg6 +lemxE1U/PmmdvvSjzN/EXpeABkSgbctkJeoYRJMCgYBKLvnZ+NNrMBxEPoTTlD/H +gFfr8JonTps1hpHSIYDVeu7HY7N8c/eseZIzo/v1ncVt113Pvfn/Ynbaq58Dk7uz +jfW5rx3b6tWeOK579gAsxa0JK68c2y8/V2VF09iPTjwQLnZN0CejCNgOv7guZ84w +tm+Zqmb2eADN8s8u20QjCQKBgQCoUHXHskKqX6Ph9nD3+zNgpQ8bNldvyMBHMMSP +B0OG7HUt6yC3HUTlPLAQc74yEe6p2vAYFjK3rdnNojlST16hLhPtRQPRUB5iOLvz +/pJSyq+3co9F1SII2bYSuSQzHiHOfecLP+CfuLQDejbpxsSNyVRIVoKQLDxurGdR +hj+sqwKBgQC+x2dapmrh7qcCbPxbheWH32ds9GgI1Nr8eW2Wm0wIguLA4TrBN0UH +HQZSdOjQoD/gTtHu79xbD1LPJk4kDtkvdzAlvbEds+3ArlaibNPcDaatkFAJt+2e +2t8UDdIKzxOaKEF8YfTaeCpyS7CZoVw3frA9nkK7h38PgFeB7Vx2Rw== +-----END RSA PRIVATE KEY----- diff --git a/phaoUtils/tests/ssl/client.crt b/phaoUtils/tests/ssl/client.crt new file mode 100644 index 0000000..c3fa4f5 --- /dev/null +++ b/phaoUtils/tests/ssl/client.crt @@ -0,0 +1,82 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 2 (0x2) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=GB, ST=Derbyshire, O=Paho Project, OU=Testing, CN=Signing CA + Validity + Not Before: Jul 7 11:14:42 2021 GMT + Not After : Jul 6 11:14:42 2026 GMT + Subject: C=GB, ST=Nottinghamshire, L=Nottingham, O=Server, OU=Production, CN=test client + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:d0:58:ed:ad:44:b4:f8:30:16:27:d9:b4:2c:b4: + 24:67:9a:19:fe:32:04:0d:9a:7e:75:97:12:d6:2c: + d4:97:33:fb:30:8c:ef:a2:b1:ef:e5:92:d6:56:05: + 75:c8:19:82:92:4e:4b:13:8e:25:90:b9:21:72:f4: + a4:bf:7a:e2:0f:75:52:08:04:4c:e8:6a:35:7e:7d: + 78:d9:b8:f7:2b:3d:8e:4e:b5:f3:7a:9a:06:10:50: + ca:95:63:2c:bd:3a:89:d0:8a:84:12:32:9b:00:a7: + 25:33:70:d2:18:0a:43:94:12:62:e7:77:db:b8:0f: + dc:23:48:95:5c:77:c6:11:4f:0f:d6:6e:73:59:7c: + ed:6a:fd:ba:24:f0:b2:59:c3:a2:16:65:ad:19:7f: + 92:87:8c:ea:b5:e5:0f:26:f8:b1:74:98:c3:fd:ed: + 4d:74:d0:58:ce:d9:9c:24:34:9b:75:79:25:d0:aa: + 6c:03:03:0c:3a:4a:4c:9a:36:50:ab:55:74:1e:8b: + de:41:a7:14:b9:57:ee:8b:31:90:5c:00:af:31:9d: + e0:55:07:8d:05:ed:c9:5f:e1:79:b7:96:be:d9:5b: + cf:a7:5c:cd:48:fc:bd:a4:34:bf:e0:49:d5:25:60: + 7a:4c:32:37:97:e4:f8:64:24:a6:79:c1:62:8d:93: + 52:53 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 61:62:90:E6:BB:8A:BB:06:6C:8A:66:9F:A5:C7:85:12:43:5C:94:6F + X509v3 Authority Key Identifier: + keyid:C2:8F:09:9B:D5:F1:BA:C4:74:5E:97:50:BB:86:9D:A1:F1:FA:C4:36 + + Signature Algorithm: sha256WithRSAEncryption + a4:ad:a8:cf:8b:f1:c0:e5:ed:e0:9f:eb:b8:46:17:fe:cf:49: + ed:e2:94:3b:3c:ea:4d:8b:e5:e3:5b:5f:f0:51:f0:53:88:22: + 61:fc:f9:9d:c3:67:5f:9c:20:f3:2c:bb:65:c3:66:d9:15:b8: + 60:82:31:95:d4:96:43:11:c1:56:da:4b:ad:bc:3f:b2:5f:3d: + 40:8d:e4:22:26:9b:d5:5d:ff:02:55:c1:f9:ca:f3:67:46:be: + 7d:d0:8c:68:40:a6:64:01:f0:ce:8e:2c:c2:6c:16:96:23:64: + e6:2f:95:b9:95:a2:85:8e:ec:61:56:6f:b9:3a:87:e9:cc:f1: + 94:ca:51:d4:ce:50:01:91:1a:8c:ff:f9:cf:30:d4:aa:53:44: + 67:44:84:4c:07:a7:ab:c3:34:3a:16:69:8c:37:7f:a0:fb:e1: + fa:ec:e6:9d:3c:fd:13:a9:6f:b2:d8:dc:46:81:ae:a6:63:4f: + 80:47:a7:80:51:a7:d4:d6:c8:11:85:7d:5f:ab:ef:3a:93:62: + d7:fb:c2:a9:e4:b9:40:7e:d1:59:d0:d4:ff:75:bf:70:72:a2: + 93:a3:47:41:d8:cf:d5:c6:8c:90:b8:d3:01:d8:53:a6:c1:3c: + a9:d9:e4:ef:15:e9:47:9c:9d:eb:5a:bb:11:df:da:f1:81:5d: + 89:c9:4a:8f +-----BEGIN CERTIFICATE----- +MIIDzjCCAragAwIBAgIBAjANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJHQjET +MBEGA1UECAwKRGVyYnlzaGlyZTEVMBMGA1UECgwMUGFobyBQcm9qZWN0MRAwDgYD +VQQLDAdUZXN0aW5nMRMwEQYDVQQDDApTaWduaW5nIENBMB4XDTIxMDcwNzExMTQ0 +MloXDTI2MDcwNjExMTQ0MloweDELMAkGA1UEBhMCR0IxGDAWBgNVBAgMD05vdHRp +bmdoYW1zaGlyZTETMBEGA1UEBwwKTm90dGluZ2hhbTEPMA0GA1UECgwGU2VydmVy +MRMwEQYDVQQLDApQcm9kdWN0aW9uMRQwEgYDVQQDDAt0ZXN0IGNsaWVudDCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANBY7a1EtPgwFifZtCy0JGeaGf4y +BA2afnWXEtYs1Jcz+zCM76Kx7+WS1lYFdcgZgpJOSxOOJZC5IXL0pL964g91UggE +TOhqNX59eNm49ys9jk6183qaBhBQypVjLL06idCKhBIymwCnJTNw0hgKQ5QSYud3 +27gP3CNIlVx3xhFPD9Zuc1l87Wr9uiTwslnDohZlrRl/koeM6rXlDyb4sXSYw/3t +TXTQWM7ZnCQ0m3V5JdCqbAMDDDpKTJo2UKtVdB6L3kGnFLlX7osxkFwArzGd4FUH +jQXtyV/hebeWvtlbz6dczUj8vaQ0v+BJ1SVgekwyN5fk+GQkpnnBYo2TUlMCAwEA +AaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0 +ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFGFikOa7irsGbIpmn6XHhRJDXJRvMB8G +A1UdIwQYMBaAFMKPCZvV8brEdF6XULuGnaHx+sQ2MA0GCSqGSIb3DQEBCwUAA4IB +AQCkrajPi/HA5e3gn+u4Rhf+z0nt4pQ7POpNi+XjW1/wUfBTiCJh/Pmdw2dfnCDz +LLtlw2bZFbhggjGV1JZDEcFW2kutvD+yXz1AjeQiJpvVXf8CVcH5yvNnRr590Ixo +QKZkAfDOjizCbBaWI2TmL5W5laKFjuxhVm+5OofpzPGUylHUzlABkRqM//nPMNSq +U0RnRIRMB6erwzQ6FmmMN3+g++H67OadPP0TqW+y2NxGga6mY0+AR6eAUafU1sgR +hX1fq+86k2LX+8Kp5LlAftFZ0NT/db9wcqKTo0dB2M/VxoyQuNMB2FOmwTyp2eTv +FelHnJ3rWrsR39rxgV2JyUqP +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/client.key b/phaoUtils/tests/ssl/client.key new file mode 100644 index 0000000..688b7ce --- /dev/null +++ b/phaoUtils/tests/ssl/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA0FjtrUS0+DAWJ9m0LLQkZ5oZ/jIEDZp+dZcS1izUlzP7MIzv +orHv5ZLWVgV1yBmCkk5LE44lkLkhcvSkv3riD3VSCARM6Go1fn142bj3Kz2OTrXz +epoGEFDKlWMsvTqJ0IqEEjKbAKclM3DSGApDlBJi53fbuA/cI0iVXHfGEU8P1m5z +WXztav26JPCyWcOiFmWtGX+Sh4zqteUPJvixdJjD/e1NdNBYztmcJDSbdXkl0Kps +AwMMOkpMmjZQq1V0HoveQacUuVfuizGQXACvMZ3gVQeNBe3JX+F5t5a+2VvPp1zN +SPy9pDS/4EnVJWB6TDI3l+T4ZCSmecFijZNSUwIDAQABAoIBAQC60zN1jsm0T/Je +E6Kz/2kxmYa7YQAvbpz9NtYGRbbwSwVwuMBdpK9Yrj4Sbtz57J4gMaKyy2E2EDxF +R8i/hyJU+D/xvmF0e2CypzKKEYlaNd15CUFma90KHlg6cu74VBimbr8VTlmd0UPT +h9RtCC8nBQG5S8ozl80vunNssl5iv2JeXvVOL2sRagCr787XeGur74jW2rsh65M/ +ba6X3iv143D/1KNGiPAoGpP6vxvnOvM2K9+oqDf5SifmXfgSPIEoBd315YeQdGez +BnOJHb7k2vokb+PsiTwjMsIf0AUqwKZPok+sLfaACxs4b2IrmFIYhcAcfyKkPD8i +A1DpsEUhAoGBAPfKBnFNiMKc+JFM9lc01nQcdIKgtSmIPNUB7jOeg62oI3VzNp11 +9iw2qCQGJZwc1U3QbmNTAap2MxDss98kP2UZcBNDC0pxo+6F7XxIANRURXrG4NRg +iPDu1lzubrbzXoh8XMxjKYacPP/gW/2VnxwhOyeIAt3mAN/Bu57t+XRxAoGBANdA +UmcYtbu1rW8tzSuXRVgFTDhK1bORao58qJBP95MpFh/nQPB2Q1nu+nReo3870xE2 +6/R0gBv8gvKdEMVvhoDRlEJNhelQSg+yVQsX25gQcsKmR6/0IH+S0CH0PibS7o3G +sjR9g27MitpX9MzzMR5R5IQErrDw01eOecGzzMUDAoGAc9IJiuJL33ORuBD6QC7h +Yqp+RySpKT2V+ZaKabRZJk2mLVrqF1Ww+F+f3h7Fa6AKj/Gx91kwOSZAnlOVi+Kc +gzwNp+M5ntVZY79UDzh0ssqlI0tcgciRmdR5fDyyoW9GK5O9qIddPJ9A3/VV6kUK +dxKNXN/1PxUoKW6brSDc7fECgYB2BcCo4rWSrLThxv0+L31IG++E1hOCl/MTGWrb +Zd1bhSWqbIQA1Pds8knFULbY5pZ+U9zgdphfv/6UxGYTu2jGbSObjyIjoXBaVu+m +W3h+UlZ6P+4CnhrLmFYip+cEJpfCiPXhLgjI0cI4og2J6rY9560ibebTAdj/oxFD +kjBuvQKBgBaKdaszx7MOBEGmmcv7HQ/prgi1s3UZbG5Jl98XS2xMVJO9N7x7k0hC +SEj6kwlOSkpoiDEHSJ2s0j58Mnp5YxwPi61w3ZCnYg/iLgFfnJ972nliBw3wB/iD +ZdYerxKGa6Btdbwt14rzE5QfzL8TiFapmq7JnY/rgE11Tx5SR06H +-----END RSA PRIVATE KEY----- diff --git a/phaoUtils/tests/ssl/crl.pem b/phaoUtils/tests/ssl/crl.pem new file mode 100644 index 0000000..1721021 --- /dev/null +++ b/phaoUtils/tests/ssl/crl.pem @@ -0,0 +1,12 @@ +-----BEGIN X509 CRL----- +MIIBzzCBuAIBATANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJHQjETMBEGA1UE +CAwKRGVyYnlzaGlyZTEVMBMGA1UECgwMUGFobyBQcm9qZWN0MRAwDgYDVQQLDAdU +ZXN0aW5nMRMwEQYDVQQDDApTaWduaW5nIENBFw0yMTA3MDcxMTE0NDNaFw0yMTA4 +MDYxMTE0NDNaMBQwEgIBBRcNMjEwNzA3MTExNDQzWqAOMAwwCgYDVR0UBAMCAQEw +DQYJKoZIhvcNAQELBQADggEBAL+k0y+sONbBjgtGs6WX3AuKPNv+uEVSAdRR1UMX +3KwcwT9jy/5ypT7dv7UNz5V3IR8t41sCN6E3rVvOv+DFn6Br+eabg6GR/iZMhoUq +hqknHZZTeMZ5VwzDIZvINHtRvli2bC5/sfU0S44d19lWW4rCVz78c1zf8MvsDc9U +fwjtZofTZr/8p7t5KIdohYCwlHu+ANxi7qCJIuPJyZaPQ4wUSbRtu6idvkgYgmpC +O74Fe+/eg6zQwd/B1MZEVdBx+66PAyELnyCW+R9PgILzET+YMDni/lT1AYwnCCJ2 +lgiTIQlmyT/xlFCfmmzbsCkTcrMPym4m3zTOzbaeiYUBAco= +-----END X509 CRL----- diff --git a/phaoUtils/tests/ssl/gen.sh b/phaoUtils/tests/ssl/gen.sh new file mode 100644 index 0000000..270bb61 --- /dev/null +++ b/phaoUtils/tests/ssl/gen.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# This file generates the keys and certificates used for testing mosquitto. +# None of the keys are encrypted, so do not just use this script to generate +# files for your own use. + +set -e + +rm -f *.crt *.key *.csr +for a in root signing; do + rm -rf ${a}CA/ + mkdir -p ${a}CA/newcerts + touch ${a}CA/index.txt + echo 01 > ${a}CA/serial + echo 01 > ${a}CA/crlnumber +done +rm -rf certs + +BASESUBJ="/C=GB/ST=Derbyshire/L=Derby/O=Paho Project/OU=Testing" +SBASESUBJ="/C=GB/ST=Nottinghamshire/L=Nottingham/O=Server/OU=Production" +BBASESUBJ="/C=GB/ST=Nottinghamshire/L=Nottingham/O=Server/OU=Bridge" + +# The root CA +openssl genrsa -out test-root-ca.key 2048 +openssl req -new -x509 -days 3650 -key test-root-ca.key -out test-root-ca.crt -config openssl.cnf -subj "${BASESUBJ}/CN=Root CA/" + +# Another root CA that doesn't sign anything +openssl genrsa -out test-bad-root-ca.key 2048 +openssl req -new -x509 -days 3650 -key test-bad-root-ca.key -out test-bad-root-ca.crt -config openssl.cnf -subj "${BASESUBJ}/CN=Bad Root CA/" + +# This is a root CA that has the exact same details as the real root CA, but is a different key and certificate. Effectively a "fake" CA. +openssl genrsa -out test-fake-root-ca.key 2048 +openssl req -new -x509 -days 3650 -key test-fake-root-ca.key -out test-fake-root-ca.crt -config openssl.cnf -subj "${BASESUBJ}/CN=Root CA/" + +# An intermediate CA, signed by the root CA, used to sign server/client csrs. +openssl genrsa -out test-signing-ca.key 2048 +openssl req -out test-signing-ca.csr -key test-signing-ca.key -new -config openssl.cnf -subj "${BASESUBJ}/CN=Signing CA/" +openssl ca -batch -config openssl.cnf -name CA_root -extensions v3_ca -out test-signing-ca.crt -infiles test-signing-ca.csr + +# An alternative intermediate CA, signed by the root CA, not used to sign anything. +openssl genrsa -out test-alt-ca.key 2048 +openssl req -out test-alt-ca.csr -key test-alt-ca.key -new -config openssl.cnf -subj "${BASESUBJ}/CN=Alternative Signing CA/" +openssl ca -batch -config openssl.cnf -name CA_root -extensions v3_ca -out test-alt-ca.crt -infiles test-alt-ca.csr + +# Valid server key and certificate. +openssl genrsa -out server.key 2048 +openssl req -new -key server.key -out server.csr -config openssl.cnf -subj "${SBASESUBJ}/CN=localhost/" +openssl ca -batch -config openssl.cnf -name CA_signing -out server.crt -infiles server.csr + +# Expired server certificate, based on the above server key. +openssl req -new -days 1 -key server.key -out server-expired.csr -config openssl.cnf -subj "${SBASESUBJ}/CN=localhost/" +echo -n > signingCA/index.txt +echo 01 > signingCA/serial +openssl ca -batch -config openssl.cnf -name CA_signing -days 1 -startdate 120820000000Z -enddate 120821000000Z -out server-expired.crt -infiles server-expired.csr + +# Valid client key and certificate. +openssl genrsa -out client.key 2048 +openssl req -new -key client.key -out client.csr -config openssl.cnf -subj "${SBASESUBJ}/CN=test client/" +openssl ca -batch -config openssl.cnf -name CA_signing -out client.crt -infiles client.csr + +# Expired client certificate, based on the above client key. +openssl req -new -days 1 -key client.key -out client-expired.csr -config openssl.cnf -subj "${SBASESUBJ}/CN=test client expired/" +openssl ca -batch -config openssl.cnf -name CA_signing -days 1 -startdate 120820000000Z -enddate 120821000000Z -out client-expired.crt -infiles client-expired.csr + +# Valid client key and certificate, key is encrypted with a password. +openssl genrsa -aes128 -passout pass:password -out client-pw.key 2048 +openssl req -new -key client-pw.key -passin pass:password -out client-pw.csr -config openssl.cnf -subj "${SBASESUBJ}/CN=test client with password/" +openssl ca -batch -config openssl.cnf -name CA_signing -out client-pw.crt -infiles client-pw.csr + +# Revoked client certificate, based on a new client key. +openssl genrsa -out client-revoked.key 2048 +openssl req -new -days 1 -key client-revoked.key -out client-revoked.csr -config openssl.cnf -subj "${SBASESUBJ}/CN=test client revoked/" +openssl ca -batch -config openssl.cnf -name CA_signing -out client-revoked.crt -infiles client-revoked.csr +openssl ca -batch -config openssl.cnf -name CA_signing -revoke client-revoked.crt +openssl ca -batch -config openssl.cnf -name CA_signing -gencrl -out crl.pem + +cat test-signing-ca.crt test-root-ca.crt > all-ca.crt +#mkdir certs +#cp test-signing-ca.crt certs/test-signing-ca.pem +#cp test-root-ca.crt certs/test-root.ca.pem +c_rehash certs + +rm -f client-expired.csr client-revoked.csr server-expired.csr server.csr test-alt-ca.csr diff --git a/phaoUtils/tests/ssl/openssl.cnf b/phaoUtils/tests/ssl/openssl.cnf new file mode 100644 index 0000000..75342a4 --- /dev/null +++ b/phaoUtils/tests/ssl/openssl.cnf @@ -0,0 +1,406 @@ +# +# OpenSSL example configuration file. +# This is mostly being used for generation of certificate requests. +# + +# This definition stops the following lines choking if HOME isn't +# defined. +HOME = . +RANDFILE = $ENV::HOME/.rnd + +# Extra OBJECT IDENTIFIER info: +#oid_file = $ENV::HOME/.oid +oid_section = new_oids + +# To use this configuration file with the "-extfile" option of the +# "openssl x509" utility, name here the section containing the +# X.509v3 extensions to use: +# extensions = +# (Alternatively, use a configuration file that has only +# X.509v3 extensions in its main [= default] section.) + +[ new_oids ] + +# We can add new OIDs in here for use by 'ca', 'req' and 'ts'. +# Add a simple OID like this: +# testoid1=1.2.3.4 +# Or use config file substitution like this: +# testoid2=${testoid1}.5.6 + +# Policies used by the TSA examples. +tsa_policy1 = 1.2.3.4.1 +tsa_policy2 = 1.2.3.4.5.6 +tsa_policy3 = 1.2.3.4.5.7 + +#################################################################### +[ ca ] +default_ca = CA_default # The default ca section + +#################################################################### +[ CA_signing ] + +dir = ./signingCA # Where everything is kept +certs = $dir/certs # Where the issued certs are kept +crl_dir = $dir/crl # Where the issued crl are kept +database = $dir/index.txt # database index file. +#unique_subject = no # Set to 'no' to allow creation of + # several certificates with same subject. +new_certs_dir = $dir/newcerts # default place for new certs. + +certificate = test-signing-ca.crt # The CA certificate +serial = $dir/serial # The current serial number +crlnumber = $dir/crlnumber # the current crl number + # must be commented out to leave a V1 CRL +crl = $dir/crl.pem # The current CRL +private_key = test-signing-ca.key # The private key +RANDFILE = $dir/.rand # private random number file + +x509_extensions = usr_cert # The extensions to add to the cert + +# Comment out the following two lines for the "traditional" +# (and highly broken) format. +name_opt = ca_default # Subject Name options +cert_opt = ca_default # Certificate field options + +# Extension copying option: use with caution. +# copy_extensions = copy + +# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs +# so this is commented out by default to leave a V1 CRL. +# crlnumber must also be commented out to leave a V1 CRL. +# crl_extensions = crl_ext + +default_days = 1825 # how long to certify for +default_crl_days= 30 # how long before next CRL +default_md = default # use public key default MD +preserve = no # keep passed DN ordering + +# A few difference way of specifying how similar the request should look +# For type CA, the listed attributes must be the same, and the optional +# and supplied fields are just that :-) +policy = policy_anything + +[ CA_inter ] +dir = ./interCA +certs = $dir/certs +crl_dir = $dir/crl +database = $dir/index.txt +new_certs_dir = $dir/newcerts + +certificate = test-inter-ca.crt +serial = $dir/serial +crlnumber = $dir/crlnumber +crl = $dir/crl.pem +private_key = test-inter-ca.key +RANDFILE = $dir/.rand + +#x509_extensions = v3_ca +x509_extensions = usr_cert + +name_opt = ca_default +cert_opt = ca_default + +default_days = 1825 +default_crl_days = 30 +default_md = default +preserve = no + +policy = policy_match +unique_subject = yes + +[ CA_root ] +dir = ./rootCA +certs = $dir/certs +crl_dir = $dir/crl +database = $dir/index.txt +new_certs_dir = $dir/newcerts + +certificate = test-root-ca.crt +serial = $dir/serial +crlnumber = $dir/crlnumber +crl = $dir/crl.pem +private_key = test-root-ca.key +RANDFILE = $dir/.rand + +x509_extensions = v3_ca + +name_opt = ca_default +cert_opt = ca_default + +default_days = 1825 +default_crl_days = 30 +default_md = default +preserve = no + +policy = policy_match +unique_subject = yes + +# For the CA policy +[ policy_match ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +# For the 'anything' policy +# At this point in time, you must list all acceptable 'object' +# types. +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +#################################################################### +[ req ] +default_bits = 2048 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca # The extensions to add to the self signed cert + +# Passwords for private keys if not present they will be prompted for +# input_password = secret +# output_password = secret + +# This sets a mask for permitted string types. There are several options. +# default: PrintableString, T61String, BMPString. +# pkix : PrintableString, BMPString (PKIX recommendation before 2004) +# utf8only: only UTF8Strings (PKIX recommendation after 2004). +# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). +# MASK:XXXX a literal mask value. +# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings. +string_mask = utf8only + +# req_extensions = v3_req # The extensions to add to a certificate request + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = GB +countryName_min = 2 +countryName_max = 2 + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Derbyshire + +localityName = Locality Name (eg, city) +localityName_default = Derby + +0.organizationName = Organization Name (eg, company) +0.organizationName_default = Paho Project + +# we can do this but it is not needed normally :-) +#1.organizationName = Second Organization Name (eg, company) +#1.organizationName_default = World Wide Web Pty Ltd + +organizationalUnitName = Organizational Unit Name (eg, section) +organizationalUnitName_default = Testing + +commonName = Common Name (e.g. server FQDN or YOUR name) +commonName_max = 64 + +emailAddress = Email Address +emailAddress_max = 64 + +# SET-ex3 = SET extension number 3 + +[ req_attributes ] +challengePassword = A challenge password +challengePassword_min = 4 +challengePassword_max = 20 + +unstructuredName = An optional company name + +[ usr_cert ] + +# These extensions are added when 'ca' signs a request. + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +# This is required for TSA certificates. +# extendedKeyUsage = critical,timeStamping + +[ v3_req ] + +# Extensions to add to a certificate request + +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +[ v3_ca ] + + +# Extensions for a typical CA + + +# PKIX recommendation. + +subjectKeyIdentifier=hash + +authorityKeyIdentifier=keyid:always,issuer + +# This is what PKIX recommends but some broken software chokes on critical +# extensions. +#basicConstraints = critical,CA:true +# So we do this instead. +basicConstraints = CA:true + +# Key usage: this is typical for a CA certificate. However since it will +# prevent it being used as an test self-signed certificate it is best +# left out by default. +# keyUsage = cRLSign, keyCertSign + +# Some might want this also +# nsCertType = sslCA, emailCA + +# Include email address in subject alt name: another PKIX recommendation +# subjectAltName=email:copy +# Copy issuer details +# issuerAltName=issuer:copy + +# DER hex encoding of an extension: beware experts only! +# obj=DER:02:03 +# Where 'obj' is a standard or added object +# You can even override a supported extension: +# basicConstraints= critical, DER:30:03:01:01:FF + +[ crl_ext ] + +# CRL extensions. +# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. + +# issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always + +[ proxy_cert_ext ] +# These extensions should be added when creating a proxy certificate + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +# This really needs to be in place for it to be a proxy certificate. +proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo + +#################################################################### +[ tsa ] + +default_tsa = tsa_config1 # the default TSA section + +[ tsa_config1 ] + +# These are used by the TSA reply generation only. +dir = ./demoCA # TSA root directory +serial = $dir/tsaserial # The current serial number (mandatory) +crypto_device = builtin # OpenSSL engine to use for signing +signer_cert = $dir/tsacert.pem # The TSA signing certificate + # (optional) +certs = $dir/cacert.pem # Certificate chain to include in reply + # (optional) +signer_key = $dir/private/tsakey.pem # The TSA private key (optional) + +default_policy = tsa_policy1 # Policy if request did not specify it + # (optional) +other_policies = tsa_policy2, tsa_policy3 # acceptable policies (optional) +digests = md5, sha1 # Acceptable message digests (mandatory) +accuracy = secs:1, millisecs:500, microsecs:100 # (optional) +clock_precision_digits = 0 # number of digits after dot. (optional) +ordering = yes # Is ordering defined for timestamps? + # (optional, default: no) +tsa_name = yes # Must the TSA name be included in the reply? + # (optional, default: no) +ess_cert_id_chain = no # Must the ESS cert id chain be included? + # (optional, default: no) diff --git a/phaoUtils/tests/ssl/server-expired.crt b/phaoUtils/tests/ssl/server-expired.crt new file mode 100644 index 0000000..0703ac6 --- /dev/null +++ b/phaoUtils/tests/ssl/server-expired.crt @@ -0,0 +1,82 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=GB, ST=Derbyshire, O=Paho Project, OU=Testing, CN=Signing CA + Validity + Not Before: Aug 20 00:00:00 2012 GMT + Not After : Aug 21 00:00:00 2012 GMT + Subject: C=GB, ST=Nottinghamshire, L=Nottingham, O=Server, OU=Production, CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:a9:4c:88:db:56:36:f8:fc:e1:eb:6b:ad:9c:0f: + 78:3e:7d:d7:34:c6:83:94:6d:83:07:d5:3e:cb:fb: + 95:61:e5:73:78:43:db:51:d9:a0:4e:ec:8e:43:21: + 91:1f:56:95:08:47:7c:83:38:90:bb:53:91:ed:fd: + b4:bd:08:27:dd:d6:d9:5b:fd:bb:84:1e:2e:62:d9: + 3c:1d:4d:c9:6b:17:45:d7:9e:b4:a5:9c:22:cd:14: + 41:32:c3:41:ad:8d:f5:2f:a3:d5:59:1f:a1:2b:67: + d3:01:83:64:93:80:6b:bf:5a:b8:51:86:20:a0:e4: + 3f:18:0c:67:19:8d:e3:58:6d:85:83:8f:8b:37:b2: + 7d:21:3f:65:cf:19:53:2e:56:df:4d:89:50:7e:8c: + 6a:8e:dd:21:15:15:31:9b:c2:5c:98:68:1e:31:ff: + c6:6c:1f:a8:42:b8:da:62:dc:ae:62:4c:40:f0:06: + c6:e6:f4:a9:98:3d:ed:fb:c0:2a:63:da:60:69:83: + 11:0e:ce:ba:93:d7:4b:27:8f:86:91:ef:e4:65:5f: + 20:be:04:f2:4d:d6:d1:74:c5:ab:e9:18:df:16:f9: + 9a:8a:ff:2f:23:c5:46:3e:04:16:4e:fa:c1:0a:f4: + dc:8e:1a:da:5f:a1:ad:50:7a:5d:60:00:3e:09:b8: + 8e:6d + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + C3:47:33:CF:07:18:14:7C:9A:E4:AB:11:62:89:88:54:3D:5D:7D:E8 + X509v3 Authority Key Identifier: + keyid:C2:8F:09:9B:D5:F1:BA:C4:74:5E:97:50:BB:86:9D:A1:F1:FA:C4:36 + + Signature Algorithm: sha256WithRSAEncryption + ca:b6:c4:8a:76:e2:14:01:2f:58:ea:5f:28:f3:1d:de:a5:73: + 17:13:d0:5a:2f:51:f8:a7:79:34:06:9d:73:8e:c9:bd:ba:e4: + 03:64:7b:fc:29:b1:f3:4a:22:a1:bd:31:7a:4e:03:0d:f0:0c: + b0:d8:40:03:7a:b6:a5:2a:ff:78:0b:de:49:7b:ee:11:97:52: + 2a:df:68:53:d3:88:ac:bd:f2:04:25:68:04:12:8f:ea:26:05: + 0d:9b:71:76:a9:cd:ff:99:78:44:86:07:56:04:14:c6:d7:1d: + 63:6e:9f:07:76:95:0b:a0:2b:a2:0d:c4:79:ff:80:c2:80:cb: + 83:c3:ec:ae:46:62:bb:09:71:c9:65:00:b8:6a:13:a4:a7:31: + ad:ff:81:97:1c:84:1e:16:d5:c2:69:83:88:63:2d:33:31:52: + 1b:fc:dc:c7:40:5c:c8:3e:0a:15:87:7f:82:47:8d:3e:f2:3e: + 43:34:c1:8f:9c:16:61:1e:17:3f:4b:37:e1:aa:80:ad:87:09: + cb:5c:fe:5a:28:4d:85:ca:45:58:6f:a6:ab:e2:f7:7a:24:c9: + 34:2a:75:b9:29:b8:db:cf:0b:72:e3:89:06:d6:6c:a9:9f:82: + e6:0f:90:b9:1a:4e:d1:f1:24:32:79:77:d3:cf:8f:27:64:f3: + d6:3e:ff:45 +-----BEGIN CERTIFICATE----- +MIIDzDCCArSgAwIBAgIBATANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJHQjET +MBEGA1UECAwKRGVyYnlzaGlyZTEVMBMGA1UECgwMUGFobyBQcm9qZWN0MRAwDgYD +VQQLDAdUZXN0aW5nMRMwEQYDVQQDDApTaWduaW5nIENBMB4XDTEyMDgyMDAwMDAw +MFoXDTEyMDgyMTAwMDAwMFowdjELMAkGA1UEBhMCR0IxGDAWBgNVBAgMD05vdHRp +bmdoYW1zaGlyZTETMBEGA1UEBwwKTm90dGluZ2hhbTEPMA0GA1UECgwGU2VydmVy +MRMwEQYDVQQLDApQcm9kdWN0aW9uMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpTIjbVjb4/OHra62cD3g+fdc0xoOU +bYMH1T7L+5Vh5XN4Q9tR2aBO7I5DIZEfVpUIR3yDOJC7U5Ht/bS9CCfd1tlb/buE +Hi5i2TwdTclrF0XXnrSlnCLNFEEyw0GtjfUvo9VZH6ErZ9MBg2STgGu/WrhRhiCg +5D8YDGcZjeNYbYWDj4s3sn0hP2XPGVMuVt9NiVB+jGqO3SEVFTGbwlyYaB4x/8Zs +H6hCuNpi3K5iTEDwBsbm9KmYPe37wCpj2mBpgxEOzrqT10snj4aR7+RlXyC+BPJN +1tF0xavpGN8W+ZqK/y8jxUY+BBZO+sEK9NyOGtpfoa1Qel1gAD4JuI5tAgMBAAGj +ezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVk +IENlcnRpZmljYXRlMB0GA1UdDgQWBBTDRzPPBxgUfJrkqxFiiYhUPV196DAfBgNV +HSMEGDAWgBTCjwmb1fG6xHRel1C7hp2h8frENjANBgkqhkiG9w0BAQsFAAOCAQEA +yrbEinbiFAEvWOpfKPMd3qVzFxPQWi9R+Kd5NAadc47JvbrkA2R7/Cmx80oiob0x +ek4DDfAMsNhAA3q2pSr/eAveSXvuEZdSKt9oU9OIrL3yBCVoBBKP6iYFDZtxdqnN +/5l4RIYHVgQUxtcdY26fB3aVC6Arog3Eef+AwoDLg8PsrkZiuwlxyWUAuGoTpKcx +rf+BlxyEHhbVwmmDiGMtMzFSG/zcx0BcyD4KFYd/gkeNPvI+QzTBj5wWYR4XP0s3 +4aqArYcJy1z+WihNhcpFWG+mq+L3eiTJNCp1uSm4288LcuOJBtZsqZ+C5g+QuRpO +0fEkMnl308+PJ2Tz1j7/RQ== +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/server.crt b/phaoUtils/tests/ssl/server.crt new file mode 100644 index 0000000..08a6db5 --- /dev/null +++ b/phaoUtils/tests/ssl/server.crt @@ -0,0 +1,82 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=GB, ST=Derbyshire, O=Paho Project, OU=Testing, CN=Signing CA + Validity + Not Before: Jul 7 11:14:42 2021 GMT + Not After : Jul 6 11:14:42 2026 GMT + Subject: C=GB, ST=Nottinghamshire, L=Nottingham, O=Server, OU=Production, CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:a9:4c:88:db:56:36:f8:fc:e1:eb:6b:ad:9c:0f: + 78:3e:7d:d7:34:c6:83:94:6d:83:07:d5:3e:cb:fb: + 95:61:e5:73:78:43:db:51:d9:a0:4e:ec:8e:43:21: + 91:1f:56:95:08:47:7c:83:38:90:bb:53:91:ed:fd: + b4:bd:08:27:dd:d6:d9:5b:fd:bb:84:1e:2e:62:d9: + 3c:1d:4d:c9:6b:17:45:d7:9e:b4:a5:9c:22:cd:14: + 41:32:c3:41:ad:8d:f5:2f:a3:d5:59:1f:a1:2b:67: + d3:01:83:64:93:80:6b:bf:5a:b8:51:86:20:a0:e4: + 3f:18:0c:67:19:8d:e3:58:6d:85:83:8f:8b:37:b2: + 7d:21:3f:65:cf:19:53:2e:56:df:4d:89:50:7e:8c: + 6a:8e:dd:21:15:15:31:9b:c2:5c:98:68:1e:31:ff: + c6:6c:1f:a8:42:b8:da:62:dc:ae:62:4c:40:f0:06: + c6:e6:f4:a9:98:3d:ed:fb:c0:2a:63:da:60:69:83: + 11:0e:ce:ba:93:d7:4b:27:8f:86:91:ef:e4:65:5f: + 20:be:04:f2:4d:d6:d1:74:c5:ab:e9:18:df:16:f9: + 9a:8a:ff:2f:23:c5:46:3e:04:16:4e:fa:c1:0a:f4: + dc:8e:1a:da:5f:a1:ad:50:7a:5d:60:00:3e:09:b8: + 8e:6d + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + C3:47:33:CF:07:18:14:7C:9A:E4:AB:11:62:89:88:54:3D:5D:7D:E8 + X509v3 Authority Key Identifier: + keyid:C2:8F:09:9B:D5:F1:BA:C4:74:5E:97:50:BB:86:9D:A1:F1:FA:C4:36 + + Signature Algorithm: sha256WithRSAEncryption + a9:34:8d:b4:6c:99:14:e7:10:dc:36:7e:c2:24:7f:bf:9d:65: + 4c:2b:50:90:13:de:85:29:d7:b0:d5:c2:a0:d6:c2:42:40:9f: + 6a:b7:6a:05:cf:7e:57:ff:2c:3c:ba:0c:cf:e7:0a:92:89:6e: + 8b:bb:0c:b5:28:79:00:76:ed:12:cc:54:79:05:41:88:26:c9: + e3:a8:6b:ba:1a:31:92:e6:40:2c:c6:a9:e8:4b:1b:4c:25:f1: + 7b:c5:19:0b:73:37:53:86:d5:8e:d1:1c:78:73:e4:a5:84:0f: + 49:5a:eb:80:15:09:c2:69:83:34:c0:da:db:9d:fa:eb:32:1f: + e0:2e:99:f2:b0:76:91:8a:eb:34:b5:4d:c9:79:2a:f8:ef:f0: + 6d:55:a4:9d:f9:5f:61:d3:f8:ab:95:0a:12:12:64:33:c3:2f: + 6b:64:14:31:bf:42:c9:c8:9e:be:45:4f:02:c8:50:54:be:79: + fe:e2:9a:fa:2d:b7:73:25:34:ea:53:dd:03:a4:f9:82:28:a7: + 95:37:f7:45:56:21:7a:e6:71:eb:95:34:99:15:1c:26:ac:00: + bc:95:b0:91:d8:8a:d0:0d:98:8e:28:d7:76:14:b6:94:c9:ab: + df:87:40:58:12:da:ee:65:d8:08:f2:05:f2:5e:3e:d2:8d:09: + 38:8f:b2:79 +-----BEGIN CERTIFICATE----- +MIIDzDCCArSgAwIBAgIBATANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJHQjET +MBEGA1UECAwKRGVyYnlzaGlyZTEVMBMGA1UECgwMUGFobyBQcm9qZWN0MRAwDgYD +VQQLDAdUZXN0aW5nMRMwEQYDVQQDDApTaWduaW5nIENBMB4XDTIxMDcwNzExMTQ0 +MloXDTI2MDcwNjExMTQ0MlowdjELMAkGA1UEBhMCR0IxGDAWBgNVBAgMD05vdHRp +bmdoYW1zaGlyZTETMBEGA1UEBwwKTm90dGluZ2hhbTEPMA0GA1UECgwGU2VydmVy +MRMwEQYDVQQLDApQcm9kdWN0aW9uMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpTIjbVjb4/OHra62cD3g+fdc0xoOU +bYMH1T7L+5Vh5XN4Q9tR2aBO7I5DIZEfVpUIR3yDOJC7U5Ht/bS9CCfd1tlb/buE +Hi5i2TwdTclrF0XXnrSlnCLNFEEyw0GtjfUvo9VZH6ErZ9MBg2STgGu/WrhRhiCg +5D8YDGcZjeNYbYWDj4s3sn0hP2XPGVMuVt9NiVB+jGqO3SEVFTGbwlyYaB4x/8Zs +H6hCuNpi3K5iTEDwBsbm9KmYPe37wCpj2mBpgxEOzrqT10snj4aR7+RlXyC+BPJN +1tF0xavpGN8W+ZqK/y8jxUY+BBZO+sEK9NyOGtpfoa1Qel1gAD4JuI5tAgMBAAGj +ezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVk +IENlcnRpZmljYXRlMB0GA1UdDgQWBBTDRzPPBxgUfJrkqxFiiYhUPV196DAfBgNV +HSMEGDAWgBTCjwmb1fG6xHRel1C7hp2h8frENjANBgkqhkiG9w0BAQsFAAOCAQEA +qTSNtGyZFOcQ3DZ+wiR/v51lTCtQkBPehSnXsNXCoNbCQkCfardqBc9+V/8sPLoM +z+cKkolui7sMtSh5AHbtEsxUeQVBiCbJ46hruhoxkuZALMap6EsbTCXxe8UZC3M3 +U4bVjtEceHPkpYQPSVrrgBUJwmmDNMDa25366zIf4C6Z8rB2kYrrNLVNyXkq+O/w +bVWknflfYdP4q5UKEhJkM8Mva2QUMb9CycievkVPAshQVL55/uKa+i23cyU06lPd +A6T5giinlTf3RVYheuZx65U0mRUcJqwAvJWwkdiK0A2YjijXdhS2lMmr34dAWBLa +7mXYCPIF8l4+0o0JOI+yeQ== +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/server.key b/phaoUtils/tests/ssl/server.key new file mode 100644 index 0000000..ff9bead --- /dev/null +++ b/phaoUtils/tests/ssl/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqUyI21Y2+Pzh62utnA94Pn3XNMaDlG2DB9U+y/uVYeVzeEPb +UdmgTuyOQyGRH1aVCEd8gziQu1OR7f20vQgn3dbZW/27hB4uYtk8HU3JaxdF1560 +pZwizRRBMsNBrY31L6PVWR+hK2fTAYNkk4Brv1q4UYYgoOQ/GAxnGY3jWG2Fg4+L +N7J9IT9lzxlTLlbfTYlQfoxqjt0hFRUxm8JcmGgeMf/GbB+oQrjaYtyuYkxA8AbG +5vSpmD3t+8AqY9pgaYMRDs66k9dLJ4+Gke/kZV8gvgTyTdbRdMWr6RjfFvmaiv8v +I8VGPgQWTvrBCvTcjhraX6GtUHpdYAA+CbiObQIDAQABAoIBAFAbWL6AIu7ZqYSd +pL4tS7Y2ETh1nhkDYHa6XkZiuqJh0atcYFBwazwtDnuRTHvJmicavD3S7BjXSDuW +SokPbN25JYwzmSDAry4yoBE1l1LG5lNKUyvxnz3ukZMVdORMQXHTUcYkAzzomZ0j +sNlicJlQsdpRXusCVSBp7fbXfnV+SCRA0JZrMkCmkkQASpzlfZaDYxT+QYzNZ7aS +W4c+YwLEaSyVRPmWdelj17d1XP5RdnsL6Fhho6wNRoT18tgSvvl1cWv+/e+eMGFQ +hxmTJmcBxTTxVDF1+bHIYNHkxHD4OEcrYIP99wwYg9zanO9edxD1OSY5a0xupNns +E9r517kCgYEA0S76Kz0UOmuLnT7KUs6dq5fXq+LJU5Dp6cnMiEtz65VqgBUEwRGn +WNPKDzMQ5SrfNCw6aEpRwPSJOPRoRbFvCZ1ZqHzunjOhssIjGiMMJq+WpckgoX8b +kvzzCpf8DfEHep7PAu/ixKZs5Jm6wliF52dWt6rgYEPK33A4qa9R1aMCgYEAzzBm +gqQ4DZy/ZkUp0GZ1gPJ+wpKJug96Bb7PMMnCtVtcfTtjyR1q7JWaZdli1vCKlMKW +/sOmydD8uPkKhnxy6Ksz5u5/ZCBxkANGnEOc3ED3v7wXHCoiQwsl59lH0yoqx4ua +Ur59L/ZVTMZAjtpci2NTTMN+mezR9LXvQb2qrK8CgYEAjVjs+mKfVIpvIKXZGPM8 +X0KPHTp1R95X8P3HEyHJBptEB6AsQjmnlsIlevfKps+9Wwe3v9jYPUX/o1ijTNSE +bz6/4rXol0XUMXI1PegIwetMJGIvhnDZNQ1vPO1OCC2iHB1LTHTECpVaZ23pYIFo +meCeHCV+0A1+/FRcNWyeI3kCgYBgFnhUOkjstzdk/MqJphr0tIHpRwCs06SpqXZ5 +j/jHFxnr0nFSwlvmYPN8LLdUK7Z5i01v1dkyW8P5HTaubGT2Vv/5J77Y9tr0CTDk +I89Jrq+3skmdfETrhu4Leo9+9V1lse7eVQ3GAp5IvuEN32NwGZ52SWwbguNUdFQD +zyyqbQKBgBj7ltu2L59S03I1rV1Wrm+BFYbsqTZrU2PaA0nz2/mZjzkp+BYqqeRA +y/LBOHiaUxsPZqyR+neOSoDuQK2HWjut5B9JFy61m2pw2E2qwdkpOmceQYuLRRO7 +UAaHfCfkHE9R8k8FePBNB1HwWGGj02BpF5jP5Oph/JyuuQvPPH+M +-----END RSA PRIVATE KEY----- diff --git a/phaoUtils/tests/ssl/test-alt-ca.crt b/phaoUtils/tests/ssl/test-alt-ca.crt new file mode 100644 index 0000000..88e8560 --- /dev/null +++ b/phaoUtils/tests/ssl/test-alt-ca.crt @@ -0,0 +1,79 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 2 (0x2) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=GB, ST=Derbyshire, L=Derby, O=Paho Project, OU=Testing, CN=Root CA + Validity + Not Before: Jul 7 11:14:42 2021 GMT + Not After : Jul 6 11:14:42 2026 GMT + Subject: C=GB, ST=Derbyshire, O=Paho Project, OU=Testing, CN=Alternative Signing CA + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:ca:f5:4f:c2:30:7d:fd:65:75:06:00:22:72:0a: + d0:74:1e:00:03:aa:f3:64:1a:d4:d0:25:85:b9:2e: + 73:72:41:06:12:d9:52:1d:39:11:78:2d:c3:0d:d7: + 6f:06:05:68:3a:cc:ce:36:8d:a7:3a:a9:31:77:eb: + e9:2d:87:00:7e:86:7a:bb:52:c2:02:d7:ef:07:4f: + a9:88:91:d6:6e:dd:19:84:89:dc:72:bb:08:23:b4: + be:1a:cf:af:b8:1a:af:62:21:d3:d4:a2:78:2f:b6: + 4a:44:6f:ab:7f:d7:27:21:79:40:2b:db:bf:90:bf: + fb:cf:a4:fa:8b:25:f6:ad:f9:73:57:41:49:86:1d: + ed:3c:c9:d5:43:e0:ac:8a:4a:88:51:ea:cf:95:f0: + 50:4b:ee:4c:fc:74:1d:92:00:5f:75:97:23:e4:b1: + 79:b1:b0:b8:e1:97:38:6c:78:b6:c1:a6:e7:2e:95: + 39:c8:ed:2a:65:65:b7:09:45:d4:f2:f1:4f:bf:97: + 9d:98:b7:26:0d:c1:cf:93:d1:55:9f:af:39:6f:71: + 29:a4:e9:74:48:2c:eb:8a:11:3d:3f:c4:3c:12:fe: + 0c:d9:c9:fc:2c:77:22:de:c8:bb:8e:05:55:0a:2b: + 18:38:0f:68:5d:2f:26:ea:cc:ec:04:df:fb:54:c4: + 83:3b + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + 9E:74:83:0D:43:6F:21:68:72:E1:A3:FC:E0:2D:C3:D0:47:55:0C:31 + X509v3 Authority Key Identifier: + keyid:13:A0:B6:1F:F5:C7:64:C2:F9:FD:2E:08:F2:19:01:77:54:19:73:7F + + X509v3 Basic Constraints: + CA:TRUE + Signature Algorithm: sha256WithRSAEncryption + 31:c8:c3:5c:31:7c:85:12:e0:01:9c:1a:eb:be:32:f0:19:cd: + f3:55:e8:13:34:27:39:69:ca:88:1e:e9:44:47:9b:e1:bf:ff: + 3f:65:62:02:9f:ae:be:21:1d:03:83:02:3e:a8:f2:d4:fa:e8: + 14:50:e6:53:9e:e1:90:f9:96:5d:73:f3:da:8d:38:33:6d:5f: + f9:ce:9b:60:d3:ae:86:18:7f:ef:4a:d1:69:4d:03:a7:e8:a5: + c4:42:59:50:22:d1:25:bd:a4:22:d1:9c:f9:4c:72:ee:3d:e3: + e1:c7:b0:c2:16:ba:46:4e:c9:29:91:e0:97:52:d8:3c:be:e2: + ef:1c:aa:89:6d:ba:75:35:80:12:5d:5c:33:15:6c:fe:1b:1f: + 4a:b4:1a:12:47:d3:4b:cd:d2:96:61:88:69:ac:b4:3c:d5:be: + 52:7e:a0:99:5a:52:65:6a:86:ea:a7:a2:50:66:48:71:e3:82: + 9f:fc:ff:89:58:ef:04:fa:af:76:98:1b:40:d6:71:14:29:1e: + db:b8:31:47:2b:4b:de:f3:e2:e5:d0:a0:75:1e:b6:d9:32:3f: + 8e:54:c3:92:e1:0f:74:85:0d:e9:27:5b:21:e8:f0:7b:10:3c: + 14:e4:9d:97:65:18:ef:57:ce:de:b9:f7:01:d0:b9:e4:81:7a: + a3:d2:35:8c +-----BEGIN CERTIFICATE----- +MIIDpDCCAoygAwIBAgIBAjANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQGEwJHQjET +MBEGA1UECAwKRGVyYnlzaGlyZTEOMAwGA1UEBwwFRGVyYnkxFTATBgNVBAoMDFBh +aG8gUHJvamVjdDEQMA4GA1UECwwHVGVzdGluZzEQMA4GA1UEAwwHUm9vdCBDQTAe +Fw0yMTA3MDcxMTE0NDJaFw0yNjA3MDYxMTE0NDJaMGwxCzAJBgNVBAYTAkdCMRMw +EQYDVQQIDApEZXJieXNoaXJlMRUwEwYDVQQKDAxQYWhvIFByb2plY3QxEDAOBgNV +BAsMB1Rlc3RpbmcxHzAdBgNVBAMMFkFsdGVybmF0aXZlIFNpZ25pbmcgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK9U/CMH39ZXUGACJyCtB0HgAD +qvNkGtTQJYW5LnNyQQYS2VIdORF4LcMN128GBWg6zM42jac6qTF36+kthwB+hnq7 +UsIC1+8HT6mIkdZu3RmEidxyuwgjtL4az6+4Gq9iIdPUongvtkpEb6t/1ycheUAr +27+Qv/vPpPqLJfat+XNXQUmGHe08ydVD4KyKSohR6s+V8FBL7kz8dB2SAF91lyPk +sXmxsLjhlzhseLbBpuculTnI7SplZbcJRdTy8U+/l52YtyYNwc+T0VWfrzlvcSmk +6XRILOuKET0/xDwS/gzZyfwsdyLeyLuOBVUKKxg4D2hdLybqzOwE3/tUxIM7AgMB +AAGjUDBOMB0GA1UdDgQWBBSedIMNQ28haHLho/zgLcPQR1UMMTAfBgNVHSMEGDAW +gBQToLYf9cdkwvn9LgjyGQF3VBlzfzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQAxyMNcMXyFEuABnBrrvjLwGc3zVegTNCc5acqIHulER5vhv/8/ZWIC +n66+IR0DgwI+qPLU+ugUUOZTnuGQ+ZZdc/PajTgzbV/5zptg066GGH/vStFpTQOn +6KXEQllQItElvaQi0Zz5THLuPePhx7DCFrpGTskpkeCXUtg8vuLvHKqJbbp1NYAS +XVwzFWz+Gx9KtBoSR9NLzdKWYYhprLQ81b5SfqCZWlJlaobqp6JQZkhx44Kf/P+J +WO8E+q92mBtA1nEUKR7buDFHK0ve8+Ll0KB1HrbZMj+OVMOS4Q90hQ3pJ1sh6PB7 +EDwU5J2XZRjvV87eufcB0LnkgXqj0jWM +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/test-alt-ca.key b/phaoUtils/tests/ssl/test-alt-ca.key new file mode 100644 index 0000000..334cc21 --- /dev/null +++ b/phaoUtils/tests/ssl/test-alt-ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAyvVPwjB9/WV1BgAicgrQdB4AA6rzZBrU0CWFuS5zckEGEtlS +HTkReC3DDddvBgVoOszONo2nOqkxd+vpLYcAfoZ6u1LCAtfvB0+piJHWbt0ZhInc +crsII7S+Gs+vuBqvYiHT1KJ4L7ZKRG+rf9cnIXlAK9u/kL/7z6T6iyX2rflzV0FJ +hh3tPMnVQ+CsikqIUerPlfBQS+5M/HQdkgBfdZcj5LF5sbC44Zc4bHi2wabnLpU5 +yO0qZWW3CUXU8vFPv5edmLcmDcHPk9FVn685b3EppOl0SCzrihE9P8Q8Ev4M2cn8 +LHci3si7jgVVCisYOA9oXS8m6szsBN/7VMSDOwIDAQABAoIBADVkjcP/b9Wu0Ddw +557q22YA0m4klf061cugY2qRHsvq8UcaJvELJ15fY5YLm+iQmZgGcyWE5H6ZLitn +Q6O3hVjD1hvbrLCE0BwzR91myGvH/MOSZQ1FyOFj1jNFeevMEWGWlpy01TtwEF+q +pQpvtpqmxEwFdoMFDqDUvRjINvoTUn1zijLA/tujEwHDriSjQPNd8/RcxYONQaAu +3SLInf7Gp3cJJ/EbE+MyK+/DpiG6kQ8Xkxdq928XtSpQAhxVjBzXaZR+hIXu+9jK +884Avl/TqRwKoMpLQIUaSVz4F65Hprz1y+Jo28OZ5x+l1oicdnWPTbNtc2xqWcQ1 +3p0lO/ECgYEA96OAHgRwFwUwOmm9CDbAiYX4yssa7gmU1GVEJukqVwSb95M2Dff8 +SgkhBABIHcPsYdNAi0klvJv6VqxRHC0dvGi3y0MFG+VdihtpOEh7GrH1aNyUohS0 +Mx0p6fZjR2mLjCfdnTVD8mtGT97bTrkOP8jYe6r5ZpCE5V3fVHQif9MCgYEA0c+c +DejT1uQMq3wQm5NwusRrMO6Eo/VOEJ22nkXNKC5kQEs8kXN8nkRdGCMznQY2Iurc +MxhFPa1mBvYGVyefZFLjHJe1rWD0zujmjdjZ3cj9h0O5jf0vefQGuP5uteCtmUoj +81eGXfRac/ntEdFQLNEv9PSvBRZpup7e/koStfkCgYEAyEYYtS4NoPB3QqaFVIFD +UXVh8lA0ZVKmZOfJKFbmAR4fLSiHTODDzvR3GQ9JQ5lSMQNybbMoq9LRsQsHRexO +4jMmgWKgXSEwdyMYA4bK2JoXyUirhDGOUtBBN5AmVnjLfPw4xI1xeDq90JaBcrdD +CN7cBZgOv54dfIpgtaJ+zDUCgYEA0CzQSDTVzAgmUhgdWmAmoAm32asvzIbe2DnE +MrJLZyzwp6J/DEqsQVTPkd2LnqfFG0wxBDl2qkXcT9fYXq2fxyk+0uXsi4UCIjKQ +X/nj4d1FQOr/t1SZwMVRzkgVjTzKwqf/l7kmRx7miOBYSy+F/5HnpYMKDWA5s8Ni +uqjAe/ECgYB/ew66RJjRiAxg5DnErIw6RX3lblHuK9tZ3uwyxVLev6wJnps1X4Ar +m1WHFGgOGDDqOfC1n7JBp7qvWfLtr93aMlcSUPp34XItBr4LMgPAhk4irb079aoJ +pCCx0JV+8ydFi4QaQcu4BRaed9T9PITf+qqTMtyXy4Q4QolV4rqiDw== +-----END RSA PRIVATE KEY----- diff --git a/phaoUtils/tests/ssl/test-bad-root-ca.crt b/phaoUtils/tests/ssl/test-bad-root-ca.crt new file mode 100644 index 0000000..9666151 --- /dev/null +++ b/phaoUtils/tests/ssl/test-bad-root-ca.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwDCCAqigAwIBAgIUZQzHgL/r7su5tQBXIZvwqj+HdqIwDQYJKoZIhvcNAQEL +BQAwcTELMAkGA1UEBhMCR0IxEzARBgNVBAgMCkRlcmJ5c2hpcmUxDjAMBgNVBAcM +BURlcmJ5MRUwEwYDVQQKDAxQYWhvIFByb2plY3QxEDAOBgNVBAsMB1Rlc3Rpbmcx +FDASBgNVBAMMC0JhZCBSb290IENBMB4XDTIxMDcwNzExMTQ0MloXDTMxMDcwNTEx +MTQ0MlowcTELMAkGA1UEBhMCR0IxEzARBgNVBAgMCkRlcmJ5c2hpcmUxDjAMBgNV +BAcMBURlcmJ5MRUwEwYDVQQKDAxQYWhvIFByb2plY3QxEDAOBgNVBAsMB1Rlc3Rp +bmcxFDASBgNVBAMMC0JhZCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA3DRlR+CK8ZBUfaZB4RzWErQ+lewTPu+FaQuCSBvBMKgd+S0r/mZ0 +dQsA2/mWymggxb1wIZt/TY9sz2v1pYmg2Cw/dld9AQvJaqMXPdn3ZAmsihYd3is8 +M4c0FlFowHv0LyWUOlRJfUrAPc4aorRK4Dqssl+s8W/ikyiKsMKBk0Z1LQBxUzst +AAQ3voBJW7SVsRzYgcbyITW2IXYBjsIJRWK68+TCNCqlmVKEKvg6DYFJ+1HLE/z6 +jFmzb10lXgg4FKKkUtWruawkErUbb8k1le+rnjZ0Wi9FhSWdM3HL1l6NX0IMWRmC +Jm7WvXBHo9KCarcp+MWnKEjP4b/gR0bEvQIDAQABo1AwTjAdBgNVHQ4EFgQUc9Tb +8nwTWl+HI3JbYIQAFL3eZYgwHwYDVR0jBBgwFoAUc9Tb8nwTWl+HI3JbYIQAFL3e +ZYgwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAnhmnsmFDcZ7b0YEQ +XlIj760EUHe/G1Rtur2fjjP59qz/8msP9QtVAy5O/a22aCBehhOzcK2e3NFIKlBs +D5xb7UE8RV0i9btP57+ZF6kP89sMB/DBHI+TDD93cms5OvTDCteKO++CpwnkNmav +xRnvQGAAOA+zxVsPlYL1Wy9Z75LQWdZKS68/JTd7b2LOQnYD2qp4omPYEYGAFtFz +38EMgRS/QyQjjiHx6rz/wU5hmQCrNOUUCw+bHumZL3mxJ/aSBNrGVBLQ2Hnofhsw +1Ik2EyzMh3+nlf2ImlSZKfjfg8PrfmgbvXvNc8AWRCad7xwt9ZPzaxj05vKupcwO +2tIkOw== +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/test-bad-root-ca.key b/phaoUtils/tests/ssl/test-bad-root-ca.key new file mode 100644 index 0000000..8162c2b --- /dev/null +++ b/phaoUtils/tests/ssl/test-bad-root-ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEA3DRlR+CK8ZBUfaZB4RzWErQ+lewTPu+FaQuCSBvBMKgd+S0r +/mZ0dQsA2/mWymggxb1wIZt/TY9sz2v1pYmg2Cw/dld9AQvJaqMXPdn3ZAmsihYd +3is8M4c0FlFowHv0LyWUOlRJfUrAPc4aorRK4Dqssl+s8W/ikyiKsMKBk0Z1LQBx +UzstAAQ3voBJW7SVsRzYgcbyITW2IXYBjsIJRWK68+TCNCqlmVKEKvg6DYFJ+1HL +E/z6jFmzb10lXgg4FKKkUtWruawkErUbb8k1le+rnjZ0Wi9FhSWdM3HL1l6NX0IM +WRmCJm7WvXBHo9KCarcp+MWnKEjP4b/gR0bEvQIDAQABAoIBAQDPcKSAo60Ah4Cw +pXCmSm34TMgwn6Y5wZYiMO9YUp0Z4yXpWH57N7U5lVYH5AYDQzisTxtU7ZFtVVGh +zQgqG47kVjqqlxxxYdMqm90HLVB6cwqRQuh8JKqfuBx/cc2Glr6fs30BvelFGKgl +EQXShJmMxnltx+e5wjblfmm4vmMmgpf/I3ROVwaPCrB0Zu0zWBqNDk++te7jMqkG +uoBQ9Zv/C93gejFUktzKEMkXUAVqKLlwXlKPc2ypzMW15Omu7YcAo+ZWZIDeQ3RU +HzH2zJylVp5F/v9nQbHU5G+8RzIwiVewwEyU2z3wve7Z+9UBA2hf0M3q6NfgYDx0 +UDgaZzzZAoGBAO/vHy027FpULbfX93KZ9AMBjo2BrnDtOQCdL7qHkyov4Y1O4hH5 +aPBdeijZQhzJlyNF+0bB0Qht26YzQx+vfkDMIcolvLAbYLNivRNaxNElpE4eZweD +T7qfhRahyrBPoKikzvIIQGScqYCmPar0fQ3CfIi0+a7GDlNQ30JsifHTAoGBAOrz +FBBLzAlwS+YiKh/734xWM8l1L/4RVyT8pqW4lNCNP9di8oJj7EEVvXUV4VoA1uNS +goyoED8OuKLhlGwE+RXq0hRGxyIJgbU08UwV6zEfAAYg+SiYI7t3oEp1IP9p7vZ9 +5LRfQyO1U6fxpub4l+tRdA1zGxVQNQJ7FJAcNMUvAoGBAJ8pDpNdxbe9833q45jA +C6Aa3kd8aQ08L/36R3kDClqH3KVyWID34+be+3QxeqvCBmI9wAwV8eYXigdcJgDU +13mAcEG6esqPvrwAmdBG/ByJTc8MV+gh8TepLg3vUZdXmwmEGktvsdeMHNzcajgH +axU/mIDPHHoVo9cc5J0ZhwBFAoGBAKpiQ6+ZuEs0A+bN6eyt9S1Jql6zvG0s2By7 +mILf/BPOC3lAiYvjuQZuJKoPhxCFQVEzmfc1PirsmxuMKd24MYcidt07gtf9OvJV +hZPe5WQHDjZjnS1CP8+I7lZw4NA5W5GoNL5Vw1PXAObvSVGBAHMn69iBHCf1taup +5Hyp598DAoGBAOhN1mSzSeyddtJiidy2ByL5PL/6BygNxXI//vXRZHmBugLWLczI +qtzyUBPMXl1AdxusDkRuQgIgmrums/szsVVgzjJcZzSlxoktbHs1JphxgTTTu7Mh +Z1KIaFjXkGF+rRat8rkmy6BVi/PpoPHIWNvEdbR5JZ3jzpPfAqVkvrvO +-----END RSA PRIVATE KEY----- diff --git a/phaoUtils/tests/ssl/test-ca.srl b/phaoUtils/tests/ssl/test-ca.srl new file mode 100644 index 0000000..c40966c --- /dev/null +++ b/phaoUtils/tests/ssl/test-ca.srl @@ -0,0 +1 @@ +CDAE0E564A2891A9 diff --git a/phaoUtils/tests/ssl/test-fake-root-ca.crt b/phaoUtils/tests/ssl/test-fake-root-ca.crt new file mode 100644 index 0000000..490d8e5 --- /dev/null +++ b/phaoUtils/tests/ssl/test-fake-root-ca.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIUcE+qUkqyZKFChp9j3+SuflCAAwgwDQYJKoZIhvcNAQEL +BQAwbTELMAkGA1UEBhMCR0IxEzARBgNVBAgMCkRlcmJ5c2hpcmUxDjAMBgNVBAcM +BURlcmJ5MRUwEwYDVQQKDAxQYWhvIFByb2plY3QxEDAOBgNVBAsMB1Rlc3Rpbmcx +EDAOBgNVBAMMB1Jvb3QgQ0EwHhcNMjEwNzA3MTExNDQyWhcNMzEwNzA1MTExNDQy +WjBtMQswCQYDVQQGEwJHQjETMBEGA1UECAwKRGVyYnlzaGlyZTEOMAwGA1UEBwwF +RGVyYnkxFTATBgNVBAoMDFBhaG8gUHJvamVjdDEQMA4GA1UECwwHVGVzdGluZzEQ +MA4GA1UEAwwHUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AJ9Oc5kDGXyzORIRKBNiQ0+p63LN4DUK8ux/PMD7SEuGKq+LIdieCjmExUq2nkIE +052iYCM9aYJa8PuP+8UWl8UKE4/QWW6yWl28/O5n/Hwe28PfiiH1f5kxXt9khMjI +oc3WCr8YkiDrrKiFyCGvF58b87woQFRMHHqus+o+Xd9YPKhsc/n/AhV4zl0S2wUC +nnV+UF5c+/vlMh/SnD84yhMlySOC7fRNHziAJqqIpj44hQTdfjM6XDHOf3jSlHfv +1JxKqyE8hAWxZVZhMBP1v14xQL5AbVhtSNZlIV/LzAGUbBztMKzPfE4GyQKIDCmi +91A7nbXbkTBz/McL3kxmmAMCAwEAAaNQME4wHQYDVR0OBBYEFDZWdWv057drndtK +9AYPQhTgHkUuMB8GA1UdIwQYMBaAFDZWdWv057drndtK9AYPQhTgHkUuMAwGA1Ud +EwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEuWZu5tSJyNZkzbFT0o/IP8zUgN +lbfp9DxqqiOwUTx2ykOpMsXU94f+/HEACYQ773G/lXPDmMrz/j3mPYklhws07/SE +q1jKM6ZXJh74nQekypvXtSY/Xd667JpRxU6GAedizi60owKPIUFpxkW70h4Of5j5 +Py7PGRGDZ7ItGtuk1fxcSCchfm0Q2bST8nOcD8D+MQcttNxGgelp2V6c0XckmijM +oFUy/3Nm1B/qv4QWckmVX+gm+iBTBANItvcj+ie2c6diFwz7htDwOVm7/1Z/73wM +YyM5Z27mVKR8FwK3jHHRQa5VWtTOdnqG3kHKmAKeOlXgO91wzCh8zq1Q76o= +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/test-fake-root-ca.key b/phaoUtils/tests/ssl/test-fake-root-ca.key new file mode 100644 index 0000000..8897d10 --- /dev/null +++ b/phaoUtils/tests/ssl/test-fake-root-ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAn05zmQMZfLM5EhEoE2JDT6nrcs3gNQry7H88wPtIS4Yqr4sh +2J4KOYTFSraeQgTTnaJgIz1pglrw+4/7xRaXxQoTj9BZbrJaXbz87mf8fB7bw9+K +IfV/mTFe32SEyMihzdYKvxiSIOusqIXIIa8XnxvzvChAVEwceq6z6j5d31g8qGxz ++f8CFXjOXRLbBQKedX5QXlz7++UyH9KcPzjKEyXJI4Lt9E0fOIAmqoimPjiFBN1+ +MzpcMc5/eNKUd+/UnEqrITyEBbFlVmEwE/W/XjFAvkBtWG1I1mUhX8vMAZRsHO0w +rM98TgbJAogMKaL3UDudtduRMHP8xwveTGaYAwIDAQABAoIBAG4D1KsHy/MlJjWG +6aExTADY/MOkz8Bx1j9iw0cWgd++QP5H3FDnG3KLcWBeaz52bNnAyBmuEI44VZG0 +5o8+QgOOKOI5ZXmf6+4uVJIj9+aTvPsxBgjbrInT4YvutBChFbS7q2I7Crd3ah5b +fVFdxLdZq2H2fi54/XXv7knHVjaldxf/mlq3XX1ndAvYXIY3L9PKjeeraEppRgce +oZR6nnzliz7mBwIezaWV+DOCpotiJVYefeWsbN1QjKKzObnq1M5w4fv1R4jbT/zh +RKIyxL3sa/8Beo3TSl4hFF9xNbQq957QdXKMqbdKdGWO0bQN4Mh4xqrEPo1ZK6qK +RLyt5xECgYEAyvrgICVB4q7VFIqMzIznLnrBSg+HtpkLBIh2JjovWEQNh88Qul3t +IH9VdOVT+SPeLCjED6vwQzU4bu4TJV2xwnv+Ujty4w8Aw4sSJlxSrMniKkdSxMus +yhNgYg8E4WEDHxGtBNTyGc1lC2rvfDorvQAFajj5WJqGXLB9MumgP9sCgYEAyOso +nZlfGKSWidUT+Mp0Jq9PG1kmAoBDEoMdCcpvp5p6ttUAb6sLVoY9Q+7U4VVVUIbH +udYBvpDklgwJD2Erc6PK81g99bS/0fTuqCMlCGfDrqVTFxtWcYd9H2E3eJfo20YQ +lUKgoOudXrlc7/a1TSK4Z0qGnWrygyhYypSwNPkCgYBKMv09IwF7sPd5g9BGcfeM +eRkxTo4IxNdPN+cgwEJQXMgpbhsqVW16ZLHDgpV4zJDJybkqFWtF1i2j92mOTjrN +4m+sdcjgkbpwwOTImxUpzr7bP6lVATNPx1eDYQQis0jl0ZtS2dkKb5fRXazf14/n +jhtsohkcN5iIR4fs1ZRb4wKBgQCtu1HCfOVS7LbS9jGv1nf7H2na7wpD7V6R+le4 +qJhFp/lmcOZQqOlD5w3A2RqwwdXkrLa1RYz6mFVgPYX0C4TEGKScKPhipumbBhz7 +vHAARaFaOdCQUW48+vhBkxGhMFIEkSAzwIoeu723M7deM8jvqw8jGbkvE1Qh/1hP +y6RWGQKBgQCNfn28PybCmShtMFXnmcbtYOfI9b7ycGqFiKcW3pT5Q/2C4ReAyEVH +uZ7xXApAzESao5V1evp2jRYGQAhK00YX/F9CXn8C57K55B5EC5cNu7LVSA81GswF +/9VRFpxIWzilLEUGmgA0rUfvgsUyx6ILREhD1Qw8ihWNKMO2gJxVog== +-----END RSA PRIVATE KEY----- diff --git a/phaoUtils/tests/ssl/test-root-ca.crt b/phaoUtils/tests/ssl/test-root-ca.crt new file mode 100644 index 0000000..c0f092b --- /dev/null +++ b/phaoUtils/tests/ssl/test-root-ca.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIUS1Q+E18/+trcKfhT+xz8ghGukmYwDQYJKoZIhvcNAQEL +BQAwbTELMAkGA1UEBhMCR0IxEzARBgNVBAgMCkRlcmJ5c2hpcmUxDjAMBgNVBAcM +BURlcmJ5MRUwEwYDVQQKDAxQYWhvIFByb2plY3QxEDAOBgNVBAsMB1Rlc3Rpbmcx +EDAOBgNVBAMMB1Jvb3QgQ0EwHhcNMjEwNzA3MTExNDQyWhcNMzEwNzA1MTExNDQy +WjBtMQswCQYDVQQGEwJHQjETMBEGA1UECAwKRGVyYnlzaGlyZTEOMAwGA1UEBwwF +RGVyYnkxFTATBgNVBAoMDFBhaG8gUHJvamVjdDEQMA4GA1UECwwHVGVzdGluZzEQ +MA4GA1UEAwwHUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AKpCq45dCrroNa+y3zgdBglQOtw4og3MD/3Rn6ZftyL0dv1rSMkCFU8lCtZ4bIpz +iNSJKau79owCudX3qQTPfiX2pmR5uuYjvMzRiZohZtz5uqXByy/CMS8dPRI3po6i +kfNx9n7EQqOlxdwkY1kae2j5ybkAld2MNci93BH4P8qqaQckVRKpv6cKq33KsXK7 +jHgjAYMGrihTAwxgP1JX9NS8yxxjMUYvFqeEOLARoeWc6Nl7oDbGLs2fr0j2Yssm +cz0AMu7LWcbhnfs2S8Troksztnq38yHu+YTs6hX4NhANBgon5CAdyzmmE/b2OwOX +p8rQepUfG7wO5QaS0OrAEXsCAwEAAaNQME4wHQYDVR0OBBYEFBOgth/1x2TC+f0u +CPIZAXdUGXN/MB8GA1UdIwQYMBaAFBOgth/1x2TC+f0uCPIZAXdUGXN/MAwGA1Ud +EwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHgE1oMwIcilQFN4xPYCf8jbsa5o +zA5ljTbxv7fU3Zd+7KdlDFYroGjgHb7o3r0//b8+ZarxBqn1274u4KPs39Ow7h6m +YJo7IM2Z2fC6IWZroqeidfFx5SwejAP1j7coYLblTIbNF+P08sJG5nSQ+Yx0gams +6C1x0mETaaglDwllU1KXHTm8fUpEwpISc/VfKABYgScODMpdsDghyHANvnFjmvp4 +ktABnasliZYTmdl0t3szNm7zIk+bntiK4KunFea8GqgslWqGPwtNxxJFHzPjMCxK +EHgubLgp1lNZzH13XSO6ZpiNRDJ6IVed3Zq+yn+24uKH+1Hqp6Bt20ZFB4E= +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/test-root-ca.key b/phaoUtils/tests/ssl/test-root-ca.key new file mode 100644 index 0000000..0d7b3be --- /dev/null +++ b/phaoUtils/tests/ssl/test-root-ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqkKrjl0Kuug1r7LfOB0GCVA63DiiDcwP/dGfpl+3IvR2/WtI +yQIVTyUK1nhsinOI1Ikpq7v2jAK51fepBM9+JfamZHm65iO8zNGJmiFm3Pm6pcHL +L8IxLx09EjemjqKR83H2fsRCo6XF3CRjWRp7aPnJuQCV3Yw1yL3cEfg/yqppByRV +Eqm/pwqrfcqxcruMeCMBgwauKFMDDGA/Ulf01LzLHGMxRi8Wp4Q4sBGh5Zzo2Xug +NsYuzZ+vSPZiyyZzPQAy7stZxuGd+zZLxOuiSzO2erfzIe75hOzqFfg2EA0GCifk +IB3LOaYT9vY7A5enytB6lR8bvA7lBpLQ6sARewIDAQABAoIBACAK+BqM7C4M8b2l +XllDLRWnocw8ZFNQaloMj41SSjcr5xD+le4ulDAW+pkuhM7xu3i0b8FAWMA06yCX +wZmEK2udpecW+dPCOhAaB1mYm7FO1o/HjyPn2jXRvOKm0pPZiLpWYlutOBVwZ3Js +7r2gPEWfbRWCRLIzZxPml3pSTD8p0IMGC4gO0jKHmGyLFQN0TOCdivVOzbQeDpUU +lpj/v2wCfQQpfc/jP2bwTlGAZWVgmUtoj5XcWtRSLtwcWtK5KKgyQKlDdDL7d/Et +J3x+QDLIwu9JNfaW8lcie16Y6qE4yOuBl95wfOpxN3wcmfxrKh7/rtN0Df1JNxvh +4bkyrGECgYEA2Lc36y+S9fOEecTwQd+AozIrBVhfaDU3L/tuKqKRJAhjI37DHkZ3 +4tRYqd85bAcd+FED9cEK7Fqb51XovHTYQx0j3y++Iq6u+gzGWgSX2JGlprOkiNMk +oXMX9P48KDBtCzD2aPxAslmrkhIPKEKmW+OTqpqHG6TsCshUoF1GQYMCgYEAyR+t +A68mrnEcR3iapGhnnKqAEVx4zRdaXhBXFZvC0mF15xKtMTtjCEaT2X+iOiZE2fNn +Si++pi/UGgLYChD7YsgWQlJUyrMVUHBYROfZ+sUIm9XvESVNQFLSSkr+vMH03hM3 +I7d4Z3pbMEwDzAnv37i1HZ91Tvm4nfIsePenRqkCgYAdWdMs+yiAPxb2FwIjKc4W +TDkfZDSnvG1ZBkiJZbMamjgzGnv6obii8/d+KklwpBYfB3nt0tNT54Gt9yiqPXj8 +vfmZxLGPqPDx1MEYd/7IyhERXsst7MrNQvU/rR8gok5icaMt3Nw2S4a9Jcz/uucl +EtFxDbS2vcNqQm+TuI5HWQKBgQC1xLj7IWsWMQfb2DX67Jjn0HhaOHa89KQpax8p +WlKjDI4gPpLkccW5DwBEi8O0Ri3nxMHPHINzcrqAn51cy6hGyIrFed9EKsHSpxY/ +gENTDowPOzQLDOlafv+rQUgklC6YHkmxL/nTm5OafLjZyQlP6oFVum2s6KhfpyVm +VnyJsQKBgBk2hykG1EZmkLDKbrCXU7ggTvA9/FhEAjOt5PtQBjdKWTInuoizWX3n +/C8ZYig7pYNytsb2um4CrF1Divgqz2ZceTWxfEF5IKqjqfhwMkBZ/uGz27t/BFaF +pE5RD8iBhG+1inxV2UVz0gzBCNGciDxKb+ZPW087yE6NphLRydHv +-----END RSA PRIVATE KEY----- diff --git a/phaoUtils/tests/ssl/test-signing-ca.crt b/phaoUtils/tests/ssl/test-signing-ca.crt new file mode 100644 index 0000000..396b7f5 --- /dev/null +++ b/phaoUtils/tests/ssl/test-signing-ca.crt @@ -0,0 +1,79 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=GB, ST=Derbyshire, L=Derby, O=Paho Project, OU=Testing, CN=Root CA + Validity + Not Before: Jul 7 11:14:42 2021 GMT + Not After : Jul 6 11:14:42 2026 GMT + Subject: C=GB, ST=Derbyshire, O=Paho Project, OU=Testing, CN=Signing CA + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:cb:32:6c:8c:48:e8:44:58:36:18:70:36:42:3d: + 2d:29:47:3c:69:12:9e:7b:f7:45:62:ef:91:44:46: + 97:a0:ea:5f:da:fd:9f:98:d4:bf:43:02:e3:39:90: + 33:7b:13:13:d5:31:30:9c:07:fc:ca:1b:a9:e4:89: + 42:e5:d0:6e:f4:a2:e0:23:ee:9d:9a:cc:80:3b:78: + bf:7e:27:a8:46:1b:28:9f:4a:64:53:7a:89:3e:ab: + 65:6f:af:0b:29:fa:4d:4f:04:f1:1e:10:2c:bf:2b: + ea:fc:c5:fa:77:c9:1a:7a:78:29:f5:a2:cb:25:7c: + 02:bb:91:8d:76:4d:23:bc:9c:19:da:be:c5:20:04: + ad:fe:bd:b9:d4:bb:29:2a:c3:e4:fc:4c:84:db:a3: + 55:9f:f0:70:7f:40:38:b5:c3:78:a5:db:06:36:b7: + 10:8e:ca:6c:1a:92:66:be:0e:1a:97:59:6b:18:f4: + c2:b8:c9:31:7b:d1:b1:a1:00:78:7f:c0:09:f6:ef: + b2:8f:94:87:5d:b1:a2:23:93:4d:ec:fa:95:09:a9: + 90:c4:02:f0:1e:d9:ab:a2:8b:7f:7f:54:95:e7:da: + c3:c9:7d:a7:d7:04:89:59:db:88:9d:57:16:5d:b9: + 66:b0:d6:88:bb:e0:ee:43:e9:ab:02:78:fc:bd:e8: + 98:d9 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + C2:8F:09:9B:D5:F1:BA:C4:74:5E:97:50:BB:86:9D:A1:F1:FA:C4:36 + X509v3 Authority Key Identifier: + keyid:13:A0:B6:1F:F5:C7:64:C2:F9:FD:2E:08:F2:19:01:77:54:19:73:7F + + X509v3 Basic Constraints: + CA:TRUE + Signature Algorithm: sha256WithRSAEncryption + 3e:70:76:69:37:e4:6e:e0:08:c6:8e:5b:2e:aa:26:fe:e9:ed: + ac:02:ce:2c:37:08:6a:8a:c3:0d:c0:ef:43:51:01:2e:e0:96: + 76:23:1b:1f:75:98:df:7c:d1:b7:c1:67:aa:62:c1:bd:ef:84: + eb:d9:28:47:50:f2:1b:54:7f:ed:cb:52:f7:fc:c3:f8:62:22: + 0c:b3:95:ed:bb:3f:74:91:bc:d2:eb:c0:81:7d:74:12:85:61: + a3:7e:fb:22:4a:25:99:0b:5d:ef:69:f2:5a:e6:d5:12:a3:95: + 38:30:0c:c7:d9:da:28:30:10:b4:3d:3e:ad:20:85:31:e0:bf: + 30:33:2e:0b:e3:07:3d:ed:22:dc:67:f8:93:64:89:ed:e7:08: + 74:b5:0a:7a:01:3d:f9:44:62:71:cf:60:12:92:c3:95:9a:e5: + a5:f2:24:6a:22:64:d5:76:22:c9:03:1c:c5:d1:a5:85:4d:55: + f9:80:47:ca:12:20:df:05:fb:82:12:45:6f:e8:c0:20:a8:ae: + f7:17:c5:c3:b6:9c:51:bd:d8:84:e4:db:c7:03:44:d2:cb:75: + 51:79:3f:86:33:3c:e4:34:1d:77:b2:60:24:5c:21:c5:c3:53: + 36:08:2f:a7:14:0b:68:78:67:95:90:b9:06:0e:85:04:65:57: + b4:34:31:cf +-----BEGIN CERTIFICATE----- +MIIDmDCCAoCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQGEwJHQjET +MBEGA1UECAwKRGVyYnlzaGlyZTEOMAwGA1UEBwwFRGVyYnkxFTATBgNVBAoMDFBh +aG8gUHJvamVjdDEQMA4GA1UECwwHVGVzdGluZzEQMA4GA1UEAwwHUm9vdCBDQTAe +Fw0yMTA3MDcxMTE0NDJaFw0yNjA3MDYxMTE0NDJaMGAxCzAJBgNVBAYTAkdCMRMw +EQYDVQQIDApEZXJieXNoaXJlMRUwEwYDVQQKDAxQYWhvIFByb2plY3QxEDAOBgNV +BAsMB1Rlc3RpbmcxEzARBgNVBAMMClNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDLMmyMSOhEWDYYcDZCPS0pRzxpEp5790Vi75FERpeg +6l/a/Z+Y1L9DAuM5kDN7ExPVMTCcB/zKG6nkiULl0G70ouAj7p2azIA7eL9+J6hG +GyifSmRTeok+q2Vvrwsp+k1PBPEeECy/K+r8xfp3yRp6eCn1osslfAK7kY12TSO8 +nBnavsUgBK3+vbnUuykqw+T8TITbo1Wf8HB/QDi1w3il2wY2txCOymwakma+DhqX +WWsY9MK4yTF70bGhAHh/wAn277KPlIddsaIjk03s+pUJqZDEAvAe2auii39/VJXn +2sPJfafXBIlZ24idVxZduWaw1oi74O5D6asCePy96JjZAgMBAAGjUDBOMB0GA1Ud +DgQWBBTCjwmb1fG6xHRel1C7hp2h8frENjAfBgNVHSMEGDAWgBQToLYf9cdkwvn9 +LgjyGQF3VBlzfzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA+cHZp +N+Ru4AjGjlsuqib+6e2sAs4sNwhqisMNwO9DUQEu4JZ2IxsfdZjffNG3wWeqYsG9 +74Tr2ShHUPIbVH/ty1L3/MP4YiIMs5Xtuz90kbzS68CBfXQShWGjfvsiSiWZC13v +afJa5tUSo5U4MAzH2dooMBC0PT6tIIUx4L8wMy4L4wc97SLcZ/iTZInt5wh0tQp6 +AT35RGJxz2ASksOVmuWl8iRqImTVdiLJAxzF0aWFTVX5gEfKEiDfBfuCEkVv6MAg +qK73F8XDtpxRvdiE5NvHA0TSy3VReT+GMzzkNB13smAkXCHFw1M2CC+nFAtoeGeV +kLkGDoUEZVe0NDHP +-----END CERTIFICATE----- diff --git a/phaoUtils/tests/ssl/test-signing-ca.key b/phaoUtils/tests/ssl/test-signing-ca.key new file mode 100644 index 0000000..5bb7838 --- /dev/null +++ b/phaoUtils/tests/ssl/test-signing-ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEAyzJsjEjoRFg2GHA2Qj0tKUc8aRKee/dFYu+RREaXoOpf2v2f +mNS/QwLjOZAzexMT1TEwnAf8yhup5IlC5dBu9KLgI+6dmsyAO3i/fieoRhson0pk +U3qJPqtlb68LKfpNTwTxHhAsvyvq/MX6d8kaengp9aLLJXwCu5GNdk0jvJwZ2r7F +IASt/r251LspKsPk/EyE26NVn/Bwf0A4tcN4pdsGNrcQjspsGpJmvg4al1lrGPTC +uMkxe9GxoQB4f8AJ9u+yj5SHXbGiI5NN7PqVCamQxALwHtmroot/f1SV59rDyX2n +1wSJWduInVcWXblmsNaIu+DuQ+mrAnj8veiY2QIDAQABAoIBAQCh3tl6J9pgF6WA +cmPHANUpPQZy7dIzDxjHZ/FhYpsIJa2W1tR8+34h8/rvsGBSezAhdb4zjmli2AbP +eElCqni5icbk2QHUf3Tn65kg9pamwpvpyWmC1ureccus3NUX674KZPVv7ZK3+FSK +aWzOX/Yn+fHzLGyIv/GtWpZG18zQQl0+i2sKqKYQu00qBBLp+t9GJYpqPx9qpumB +JzWaVuMEwkTJ4J6j7d28V8r8fnrURIcb/3R6dZsB6QtjgnJNzJRwFpaDDKoH3ZNV +IkMqRNGuzuuzhL5Rzd2nd8oRUvgUAl93ad/fxWfmVVSyVbh/LCkOBDwSC6Z2Ri4c +BafNAoMBAoGBAP0E9y121/lMkiLh0KF3suwbSJ/Z/GFO9ZhP5wW2vibcrQppF6vz +kdYyEmjyPH+3UoKZOYzAwkTDJFJaosaagmiPuIzsGeF20A7D7ZQ1Ru5ScxVo/XWK +i2g4s4wqlEZ9vhlcg/QBOUfzi23lUyGAXuQlgORQtzbN/vzGSRkIivWNAoGBAM2X +NU32Hw0RuOXtaw4aZyY4oFT2nyPP99fAzR+IGX7XMny5bt2kBnHExpC43VZevFHc +qzQdot4DbOUi/kO+LOiUHcYIW2/nADJjsxnDlnxU+9L6cMv74rQgskjNgjS0l+bx +/W6/QFoOOJeVDT1VXhQbjIL3PxffdKmEWPs7ZT99AoGBAKZ6SuymIorMv+alr/Fd +4eMKPKm48x9Ppba29CnFSK4nSs/rwACKva0yuvxETlw2UdrOWJhtCCXYRCDPtAR7 +C00jK2nFu22nEFR2w+5dc7NBmqk+sG5TX1CO5kxWg8Mx3w+u2L+Gwpq9+0Kuvhjv +7v+sUXdoSHSN67WD/fqzrULNAoGBAKv+Qvbc329Ek0Wv0K70wbSFDQTnaY1BT9us +jS5C4ultSOx1CV3c+hM1htTOA0VdbfiiPowT+wv3G6O6GbM8pz9PonToyu4b99sv +80arjPqo8h+3qqPMLwV4kQ489x/2sVngup9q2oA8g3W0mWXlRBZYUb3C8IKdS3EB +qptLPlHVAoGBAMgetjin8PFljR6Wt/GF5Y84MtonxH0oyJ7F1nw4NfmxqZxPwSXG +L1/Adc3qTyOTM+JhWL+mSqiF+go2RpEHB04kItFWgShGb6k84T7Hdq+Qrw6yZGR1 +wX7UpLwK2mrdIkzEVRMw2+7Uvi9nAn1rVxmlCFNamPxhC4ky0TBIboKl +-----END RSA PRIVATE KEY----- diff --git a/phaoUtils/tests/test_client.py b/phaoUtils/tests/test_client.py new file mode 100644 index 0000000..09e4606 --- /dev/null +++ b/phaoUtils/tests/test_client.py @@ -0,0 +1,1021 @@ +import threading +import time +import unicodedata + +import paho.mqtt.client as client +import pytest +from paho.mqtt.enums import CallbackAPIVersion, MQTTErrorCode, MQTTProtocolVersion +from paho.mqtt.packettypes import PacketTypes +from paho.mqtt.properties import Properties +from paho.mqtt.reasoncodes import ReasonCode + +import tests.paho_test as paho_test + +# Import test fixture +from tests.testsupport.broker import FakeBroker, fake_broker # noqa: F401 + + +@pytest.mark.parametrize("proto_ver,callback_version", [ + (MQTTProtocolVersion.MQTTv31, CallbackAPIVersion.VERSION1), + (MQTTProtocolVersion.MQTTv31, CallbackAPIVersion.VERSION2), + (MQTTProtocolVersion.MQTTv311, CallbackAPIVersion.VERSION1), + (MQTTProtocolVersion.MQTTv311, CallbackAPIVersion.VERSION2), +]) +class Test_connect: + """ + Tests on connect/disconnect behaviour of the client + """ + + def test_01_con_discon_success(self, proto_ver, callback_version, fake_broker): + mqttc = client.Client( + callback_version, + "01-con-discon-success", + protocol=proto_ver, + transport=fake_broker.transport, + ) + + def on_connect(mqttc, obj, flags, rc_or_reason_code, properties_or_none=None): + assert rc_or_reason_code == 0 + mqttc.disconnect() + + mqttc.on_connect = on_connect + + mqttc.connect_async("localhost", fake_broker.port) + mqttc.loop_start() + + try: + fake_broker.start() + + connect_packet = paho_test.gen_connect( + "01-con-discon-success", keepalive=60, + proto_ver=proto_ver) + packet_in = fake_broker.receive_packet(1000) + assert packet_in # Check connection was not closed + assert packet_in == connect_packet + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + disconnect_packet = paho_test.gen_disconnect() + packet_in = fake_broker.receive_packet(1000) + assert packet_in # Check connection was not closed + assert packet_in == disconnect_packet + + finally: + mqttc.loop_stop() + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed + + def test_01_con_failure_rc(self, proto_ver, callback_version, fake_broker): + mqttc = client.Client( + callback_version, "01-con-failure-rc", + protocol=proto_ver, transport=fake_broker.transport) + + def on_connect(mqttc, obj, flags, rc_or_reason_code, properties_or_none=None): + assert rc_or_reason_code > 0 + assert rc_or_reason_code != 0 + if callback_version == CallbackAPIVersion.VERSION1: + assert rc_or_reason_code == 1 + else: + assert rc_or_reason_code == ReasonCode(PacketTypes.CONNACK, "Unsupported protocol version") + + mqttc.on_connect = on_connect + + mqttc.connect_async("localhost", fake_broker.port) + mqttc.loop_start() + + try: + fake_broker.start() + + connect_packet = paho_test.gen_connect( + "01-con-failure-rc", keepalive=60, + proto_ver=proto_ver) + packet_in = fake_broker.receive_packet(1000) + assert packet_in # Check connection was not closed + assert packet_in == connect_packet + + connack_packet = paho_test.gen_connack(rc=1) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed + + finally: + mqttc.loop_stop() + + def test_connection_properties(self, proto_ver, callback_version, fake_broker): + mqttc = client.Client( + CallbackAPIVersion.VERSION2, "client-id", + protocol=proto_ver, transport=fake_broker.transport) + mqttc.enable_logger() + + is_connected = threading.Event() + is_disconnected = threading.Event() + + def on_connect(mqttc, obj, flags, rc, properties): + assert rc == 0 + is_connected.set() + + def on_disconnect(*args): + import logging + logging.info("disco") + is_disconnected.set() + + mqttc.on_connect = on_connect + mqttc.on_disconnect = on_disconnect + + mqttc.host = "localhost" + mqttc.connect_timeout = 7 + mqttc.port = fake_broker.port + mqttc.keepalive = 7 + mqttc.max_inflight_messages = 7 + mqttc.max_queued_messages = 7 + mqttc.transport = fake_broker.transport + mqttc.username = "username" + mqttc.password = "password" + + mqttc.reconnect() + + # As soon as connection try to be established, no longer accept updates + with pytest.raises(RuntimeError): + mqttc.host = "localhost" + + mqttc.loop_start() + + try: + fake_broker.start() + + connect_packet = paho_test.gen_connect( + "client-id", + keepalive=7, + username="username", + password="password", + proto_ver=proto_ver, + ) + packet_in = fake_broker.receive_packet(1000) + assert packet_in # Check connection was not closed + assert packet_in == connect_packet + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + is_connected.wait() + + # Check that all connections related properties can't be updated + with pytest.raises(RuntimeError): + mqttc.host = "localhost" + + with pytest.raises(RuntimeError): + mqttc.connect_timeout = 7 + + with pytest.raises(RuntimeError): + mqttc.port = fake_broker.port + + with pytest.raises(RuntimeError): + mqttc.keepalive = 7 + + with pytest.raises(RuntimeError): + mqttc.max_inflight_messages = 7 + + with pytest.raises(RuntimeError): + mqttc.max_queued_messages = 7 + + with pytest.raises(RuntimeError): + mqttc.transport = fake_broker.transport + + with pytest.raises(RuntimeError): + mqttc.username = "username" + + with pytest.raises(RuntimeError): + mqttc.password = "password" + + # close the connection, but from broker + fake_broker.finish() + + is_disconnected.wait() + assert not mqttc.is_connected() + + # still not allowed to update, because client try to reconnect in background + with pytest.raises(RuntimeError): + mqttc.host = "localhost" + + mqttc.disconnect() + + # Now it's allowed, connection is closing AND not trying to reconnect + mqttc.host = "localhost" + + finally: + mqttc.loop_stop() + + +class Test_connect_v5: + """ + Tests on connect/disconnect behaviour of the client with MQTTv5 + """ + + def test_01_broker_no_support(self, fake_broker): + mqttc = client.Client( + CallbackAPIVersion.VERSION2, "01-broker-no-support", + protocol=MQTTProtocolVersion.MQTTv5, transport=fake_broker.transport) + + def on_connect(mqttc, obj, flags, reason, properties): + assert reason == 132 + assert reason == ReasonCode(client.CONNACK >> 4, aName="Unsupported protocol version") + mqttc.disconnect() + + mqttc.on_connect = on_connect + + mqttc.connect_async("localhost", fake_broker.port) + mqttc.loop_start() + + try: + fake_broker.start() + + # Can't test the connect_packet, we can't yet generate MQTTv5 packet. + # connect_packet = paho_test.gen_connect( + # "01-con-discon-success", keepalive=60, + # proto_ver=client.MQTTv311) + packet_in = fake_broker.receive_packet(1000) + assert packet_in # Check connection was not closed + # assert packet_in == connect_packet + + # The reply packet is a MQTTv3 connack. But that the propose of this test, + # ensure client convert it to a reason code 132 "Unsupported protocol version" + connack_packet = paho_test.gen_connack(rc=1) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed + + finally: + mqttc.loop_stop() + + +class TestConnectionLost: + def test_with_loop_start(self, fake_broker: FakeBroker): + mqttc = client.Client( + CallbackAPIVersion.VERSION1, + "test_with_loop_start", + protocol=MQTTProtocolVersion.MQTTv311, + reconnect_on_failure=False, + transport=fake_broker.transport + ) + + on_connect_reached = threading.Event() + on_disconnect_reached = threading.Event() + + + def on_connect(mqttc, obj, flags, rc): + assert rc == 0 + on_connect_reached.set() + + def on_disconnect(*args): + on_disconnect_reached.set() + + mqttc.on_connect = on_connect + mqttc.on_disconnect = on_disconnect + + mqttc.connect_async("localhost", fake_broker.port) + mqttc.loop_start() + + try: + fake_broker.start() + + connect_packet = paho_test.gen_connect( + "test_with_loop_start", keepalive=60, + proto_ver=MQTTProtocolVersion.MQTTv311) + packet_in = fake_broker.receive_packet(1000) + assert packet_in # Check connection was not closed + assert packet_in == connect_packet + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + assert on_connect_reached.wait(1) + assert mqttc.is_connected() + + fake_broker.finish() + + assert on_disconnect_reached.wait(1) + assert not mqttc.is_connected() + + finally: + mqttc.loop_stop() + + def test_with_loop(self, fake_broker: FakeBroker): + mqttc = client.Client( + CallbackAPIVersion.VERSION1, + "test_with_loop", + clean_session=True, + transport=fake_broker.transport, + ) + + on_connect_reached = threading.Event() + on_disconnect_reached = threading.Event() + + + def on_connect(mqttc, obj, flags, rc): + assert rc == 0 + on_connect_reached.set() + + def on_disconnect(*args): + on_disconnect_reached.set() + + mqttc.on_connect = on_connect + mqttc.on_disconnect = on_disconnect + + mqttc.connect("localhost", fake_broker.port) + + fake_broker.start() + + # not yet connected, packet are not yet processed by loop() + assert not mqttc.is_connected() + + # connect packet is sent during connect() call + connect_packet = paho_test.gen_connect( + "test_with_loop", keepalive=60, + proto_ver=MQTTProtocolVersion.MQTTv311) + packet_in = fake_broker.receive_packet(1000) + assert packet_in # Check connection was not closed + assert packet_in == connect_packet + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + # call loop() to process the connack packet + assert mqttc.loop(timeout=1) == MQTTErrorCode.MQTT_ERR_SUCCESS + + assert on_connect_reached.wait(1) + assert mqttc.is_connected() + + fake_broker.finish() + + # call loop() to detect the connection lost + assert mqttc.loop(timeout=1) == MQTTErrorCode.MQTT_ERR_CONN_LOST + + assert on_disconnect_reached.wait(1) + assert not mqttc.is_connected() + + +class TestPublish: + def test_publish_before_connect(self, fake_broker: FakeBroker) -> None: + mqttc = client.Client( + CallbackAPIVersion.VERSION1, + "test_publish_before_connect", + transport=fake_broker.transport, + ) + + def on_connect(mqttc, obj, flags, rc): + assert rc == 0 + + mqttc.on_connect = on_connect + + mqttc.loop_start() + mqttc.connect("localhost", fake_broker.port) + mqttc.enable_logger() + + try: + mi = mqttc.publish("test", "testing") + + fake_broker.start() + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed + # re-call fake_broker.start() to take the 2nd connection done by client + # ... this is probably a bug, when using loop_start/loop_forever + # and doing a connect() before, the TCP connection is opened twice. + fake_broker.start() + + connect_packet = paho_test.gen_connect( + "test_publish_before_connect", keepalive=60, + proto_ver=client.MQTTv311) + packet_in = fake_broker.receive_packet(1000) + assert packet_in # Check connection was not closed + assert packet_in == connect_packet + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + with pytest.raises(RuntimeError): + mi.wait_for_publish(1) + + mqttc.disconnect() + + disconnect_packet = paho_test.gen_disconnect() + packet_in = fake_broker.receive_packet(1000) + assert packet_in # Check connection was not closed + assert packet_in == disconnect_packet + + finally: + mqttc.loop_stop() + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed + + @pytest.mark.parametrize("user_payload,sent_payload", [ + ("string", b"string"), + (b"byte", b"byte"), + (bytearray(b"bytearray"), b"bytearray"), + (42, b"42"), + (4.2, b"4.2"), + (None, b""), + ]) + def test_publish_various_payload(self, user_payload: client.PayloadType, sent_payload: bytes, fake_broker: FakeBroker) -> None: + mqttc = client.Client( + CallbackAPIVersion.VERSION2, + "test_publish_various_payload", + transport=fake_broker.transport, + ) + + mqttc.connect("localhost", fake_broker.port) + mqttc.loop_start() + mqttc.enable_logger() + + try: + fake_broker.start() + + connect_packet = paho_test.gen_connect( + "test_publish_various_payload", keepalive=60, + proto_ver=client.MQTTv311) + fake_broker.expect_packet("connect", connect_packet) + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + mqttc.publish("test", user_payload) + + publish_packet = paho_test.gen_publish( + b"test", payload=sent_payload, qos=0 + ) + fake_broker.expect_packet("publish", publish_packet) + + mqttc.disconnect() + + disconnect_packet = paho_test.gen_disconnect() + packet_in = fake_broker.receive_packet(1000) + assert packet_in # Check connection was not closed + assert packet_in == disconnect_packet + + finally: + mqttc.loop_stop() + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed + + +@pytest.mark.parametrize("callback_version", [ + (CallbackAPIVersion.VERSION1), + (CallbackAPIVersion.VERSION2), +]) +class TestPublishBroker2Client: + def test_invalid_utf8_topic(self, callback_version, fake_broker): + mqttc = client.Client(callback_version, "client-id", transport=fake_broker.transport) + + def on_message(client, userdata, msg): + with pytest.raises(UnicodeDecodeError): + assert msg.topic + client.disconnect() + + mqttc.on_message = on_message + + mqttc.connect_async("localhost", fake_broker.port) + mqttc.loop_start() + + try: + fake_broker.start() + + connect_packet = paho_test.gen_connect("client-id") + packet_in = fake_broker.receive_packet(len(connect_packet)) + assert packet_in # Check connection was not closed + assert packet_in == connect_packet + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + publish_packet = paho_test.gen_publish(b"\xff", qos=0) + count = fake_broker.send_packet(publish_packet) + assert count # Check connection was not closed + assert count == len(publish_packet) + + disconnect_packet = paho_test.gen_disconnect() + packet_in = fake_broker.receive_packet(len(disconnect_packet)) + assert packet_in # Check connection was not closed + assert packet_in == disconnect_packet + + finally: + mqttc.loop_stop() + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed + + def test_valid_utf8_topic_recv(self, callback_version, fake_broker): + mqttc = client.Client(callback_version, "client-id", transport=fake_broker.transport) + + # It should be non-ascii multi-bytes character + topic = unicodedata.lookup('SNOWMAN') + + def on_message(client, userdata, msg): + assert msg.topic == topic + client.disconnect() + + mqttc.on_message = on_message + + mqttc.connect_async("localhost", fake_broker.port) + mqttc.loop_start() + + try: + fake_broker.start() + + connect_packet = paho_test.gen_connect("client-id") + packet_in = fake_broker.receive_packet(len(connect_packet)) + assert packet_in # Check connection was not closed + assert packet_in == connect_packet + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + publish_packet = paho_test.gen_publish( + topic.encode('utf-8'), qos=0 + ) + count = fake_broker.send_packet(publish_packet) + assert count # Check connection was not closed + assert count == len(publish_packet) + + disconnect_packet = paho_test.gen_disconnect() + packet_in = fake_broker.receive_packet(len(disconnect_packet)) + assert packet_in # Check connection was not closed + assert packet_in == disconnect_packet + + finally: + mqttc.loop_stop() + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed + + def test_valid_utf8_topic_publish(self, callback_version, fake_broker): + mqttc = client.Client(callback_version, "client-id", transport=fake_broker.transport) + + # It should be non-ascii multi-bytes character + topic = unicodedata.lookup('SNOWMAN') + + mqttc.connect_async("localhost", fake_broker.port) + mqttc.loop_start() + + try: + fake_broker.start() + + connect_packet = paho_test.gen_connect("client-id") + packet_in = fake_broker.receive_packet(len(connect_packet)) + assert packet_in # Check connection was not closed + assert packet_in == connect_packet + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + mqttc.publish(topic, None, 0) + # Small sleep needed to avoid connection reset. + time.sleep(0.3) + + publish_packet = paho_test.gen_publish( + topic.encode('utf-8'), qos=0 + ) + packet_in = fake_broker.receive_packet(len(publish_packet)) + assert packet_in # Check connection was not closed + assert packet_in == publish_packet + + mqttc.disconnect() + + disconnect_packet = paho_test.gen_disconnect() + packet_in = fake_broker.receive_packet(len(disconnect_packet)) + assert packet_in # Check connection was not closed + assert packet_in == disconnect_packet + + finally: + mqttc.loop_stop() + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed + + def test_message_callback(self, callback_version, fake_broker): + mqttc = client.Client(callback_version, "client-id", transport=fake_broker.transport) + userdata = { + 'on_message': 0, + 'callback1': 0, + 'callback2': 0, + } + mqttc.user_data_set(userdata) + + def on_message(client, userdata, msg): + assert msg.topic == 'topic/value' + userdata['on_message'] += 1 + + def callback1(client, userdata, msg): + assert msg.topic == 'topic/callback/1' + userdata['callback1'] += 1 + + def callback2(client, userdata, msg): + assert msg.topic in ('topic/callback/3', 'topic/callback/1') + userdata['callback2'] += 1 + + mqttc.on_message = on_message + mqttc.message_callback_add('topic/callback/1', callback1) + mqttc.message_callback_add('topic/callback/+', callback2) + + mqttc.connect_async("localhost", fake_broker.port) + mqttc.loop_start() + + try: + fake_broker.start() + + connect_packet = paho_test.gen_connect("client-id") + packet_in = fake_broker.receive_packet(len(connect_packet)) + assert packet_in # Check connection was not closed + assert packet_in == connect_packet + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + publish_packet = paho_test.gen_publish(b"topic/value", qos=1, mid=1) + count = fake_broker.send_packet(publish_packet) + assert count # Check connection was not closed + assert count == len(publish_packet) + + publish_packet = paho_test.gen_publish(b"topic/callback/1", qos=1, mid=2) + count = fake_broker.send_packet(publish_packet) + assert count # Check connection was not closed + assert count == len(publish_packet) + + publish_packet = paho_test.gen_publish(b"topic/callback/3", qos=1, mid=3) + count = fake_broker.send_packet(publish_packet) + assert count # Check connection was not closed + assert count == len(publish_packet) + + + puback_packet = paho_test.gen_puback(mid=1) + packet_in = fake_broker.receive_packet(len(puback_packet)) + assert packet_in # Check connection was not closed + assert packet_in == puback_packet + + puback_packet = paho_test.gen_puback(mid=2) + packet_in = fake_broker.receive_packet(len(puback_packet)) + assert packet_in # Check connection was not closed + assert packet_in == puback_packet + + puback_packet = paho_test.gen_puback(mid=3) + packet_in = fake_broker.receive_packet(len(puback_packet)) + assert packet_in # Check connection was not closed + assert packet_in == puback_packet + + mqttc.disconnect() + + disconnect_packet = paho_test.gen_disconnect() + packet_in = fake_broker.receive_packet(len(disconnect_packet)) + assert packet_in # Check connection was not closed + assert packet_in == disconnect_packet + + finally: + mqttc.loop_stop() + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed + + assert userdata['on_message'] == 1 + assert userdata['callback1'] == 1 + assert userdata['callback2'] == 2 + + +class TestCompatibility: + """ + Some tests for backward compatibility + """ + + def test_change_error_code_to_enum(self): + """Make sure code don't break after MQTTErrorCode enum introduction""" + rc_ok = client.MQTTErrorCode.MQTT_ERR_SUCCESS + rc_again = client.MQTTErrorCode.MQTT_ERR_AGAIN + rc_err = client.MQTTErrorCode.MQTT_ERR_NOMEM + + # Access using old name still works + assert rc_ok == client.MQTT_ERR_SUCCESS + + # User might compare to 0 to check for success + assert rc_ok == 0 + assert not rc_err == 0 + assert not rc_again == 0 + assert not rc_ok != 0 + assert rc_err != 0 + assert rc_again != 0 + + # User might compare to specific code + assert rc_again == -1 + assert rc_err == 1 + + # User might just use "if rc:" + assert not rc_ok + assert rc_err + assert rc_again + + # User might do inequality with 0 (like "if rc > 0") + assert not (rc_ok > 0) + assert rc_err > 0 + assert rc_again < 0 + + # This might probably not be done: User might use rc as number in + # operation + assert rc_ok + 1 == 1 + + def test_migration_callback_version(self): + with pytest.raises(ValueError, match="see docs/migrations.rst"): + _ = client.Client("client-id") + + def test_callback_v1_mqtt3(self, fake_broker): + callback_called = [] + with pytest.deprecated_call(): + mqttc = client.Client( + CallbackAPIVersion.VERSION1, + "client-id", + userdata=callback_called, + transport=fake_broker.transport, + ) + + def on_connect(cl, userdata, flags, rc): + assert isinstance(cl, client.Client) + assert isinstance(flags, dict) + assert isinstance(flags["session present"], int) + assert isinstance(rc, int) + userdata.append("on_connect") + cl.subscribe([("topic", 0)]) + + def on_subscribe(cl, userdata, mid, granted_qos): + assert isinstance(cl, client.Client) + assert isinstance(mid, int) + assert isinstance(granted_qos, tuple) + assert isinstance(granted_qos[0], int) + userdata.append("on_subscribe") + cl.publish("topic", "payload", 2) + + def on_publish(cl, userdata, mid): + assert isinstance(cl, client.Client) + assert isinstance(mid, int) + userdata.append("on_publish") + + def on_message(cl, userdata, message): + assert isinstance(cl, client.Client) + assert isinstance(message, client.MQTTMessage) + userdata.append("on_message") + cl.unsubscribe("topic") + + def on_unsubscribe(cl, userdata, mid): + assert isinstance(cl, client.Client) + assert isinstance(mid, int) + userdata.append("on_unsubscribe") + cl.disconnect() + + def on_disconnect(cl, userdata, rc): + assert isinstance(cl, client.Client) + assert isinstance(rc, int) + userdata.append("on_disconnect") + + mqttc.on_connect = on_connect + mqttc.on_subscribe = on_subscribe + mqttc.on_publish = on_publish + mqttc.on_message = on_message + mqttc.on_unsubscribe = on_unsubscribe + mqttc.on_disconnect = on_disconnect + + mqttc.enable_logger() + mqttc.connect_async("localhost", fake_broker.port) + mqttc.loop_start() + + try: + fake_broker.start() + + connect_packet = paho_test.gen_connect( + "client-id", keepalive=60) + fake_broker.expect_packet("connect", connect_packet) + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + subscribe_packet = paho_test.gen_subscribe(1, "topic", 0) + fake_broker.expect_packet("subscribe", subscribe_packet) + + suback_packet = paho_test.gen_suback(1, 0) + count = fake_broker.send_packet(suback_packet) + assert count # Check connection was not closed + assert count == len(suback_packet) + + publish_packet = paho_test.gen_publish("topic", 2, "payload", mid=2) + fake_broker.expect_packet("publish", publish_packet) + + pubrec_packet = paho_test.gen_pubrec(mid=2) + count = fake_broker.send_packet(pubrec_packet) + assert count # Check connection was not closed + assert count == len(pubrec_packet) + + pubrel_packet = paho_test.gen_pubrel(mid=2) + fake_broker.expect_packet("pubrel", pubrel_packet) + + pubcomp_packet = paho_test.gen_pubcomp(mid=2) + count = fake_broker.send_packet(pubcomp_packet) + assert count # Check connection was not closed + assert count == len(pubcomp_packet) + + publish_from_broker_packet = paho_test.gen_publish("topic", qos=0, payload="payload", mid=99) + count = fake_broker.send_packet(publish_from_broker_packet) + assert count # Check connection was not closed + assert count == len(publish_from_broker_packet) + + unsubscribe_packet = paho_test.gen_unsubscribe(mid=3, topic="topic") + fake_broker.expect_packet("unsubscribe", unsubscribe_packet) + + suback_packet = paho_test.gen_unsuback(mid=3) + count = fake_broker.send_packet(suback_packet) + assert count # Check connection was not closed + assert count == len(suback_packet) + + disconnect_packet = paho_test.gen_disconnect() + fake_broker.expect_packet("disconnect", disconnect_packet) + + assert callback_called == [ + "on_connect", + "on_subscribe", + "on_publish", + "on_message", + "on_unsubscribe", + "on_disconnect", + ] + + finally: + mqttc.disconnect() + mqttc.loop_stop() + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed + + def test_callback_v2_mqtt3(self, fake_broker): + callback_called = [] + mqttc = client.Client( + CallbackAPIVersion.VERSION2, + "client-id", + userdata=callback_called, + transport=fake_broker.transport, + ) + + def on_connect(cl, userdata, flags, reason, properties): + assert isinstance(cl, client.Client) + assert isinstance(flags, client.ConnectFlags) + assert isinstance(reason, ReasonCode) + assert isinstance(properties, Properties) + assert reason == 0 + assert properties.isEmpty() + userdata.append("on_connect") + cl.subscribe([("topic", 0)]) + + def on_subscribe(cl, userdata, mid, reason_code_list, properties): + assert isinstance(cl, client.Client) + assert isinstance(mid, int) + assert isinstance(reason_code_list, list) + assert isinstance(reason_code_list[0], ReasonCode) + assert isinstance(properties, Properties) + assert properties.isEmpty() + userdata.append("on_subscribe") + cl.publish("topic", "payload", 2) + + def on_publish(cl, userdata, mid, reason_code, properties): + assert isinstance(cl, client.Client) + assert isinstance(mid, int) + assert isinstance(reason_code, ReasonCode) + assert isinstance(properties, Properties) + assert properties.isEmpty() + userdata.append("on_publish") + + def on_message(cl, userdata, message): + assert isinstance(cl, client.Client) + assert isinstance(message, client.MQTTMessage) + userdata.append("on_message") + cl.unsubscribe("topic") + + def on_unsubscribe(cl, userdata, mid, reason_code_list, properties): + assert isinstance(cl, client.Client) + assert isinstance(mid, int) + assert isinstance(reason_code_list, list) + assert len(reason_code_list) == 0 + assert isinstance(properties, Properties) + assert properties.isEmpty() + userdata.append("on_unsubscribe") + cl.disconnect() + + def on_disconnect(cl, userdata, flags, reason_code, properties): + assert isinstance(cl, client.Client) + assert isinstance(flags, client.DisconnectFlags) + assert isinstance(reason_code, ReasonCode) + assert isinstance(properties, Properties) + assert properties.isEmpty() + userdata.append("on_disconnect") + + mqttc.on_connect = on_connect + mqttc.on_subscribe = on_subscribe + mqttc.on_publish = on_publish + mqttc.on_message = on_message + mqttc.on_unsubscribe = on_unsubscribe + mqttc.on_disconnect = on_disconnect + + mqttc.enable_logger() + mqttc.connect_async("localhost", fake_broker.port) + mqttc.loop_start() + + try: + fake_broker.start() + + connect_packet = paho_test.gen_connect( + "client-id", keepalive=60) + fake_broker.expect_packet("connect", connect_packet) + + connack_packet = paho_test.gen_connack(rc=0) + count = fake_broker.send_packet(connack_packet) + assert count # Check connection was not closed + assert count == len(connack_packet) + + subscribe_packet = paho_test.gen_subscribe(1, "topic", 0) + fake_broker.expect_packet("subscribe", subscribe_packet) + + suback_packet = paho_test.gen_suback(1, 0) + count = fake_broker.send_packet(suback_packet) + assert count # Check connection was not closed + assert count == len(suback_packet) + + publish_packet = paho_test.gen_publish("topic", 2, "payload", mid=2) + fake_broker.expect_packet("publish", publish_packet) + + pubrec_packet = paho_test.gen_pubrec(mid=2) + count = fake_broker.send_packet(pubrec_packet) + assert count # Check connection was not closed + assert count == len(pubrec_packet) + + pubrel_packet = paho_test.gen_pubrel(mid=2) + fake_broker.expect_packet("pubrel", pubrel_packet) + + pubcomp_packet = paho_test.gen_pubcomp(mid=2) + count = fake_broker.send_packet(pubcomp_packet) + assert count # Check connection was not closed + assert count == len(pubcomp_packet) + + publish_from_broker_packet = paho_test.gen_publish("topic", qos=0, payload="payload", mid=99) + count = fake_broker.send_packet(publish_from_broker_packet) + assert count # Check connection was not closed + assert count == len(publish_from_broker_packet) + + unsubscribe_packet = paho_test.gen_unsubscribe(mid=3, topic="topic") + fake_broker.expect_packet("unsubscribe", unsubscribe_packet) + + suback_packet = paho_test.gen_unsuback(mid=3) + count = fake_broker.send_packet(suback_packet) + assert count # Check connection was not closed + assert count == len(suback_packet) + + disconnect_packet = paho_test.gen_disconnect() + fake_broker.expect_packet("disconnect", disconnect_packet) + + assert callback_called == [ + "on_connect", + "on_subscribe", + "on_publish", + "on_message", + "on_unsubscribe", + "on_disconnect", + ] + + finally: + mqttc.disconnect() + mqttc.loop_stop() + + packet_in = fake_broker.receive_packet(1) + assert not packet_in # Check connection is closed diff --git a/phaoUtils/tests/test_matcher.py b/phaoUtils/tests/test_matcher.py new file mode 100644 index 0000000..e2dc02a --- /dev/null +++ b/phaoUtils/tests/test_matcher.py @@ -0,0 +1,36 @@ +import paho.mqtt.client as client +import pytest + + +class Test_client_function: + """ + Tests on topic_matches_sub function in the client module + """ + + @pytest.mark.parametrize("sub,topic", [ + ("foo/bar", "foo/bar"), + ("foo/+", "foo/bar"), + ("foo/+/baz", "foo/bar/baz"), + ("foo/+/#", "foo/bar/baz"), + ("A/B/+/#", "A/B/B/C"), + ("#", "foo/bar/baz"), + ("#", "/foo/bar"), + ("/#", "/foo/bar"), + ("$SYS/bar", "$SYS/bar"), + ]) + def test_matching(self, sub, topic): + assert client.topic_matches_sub(sub, topic) + + + @pytest.mark.parametrize("sub,topic", [ + ("test/6/#", "test/3"), + ("foo/bar", "foo"), + ("foo/+", "foo/bar/baz"), + ("foo/+/baz", "foo/bar/bar"), + ("foo/+/#", "fo2/bar/baz"), + ("/#", "foo/bar"), + ("#", "$SYS/bar"), + ("$BOB/bar", "$SYS/bar"), + ]) + def test_not_matching(self, sub, topic): + assert not client.topic_matches_sub(sub, topic) diff --git a/phaoUtils/tests/test_mqttv5.py b/phaoUtils/tests/test_mqttv5.py new file mode 100644 index 0000000..83a1fdd --- /dev/null +++ b/phaoUtils/tests/test_mqttv5.py @@ -0,0 +1,1410 @@ +""" +******************************************************************* + Copyright (c) 2013, 2019 IBM Corp. + + All rights reserved. This program and the accompanying materials + are made available under the terms of the Eclipse Public License v2.0 + and Eclipse Distribution License v1.0 which accompany this distribution. + + The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v20.html + and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + + Contributors: + Ian Craggs - initial implementation and/or documentation +******************************************************************* +""" + +import logging +import queue +import sys +import threading +import time +import unittest +import unittest.mock + +import paho.mqtt +import paho.mqtt.client +from paho.mqtt.enums import CallbackAPIVersion +from paho.mqtt.packettypes import PacketTypes +from paho.mqtt.properties import Properties +from paho.mqtt.subscribeoptions import SubscribeOptions + +DEFAULT_TIMEOUT = 5 +# timeout for something that should not happen but we wait to +# give it time to happen if it does due to a bug. +WAIT_NON_EVENT_TIMEOUT = 1 + +class Callbacks: + + def __init__(self): + self.messages = queue.Queue() + self.publisheds = queue.Queue() + self.subscribeds = queue.Queue() + self.unsubscribeds = queue.Queue() + self.disconnecteds = queue.Queue() + self.connecteds = queue.Queue() + self.conn_failures = queue.Queue() + + def __str__(self): + return str(self.messages.queue) + str(self.messagedicts.queue) + str(self.publisheds.queue) + \ + str(self.subscribeds.queue) + \ + str(self.unsubscribeds.queue) + str(self.disconnects.queue) + + def clear(self): + self.__init__() + + def on_connect(self, client, userdata, flags, reasonCode, properties): + self.connecteds.put({"userdata": userdata, "flags": flags, + "reasonCode": reasonCode, "properties": properties}) + + def on_connect_fail(self, client, userdata): + self.conn_failures.put({"userdata": userdata}) + + def wait_connect_fail(self): + return self.conn_failures.get(timeout=10) + + def wait_connected(self): + return self.connecteds.get(timeout=2) + + def on_disconnect(self, client, userdata, reasonCode, properties=None): + self.disconnecteds.put( + {"reasonCode": reasonCode, "properties": properties}) + + def wait_disconnected(self): + return self.disconnecteds.get(timeout=2) + + def on_message(self, client, userdata, message): + self.messages.put({"userdata": userdata, "message": message}) + + def published(self, client, userdata, msgid): + self.publisheds.put(msgid) + + def wait_published(self): + return self.publisheds.get(timeout=2) + + def on_subscribe(self, client, userdata, mid, reasonCodes, properties): + self.subscribeds.put({"mid": mid, "userdata": userdata, + "properties": properties, "reasonCodes": reasonCodes}) + + def wait_subscribed(self): + return self.subscribeds.get(timeout=2) + + def unsubscribed(self, client, userdata, mid, properties, reasonCodes): + self.unsubscribeds.put({"mid": mid, "userdata": userdata, + "properties": properties, "reasonCodes": reasonCodes}) + + def wait_unsubscribed(self): + return self.unsubscribeds.get(timeout=2) + + def on_log(self, client, userdata, level, buf): + print(buf) + + def register(self, client): + client.on_connect = self.on_connect + client.on_subscribe = self.on_subscribe + client.on_publish = self.published + client.on_unsubscribe = self.unsubscribed + client.on_message = self.on_message + client.on_disconnect = self.on_disconnect + client.on_connect_fail = self.on_connect_fail + client.on_log = self.on_log + + def get_messages(self, count: int, timeout: float = DEFAULT_TIMEOUT): + result = [] + deadline = time.time() + timeout + while len(result) < count: + get_timeout = deadline - time.time() + if get_timeout <= 0: + result.append(self.messages.get_nowait()) + else: + result.append(self.messages.get(timeout=get_timeout)) + + return result + + def get_at_most_messages(self, count: int, timeout: float = DEFAULT_TIMEOUT): + result = [] + deadline = time.time() + timeout + try: + while len(result) < count: + get_timeout = deadline - time.time() + if get_timeout <= 0: + result.append(self.messages.get_nowait()) + else: + result.append(self.messages.get(timeout=get_timeout)) + except queue.Empty: + pass + + return result + + +def cleanRetained(port): + callback = Callbacks() + curclient = paho.mqtt.client.Client( + CallbackAPIVersion.VERSION1, + b"clean retained", + protocol=paho.mqtt.client.MQTTv5, + ) + callback.register(curclient) + curclient.connect(host="localhost", port=port) + curclient.loop_start() + callback.wait_connected() + curclient.subscribe("#", options=SubscribeOptions(qos=0)) + callback.wait_subscribed() # wait for retained messages to arrive + try: + while True: + message = callback.messages.get(timeout=WAIT_NON_EVENT_TIMEOUT) + if message["message"].payload != b"": + logging.info("deleting retained message for topic", message["message"]) + curclient.publish(message["message"].topic, b"", 0, retain=True) + except queue.Empty: + pass + curclient.disconnect() + curclient.loop_stop() + + +def cleanup(port): + # clean all client state + print("clean up starting") + clientids = ("aclient", "bclient") + + def _on_connect(client, *args): + client.disconnect() + + for clientid in clientids: + curclient = paho.mqtt.client.Client( + CallbackAPIVersion.VERSION1, + clientid.encode("utf-8"), + protocol=paho.mqtt.client.MQTTv5, + ) + curclient.on_connect = _on_connect + curclient.connect(host="localhost", port=port, clean_start=True) + curclient.loop_forever() + + # clean retained messages + cleanRetained(port) + print("clean up finished") + + +class Test(unittest.TestCase): + + @classmethod + def setUpClass(cls): + global callback, callback2, aclient, bclient + + sys.path.append("paho.mqtt.testing/interoperability/") + try: + import mqtt.brokers + except ImportError as ie: + raise unittest.SkipTest("paho.mqtt.testing not present.") from ie + + # Hack: we need to patch `signal.signal()` because `mqtt.brokers.run()` + # calls it to set up a signal handler; however, that won't work + # from a thread... + with unittest.mock.patch("signal.signal", unittest.mock.MagicMock()): + cls._test_broker = threading.Thread( + target=mqtt.brokers.run, + kwargs={ + "config": ["listener 0"], + }, + ) + cls._test_broker.daemon = True + cls._test_broker.start() + # Wait a bit for TCP server to bind to an address + for _ in range(20): + time.sleep(0.1) + if mqtt.brokers.listeners.TCPListeners.server is not None: + port = mqtt.brokers.listeners.TCPListeners.server.socket.getsockname()[1] + if port != 0: + cls._test_broker_port = port + break + else: + raise ValueError("can't find the test broker port") + setData() + cleanup(cls._test_broker_port) + + callback = Callbacks() + callback2 = Callbacks() + + #aclient = mqtt_client.Client(b"\xEF\xBB\xBF" + "myclientid".encode("utf-8")) + #aclient = mqtt_client.Client("myclientid".encode("utf-8")) + aclient = paho.mqtt.client.Client(CallbackAPIVersion.VERSION1, b"aclient", protocol=paho.mqtt.client.MQTTv5) + callback.register(aclient) + + bclient = paho.mqtt.client.Client(CallbackAPIVersion.VERSION1, b"bclient", protocol=paho.mqtt.client.MQTTv5) + callback2.register(bclient) + + @classmethod + def tearDownClass(cls): + # Another hack to stop the test broker... we rely on fact that it use a sockserver.TCPServer + import mqtt.brokers + mqtt.brokers.listeners.TCPListeners.server.shutdown() + cls._test_broker.join(5) + + def test_basic(self): + import datetime + print(datetime.datetime.now(), "start") + aclient.connect(host="localhost", port=self._test_broker_port) + aclient.loop_start() + print(datetime.datetime.now(), "loop_start") + response = callback.wait_connected() + print(datetime.datetime.now(), "connected") + self.assertEqual(response["reasonCode"].getName(), "Success") + + aclient.subscribe(topics[0], options=SubscribeOptions(qos=2)) + response = callback.wait_subscribed() + print(datetime.datetime.now(), "wait_subscribed") + self.assertEqual(response["reasonCodes"][0].getName(), "Granted QoS 2") + + aclient.publish(topics[0], b"qos 0") + aclient.publish(topics[0], b"qos 1", 1) + aclient.publish(topics[0], b"qos 2", 2) + + msgs = callback.get_messages(3) + print(datetime.datetime.now(), "publish get") + got_payload = { + x["message"].payload + for x in msgs + } + + self.assertEqual(got_payload, {b"qos 0", b"qos 1", b"qos 2"}) + aclient.disconnect() + + callback.clear() + aclient.loop_stop() + + def test_connect_fail(self): + clientid = "connection failure" + + fclient, fcallback = self.new_client(clientid) + + fclient.user_data_set(1) + fclient.connect_async("localhost", 1) + response = fcallback.wait_connect_fail() + self.assertEqual(response["userdata"], 1) + fclient.loop_stop() + + def test_retained_message(self): + + publish_properties = Properties(PacketTypes.PUBLISH) + publish_properties.UserProperty = ("a", "2") + publish_properties.UserProperty = ("c", "3") + + # retained messages + callback.clear() + aclient.connect(host="localhost", port=self._test_broker_port) + aclient.loop_start() + response = callback.wait_connected() + aclient.publish(topics[1], b"qos 0", 0, + retain=True, properties=publish_properties) + aclient.publish(topics[2], b"qos 1", 1, + retain=True, properties=publish_properties) + aclient.publish(topics[3], b"qos 2", 2, + retain=True, properties=publish_properties) + # wait until those messages are published + time.sleep(WAIT_NON_EVENT_TIMEOUT) + aclient.subscribe(wildtopics[5], options=SubscribeOptions(qos=2)) + response = callback.wait_subscribed() + self.assertEqual(response["reasonCodes"][0].getName(), "Granted QoS 2") + msgs = callback.get_messages(3) + + aclient.disconnect() + aclient.loop_stop() + + self.assertTrue(callback.messages.empty()) + + userprops = msgs[0]["message"].properties.UserProperty + self.assertTrue(userprops in [[("a", "2"), ("c", "3")], [ + ("c", "3"), ("a", "2")]], userprops) + userprops = msgs[1]["message"].properties.UserProperty + self.assertTrue(userprops in [[("a", "2"), ("c", "3")], [ + ("c", "3"), ("a", "2")]], userprops) + userprops = msgs[2]["message"].properties.UserProperty + self.assertTrue(userprops in [[("a", "2"), ("c", "3")], [ + ("c", "3"), ("a", "2")]], userprops) + qoss = [x["message"].qos for x in msgs] + self.assertTrue(1 in qoss and 2 in qoss and 0 in qoss, qoss) + + cleanRetained(self._test_broker_port) + + def test_will_message(self): + # will messages and keep alive + callback.clear() + callback2.clear() + self.assertTrue(callback2.messages.empty(), callback2.messages.queue) + + will_properties = Properties(PacketTypes.WILLMESSAGE) + will_properties.WillDelayInterval = 0 # this is the default anyway + will_properties.UserProperty = ("a", "2") + will_properties.UserProperty = ("c", "3") + + aclient.will_set(topics[2], payload=b"will message", + properties=will_properties) + + aclient.connect(host="localhost", port=self._test_broker_port, keepalive=2) + aclient.loop_start() + response = callback.wait_connected() + bclient.connect(host="localhost", port=self._test_broker_port) + bclient.loop_start() + response = callback2.wait_connected() + bclient.subscribe(topics[2], qos=2) + response = callback2.wait_subscribed() + self.assertEqual(response["reasonCodes"][0].getName(), "Granted QoS 2") + + # keep alive timeout ought to be triggered so the will message is received + aclient.loop_stop() # so that pings aren't sent + msg = callback2.messages.get(timeout=10) + bclient.disconnect() + bclient.loop_stop() + + props = msg["message"].properties + self.assertEqual(props.UserProperty, [("a", "2"), ("c", "3")]) + + def test_zero_length_clientid(self): + logging.info("Zero length clientid test starting") + + callback0 = Callbacks() + + client0 = paho.mqtt.client.Client(CallbackAPIVersion.VERSION1, protocol=paho.mqtt.client.MQTTv5) + callback0.register(client0) + client0.loop_start() + # should not be rejected + client0.connect(host="localhost", port=self._test_broker_port, clean_start=False) + response = callback0.wait_connected() + self.assertEqual(response["reasonCode"].getName(), "Success") + self.assertTrue( + len(response["properties"].AssignedClientIdentifier) > 0) + client0.disconnect() + client0.loop_stop() + + client0 = paho.mqtt.client.Client(CallbackAPIVersion.VERSION1, protocol=paho.mqtt.client.MQTTv5) + callback0.register(client0) + client0.loop_start() + client0.connect(host="localhost", port=self._test_broker_port) # should work + response = callback0.wait_connected() + self.assertEqual(response["reasonCode"].getName(), "Success") + self.assertTrue( + len(response["properties"].AssignedClientIdentifier) > 0) + client0.disconnect() + client0.loop_stop() + + # when we supply a client id, we should not get one assigned + client0 = paho.mqtt.client.Client( + CallbackAPIVersion.VERSION1, "client0", protocol=paho.mqtt.client.MQTTv5, + ) + callback0.register(client0) + client0.loop_start() + client0.connect(host="localhost", port=self._test_broker_port) # should work + response = callback0.wait_connected() + self.assertEqual(response["reasonCode"].getName(), "Success") + self.assertFalse( + hasattr(response["properties"], "AssignedClientIdentifier")) + client0.disconnect() + client0.loop_stop() + + def test_offline_message_queueing(self): + # message queueing for offline clients + cleanRetained(self._test_broker_port) + ocallback = Callbacks() + clientid = b"offline message queueing" + + oclient = paho.mqtt.client.Client( + CallbackAPIVersion.VERSION1, clientid, protocol=paho.mqtt.client.MQTTv5, + ) + ocallback.register(oclient) + connect_properties = Properties(PacketTypes.CONNECT) + connect_properties.SessionExpiryInterval = 99999 + oclient.loop_start() + oclient.connect(host="localhost", port=self._test_broker_port, properties=connect_properties) + ocallback.wait_connected() + oclient.subscribe(wildtopics[5], qos=2) + ocallback.wait_subscribed() + oclient.disconnect() + oclient.loop_stop() + + bclient.loop_start() + bclient.connect(host="localhost", port=self._test_broker_port) + callback2.wait_connected() + msg1 = bclient.publish(topics[1], b"qos 0", 0) + msg2 = bclient.publish(topics[2], b"qos 1", 1) + msg3 = bclient.publish(topics[3], b"qos 2", 2) + + msg1.wait_for_publish() + msg2.wait_for_publish() + msg3.wait_for_publish() + + bclient.disconnect() + bclient.loop_stop() + + oclient = paho.mqtt.client.Client( + CallbackAPIVersion.VERSION1, clientid, protocol=paho.mqtt.client.MQTTv5, + ) + ocallback.register(oclient) + oclient.loop_start() + oclient.connect(host="localhost", port=self._test_broker_port, clean_start=False) + ocallback.wait_connected() + + msgs = ocallback.get_at_most_messages(3) + + oclient.disconnect() + oclient.loop_stop() + + self.assertTrue(len(msgs) in [ + 2, 3], ocallback.messages.qsize()) + logging.info("This server %s queueing QoS 0 messages for offline clients" % + ("is" if len(msgs) == 3 else "is not")) + + def test_overlapping_subscriptions(self): + # overlapping subscriptions. When there is more than one matching subscription for the same client for a topic, + # the server may send back one message with the highest QoS of any matching subscription, or one message for + # each subscription with a matching QoS. + ocallback = Callbacks() + clientid = b"overlapping subscriptions" + + oclient = paho.mqtt.client.Client( + CallbackAPIVersion.VERSION1, clientid, protocol=paho.mqtt.client.MQTTv5, + ) + ocallback.register(oclient) + + oclient.loop_start() + oclient.connect(host="localhost", port=self._test_broker_port) + ocallback.wait_connected() + oclient.subscribe([(wildtopics[6], SubscribeOptions(qos=2)), + (wildtopics[0], SubscribeOptions(qos=1))]) + ocallback.wait_subscribed() + oclient.publish(topics[3], b"overlapping topic filters", 2) + ocallback.wait_published() + + msgs = ocallback.get_at_most_messages(2) + if len(msgs) == 1: + logging.info( + "This server is publishing one message for all matching overlapping subscriptions, not one for each.") + self.assertEqual( + msgs[0]["message"].qos, 2, msgs[0]["message"].qos) + else: + logging.info( + "This server is publishing one message per each matching overlapping subscription.") + self.assertTrue((msgs[0]["message"].qos == 2 and msgs[1]["message"].qos == 1) or + (msgs[0]["message"].qos == 1 and msgs[1]["message"].qos == 2), msgs) + oclient.disconnect() + oclient.loop_stop() + ocallback.clear() + + def test_subscribe_failure(self): + # Subscribe failure. A new feature of MQTT 3.1.1 is the ability to send back negative responses to subscribe + # requests. One way of doing this is to subscribe to a topic which is not allowed to be subscribed to. + logging.info("Subscribe failure test starting") + + ocallback = Callbacks() + clientid = b"subscribe failure" + oclient = paho.mqtt.client.Client( + CallbackAPIVersion.VERSION1, clientid, protocol=paho.mqtt.client.MQTTv5, + ) + ocallback.register(oclient) + oclient.loop_start() + oclient.connect(host="localhost", port=self._test_broker_port) + ocallback.wait_connected() + oclient.subscribe(nosubscribe_topics[0], qos=2) + response = ocallback.wait_subscribed() + + self.assertEqual(response["reasonCodes"][0].getName(), "Unspecified error", + f"return code should be 0x80 {response['reasonCodes'][0].getName()}") + oclient.disconnect() + oclient.loop_stop() + + def test_unsubscribe(self): + callback2.clear() + bclient.connect(host="localhost", port=self._test_broker_port) + bclient.loop_start() + callback2.wait_connected() + bclient.subscribe(topics[0], qos=2) + callback2.wait_subscribed() + bclient.subscribe(topics[1], qos=2) + callback2.wait_subscribed() + bclient.subscribe(topics[2], qos=2) + callback2.wait_subscribed() + time.sleep(1) # wait for any retained messages, hopefully + # Unsubscribe from one topic + bclient.unsubscribe(topics[0]) + callback2.wait_unsubscribed() + callback2.clear() # if there were any retained messages + + aclient.connect(host="localhost", port=self._test_broker_port) + aclient.loop_start() + callback.wait_connected() + aclient.publish(topics[0], b"topic 0 - unsubscribed", 1, retain=False) + aclient.publish(topics[1], b"topic 1", 1, retain=False) + aclient.publish(topics[2], b"topic 2", 1, retain=False) + + msgs = callback2.get_messages(2) + + bclient.disconnect() + bclient.loop_stop() + aclient.disconnect() + aclient.loop_stop() + self.assertEqual(len(msgs), 2) + + def new_client(self, clientid): + callback = Callbacks() + client = paho.mqtt.client.Client( + CallbackAPIVersion.VERSION1, + clientid.encode("utf-8"), + protocol=paho.mqtt.client.MQTTv5, + ) + callback.register(client) + client.loop_start() + return client, callback + + def test_session_expiry(self): + # no session expiry property == never expire + + connect_properties = Properties(PacketTypes.CONNECT) + connect_properties.SessionExpiryInterval = 0 # expire immediately + + clientid = "session expiry" + + eclient, ecallback = self.new_client(clientid) + + eclient.connect(host="localhost", port=self._test_broker_port, properties=connect_properties) + connack = ecallback.wait_connected() + self.assertEqual(connack["reasonCode"].getName(), "Success") + self.assertEqual(connack["flags"]["session present"], False) + eclient.subscribe(topics[0], qos=2) + ecallback.wait_subscribed() + eclient.disconnect() + ecallback.wait_disconnected() + eclient.loop_stop() + + fclient, fcallback = self.new_client(clientid) + + # session should immediately expire + fclient.connect_async(host="localhost", port=self._test_broker_port, clean_start=False, + properties=connect_properties) + connack = fcallback.wait_connected() + self.assertEqual(connack["reasonCode"].getName(), "Success") + self.assertEqual(connack["flags"]["session present"], False) + fclient.disconnect() + fcallback.wait_disconnected() + + connect_properties.SessionExpiryInterval = 5 + + eclient, ecallback = self.new_client(clientid) + + eclient.connect(host="localhost", port=self._test_broker_port, properties=connect_properties) + connack = ecallback.wait_connected() + self.assertEqual(connack["reasonCode"].getName(), "Success") + self.assertEqual(connack["flags"]["session present"], False) + eclient.subscribe(topics[0], qos=2) + ecallback.wait_subscribed() + eclient.disconnect() + ecallback.wait_disconnected() + eclient.loop_stop() + + time.sleep(2) + # session should still exist + fclient, fcallback = self.new_client(clientid) + fclient.connect(host="localhost", port=self._test_broker_port, clean_start=False, + properties=connect_properties) + connack = fcallback.wait_connected() + self.assertEqual(connack["reasonCode"].getName(), "Success") + self.assertEqual(connack["flags"]["session present"], True) + fclient.disconnect() + fcallback.wait_disconnected() + fclient.loop_stop() + + time.sleep(6) + # session should not exist + fclient, fcallback = self.new_client(clientid) + fclient.connect(host="localhost", port=self._test_broker_port, clean_start=False, + properties=connect_properties) + connack = fcallback.wait_connected() + self.assertEqual(connack["reasonCode"].getName(), "Success") + self.assertEqual(connack["flags"]["session present"], False) + fclient.disconnect() + fcallback.wait_disconnected() + fclient.loop_stop() + + eclient, ecallback = self.new_client(clientid) + connect_properties.SessionExpiryInterval = 1 + connack = eclient.connect( + host="localhost", port=self._test_broker_port, properties=connect_properties) + connack = ecallback.wait_connected() + self.assertEqual(connack["reasonCode"].getName(), "Success") + self.assertEqual(connack["flags"]["session present"], False) + eclient.subscribe(topics[0], qos=2) + ecallback.wait_subscribed() + disconnect_properties = Properties(PacketTypes.DISCONNECT) + disconnect_properties.SessionExpiryInterval = 5 + eclient.disconnect(properties=disconnect_properties) + ecallback.wait_disconnected() + eclient.loop_stop() + + time.sleep(3) + # session should still exist as we changed the expiry interval on disconnect + fclient, fcallback = self.new_client(clientid) + fclient.connect(host="localhost", port=self._test_broker_port, clean_start=False, + properties=connect_properties) + connack = fcallback.wait_connected() + self.assertEqual(connack["reasonCode"].getName(), "Success") + self.assertEqual(connack["flags"]["session present"], True) + disconnect_properties.SessionExpiryInterval = 0 + fclient.disconnect(properties=disconnect_properties) + fcallback.wait_disconnected() + fclient.loop_stop() + + # session should immediately expire + fclient, fcallback = self.new_client(clientid) + fclient.connect(host="localhost", port=self._test_broker_port, clean_start=False, + properties=connect_properties) + connack = fcallback.wait_connected() + self.assertEqual(connack["reasonCode"].getName(), "Success") + self.assertEqual(connack["flags"]["session present"], False) + fclient.disconnect() + fcallback.wait_disconnected() + fclient.loop_stop() + + fclient.loop_stop() + eclient.loop_stop() + + def test_user_properties(self): + clientid = "user properties" + uclient, ucallback = self.new_client(clientid) + uclient.loop_start() + uclient.connect(host="localhost", port=self._test_broker_port) + ucallback.wait_connected() + + uclient.subscribe(topics[0], qos=2) + ucallback.wait_subscribed() + + publish_properties = Properties(PacketTypes.PUBLISH) + publish_properties.UserProperty = ("a", "2") + publish_properties.UserProperty = ("c", "3") + uclient.publish(topics[0], b"", 0, retain=False, + properties=publish_properties) + uclient.publish(topics[0], b"", 1, retain=False, + properties=publish_properties) + uclient.publish(topics[0], b"", 2, retain=False, + properties=publish_properties) + + msgs = ucallback.get_messages(3) + + uclient.disconnect() + ucallback.wait_disconnected() + uclient.loop_stop() + self.assertTrue(ucallback.messages.empty(), ucallback.messages.queue) + userprops = msgs[0]["message"].properties.UserProperty + self.assertTrue(userprops in [[("a", "2"), ("c", "3")], [ + ("c", "3"), ("a", "2")]], userprops) + userprops = msgs[1]["message"].properties.UserProperty + self.assertTrue(userprops in [[("a", "2"), ("c", "3")], [ + ("c", "3"), ("a", "2")]], userprops) + userprops = msgs[2]["message"].properties.UserProperty + self.assertTrue(userprops in [[("a", "2"), ("c", "3")], [ + ("c", "3"), ("a", "2")]], userprops) + qoss = [x["message"].qos for x in msgs] + self.assertTrue(1 in qoss and 2 in qoss and 0 in qoss, qoss) + + def test_payload_format(self): + clientid = "payload format" + pclient, pcallback = self.new_client(clientid) + pclient.loop_start() + pclient.connect_async(host="localhost", port=self._test_broker_port) + pcallback.wait_connected() + + pclient.subscribe(topics[0], qos=2) + pcallback.wait_subscribed() + publish_properties = Properties(PacketTypes.PUBLISH) + publish_properties.PayloadFormatIndicator = 1 + publish_properties.ContentType = "My name" + info = pclient.publish( + topics[0], b"qos 0", 0, retain=False, properties=publish_properties) + info.wait_for_publish() + info = pclient.publish( + topics[0], b"qos 1", 1, retain=False, properties=publish_properties) + info.wait_for_publish() + info = pclient.publish( + topics[0], b"qos 2", 2, retain=False, properties=publish_properties) + info.wait_for_publish() + + msgs = pcallback.get_messages(3) + + pclient.disconnect() + pcallback.wait_disconnected() + pclient.loop_stop() + + self.assertTrue(pcallback.messages.empty(), pcallback.messages.queue) + props = msgs[0]["message"].properties + self.assertEqual(props.ContentType, "My name", props.ContentType) + self.assertEqual(props.PayloadFormatIndicator, + 1, props.PayloadFormatIndicator) + props = msgs[1]["message"].properties + self.assertEqual(props.ContentType, "My name", props.ContentType) + self.assertEqual(props.PayloadFormatIndicator, + 1, props.PayloadFormatIndicator) + props = msgs[2]["message"].properties + self.assertEqual(props.ContentType, "My name", props.ContentType) + self.assertEqual(props.PayloadFormatIndicator, + 1, props.PayloadFormatIndicator) + qoss = [x["message"].qos for x in msgs] + self.assertTrue(1 in qoss and 2 in qoss and 0 in qoss, qoss) + + def test_message_expiry(self): + clientid = "message expiry" + + connect_properties = Properties(PacketTypes.CONNECT) + connect_properties.SessionExpiryInterval = 99999 + + lbclient, lbcallback = self.new_client(f"{clientid} b") + lbclient.loop_start() + lbclient.connect(host="localhost", port=self._test_broker_port, properties=connect_properties) + lbcallback.wait_connected() + lbclient.subscribe(topics[0], qos=2) + lbcallback.wait_subscribed() + disconnect_properties = Properties(PacketTypes.DISCONNECT) + disconnect_properties.SessionExpiryInterval = 999999999 + lbclient.disconnect(properties=disconnect_properties) + lbcallback.wait_disconnected() + lbclient.loop_stop() + + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.loop_start() + laclient.connect(host="localhost", port=self._test_broker_port) + publish_properties = Properties(PacketTypes.PUBLISH) + publish_properties.MessageExpiryInterval = 1 + laclient.publish(topics[0], b"qos 1 - expire", 1, + retain=False, properties=publish_properties) + laclient.publish(topics[0], b"qos 2 - expire", 2, + retain=False, properties=publish_properties) + + publish_properties = Properties(PacketTypes.PUBLISH) + publish_properties.MessageExpiryInterval = 6 + laclient.publish(topics[0], b"qos 1 - don't expire", + 1, retain=False, properties=publish_properties) + laclient.publish(topics[0], b"qos 2 - don't expire", + 2, retain=False, properties=publish_properties) + + time.sleep(3) + lbclient, lbcallback = self.new_client(f"{clientid} b") + lbclient.loop_start() + lbclient.connect(host="localhost", port=self._test_broker_port, clean_start=False) + lbcallback.wait_connected() + + msgs = lbcallback.get_messages(2) + + self.assertTrue(lbcallback.messages.empty(), lbcallback.messages.queue) + self.assertTrue(msgs[0]["message"].properties.MessageExpiryInterval < 6, + msgs[0]["message"].properties.MessageExpiryInterval) + self.assertTrue(msgs[1]["message"].properties.MessageExpiryInterval < 6, + msgs[1]["message"].properties.MessageExpiryInterval) + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + + lbclient.disconnect() + lbcallback.wait_disconnected() + lbclient.loop_stop() + + def test_subscribe_options(self): + # noLocal + clientid = 'subscribe options - noLocal' + + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port) + lacallback.wait_connected() + laclient.loop_start() + laclient.subscribe( + topics[0], options=SubscribeOptions(qos=2, noLocal=True)) + lacallback.wait_subscribed() + + lbclient, lbcallback = self.new_client(f"{clientid} b") + lbclient.connect(host="localhost", port=self._test_broker_port) + lbcallback.wait_connected() + lbclient.loop_start() + lbclient.subscribe( + topics[0], options=SubscribeOptions(qos=2, noLocal=True)) + lbcallback.wait_subscribed() + + laclient.publish(topics[0], b"noLocal test", 1, retain=False) + + lbcallback.messages.get(timeout=DEFAULT_TIMEOUT) + try: + lacallback.messages.get(timeout=WAIT_NON_EVENT_TIMEOUT) + raise ValueError("unexpected message received") + except queue.Empty: + pass + + self.assertTrue(lacallback.messages.empty(), lacallback.messages.queue) + self.assertTrue(lbcallback.messages.empty(), lbcallback.messages.queue) + laclient.disconnect() + lacallback.wait_disconnected() + lbclient.disconnect() + lbcallback.wait_disconnected() + laclient.loop_stop() + lbclient.loop_stop() + + # retainAsPublished + clientid = 'subscribe options - retain as published' + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port) + lacallback.wait_connected() + laclient.subscribe(topics[0], options=SubscribeOptions( + qos=2, retainAsPublished=True)) + lacallback.wait_subscribed() + laclient.publish( + topics[0], b"retain as published false", 1, retain=False) + laclient.publish( + topics[0], b"retain as published true", 1, retain=True) + + msgs = lacallback.get_messages(2) + + self.assertTrue(lacallback.messages.empty(), lacallback.messages.queue) + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + self.assertEqual(msgs[0]["message"].retain, False) + self.assertEqual(msgs[1]["message"].retain, True) + + # retainHandling + clientid = 'subscribe options - retain handling' + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port) + lacallback.wait_connected() + laclient.publish(topics[1], b"qos 0", 0, retain=True) + laclient.publish(topics[2], b"qos 1", 1, retain=True) + laclient.publish(topics[3], b"qos 2", 2, retain=True) + time.sleep(1) + + # retain handling 1 only gives us retained messages on a new subscription + laclient.subscribe( + wildtopics[5], options=SubscribeOptions(2, retainHandling=1)) + lacallback.wait_subscribed() + + msgs = lacallback.get_messages(3) + + self.assertTrue(lacallback.messages.empty()) + qoss = [x["message"].qos for x in msgs] + self.assertTrue(1 in qoss and 2 in qoss and 0 in qoss, qoss) + lacallback.clear() + laclient.subscribe( + wildtopics[5], options=SubscribeOptions(2, retainHandling=1)) + lacallback.wait_subscribed() + time.sleep(1) + self.assertTrue(lacallback.messages.empty()) + + # remove that subscription + properties = Properties(PacketTypes.UNSUBSCRIBE) + properties.UserProperty = ("a", "2") + properties.UserProperty = ("c", "3") + laclient.unsubscribe(wildtopics[5], properties) + lacallback.wait_unsubscribed() + + # check that we really did remove that subscription + laclient.subscribe( + wildtopics[5], options=SubscribeOptions(2, retainHandling=1)) + lacallback.wait_subscribed() + msgs = lacallback.get_messages(3) + qoss = [x["message"].qos for x in msgs] + self.assertTrue(1 in qoss and 2 in qoss and 0 in qoss, qoss) + lacallback.clear() + laclient.subscribe( + wildtopics[5], options=SubscribeOptions(2, retainHandling=1)) + lacallback.wait_subscribed() + time.sleep(WAIT_NON_EVENT_TIMEOUT) + self.assertTrue(lacallback.messages.empty()) + + # remove that subscription + properties = Properties(PacketTypes.UNSUBSCRIBE) + properties.UserProperty = ("a", "2") + properties.UserProperty = ("c", "3") + laclient.unsubscribe(wildtopics[5], properties) + lacallback.wait_unsubscribed() + + lacallback.clear() + laclient.subscribe( + wildtopics[5], options=SubscribeOptions(2, retainHandling=2)) + lacallback.wait_subscribed() + self.assertTrue(lacallback.messages.empty()) + laclient.subscribe( + wildtopics[5], options=SubscribeOptions(2, retainHandling=2)) + lacallback.wait_subscribed() + self.assertTrue(lacallback.messages.empty()) + + # remove that subscription + laclient.unsubscribe(wildtopics[5]) + lacallback.wait_unsubscribed() + + laclient.subscribe( + wildtopics[5], options=SubscribeOptions(2, retainHandling=0)) + lacallback.wait_subscribed() + msgs = lacallback.get_messages(3) + qoss = [x["message"].qos for x in msgs] + self.assertTrue(1 in qoss and 2 in qoss and 0 in qoss, qoss) + lacallback.clear() + laclient.subscribe( + wildtopics[5], options=SubscribeOptions(2, retainHandling=0)) + msgs = lacallback.get_messages(3) + qoss = [x["message"].qos for x in msgs] + self.assertTrue(1 in qoss and 2 in qoss and 0 in qoss, qoss) + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + + cleanRetained(self._test_broker_port) + + def test_subscription_identifiers(self): + clientid = 'subscription identifiers' + + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port) + lacallback.wait_connected() + laclient.loop_start() + + sub_properties = Properties(PacketTypes.SUBSCRIBE) + sub_properties.SubscriptionIdentifier = 456789 + laclient.subscribe(topics[0], qos=2, properties=sub_properties) + lacallback.wait_subscribed() + + lbclient, lbcallback = self.new_client(f"{clientid} b") + lbclient.connect(host="localhost", port=self._test_broker_port) + lbcallback.wait_connected() + lbclient.loop_start() + sub_properties = Properties(PacketTypes.SUBSCRIBE) + sub_properties.SubscriptionIdentifier = 2 + lbclient.subscribe(topics[0], qos=2, properties=sub_properties) + lbcallback.wait_subscribed() + + sub_properties.clear() + sub_properties.SubscriptionIdentifier = 3 + lbclient.subscribe(f"{topics[0]}/#", qos=2, properties=sub_properties) + + lbclient.publish(topics[0], b"sub identifier test", 1, retain=False) + + msg = lacallback.messages.get(timeout=DEFAULT_TIMEOUT) + self.assertTrue(lacallback.messages.empty(), lacallback.messages.queue) + self.assertEqual(msg["message"].properties.SubscriptionIdentifier[0], + 456789, msg["message"].properties.SubscriptionIdentifier) + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + + msg = lbcallback.messages.get(timeout=DEFAULT_TIMEOUT) + self.assertTrue(lbcallback.messages.empty(), lbcallback.messages.queue) + expected_subsids = {2, 3} + received_subsids = set( + msg["message"].properties.SubscriptionIdentifier) + self.assertEqual(received_subsids, expected_subsids, received_subsids) + lbclient.disconnect() + lbcallback.wait_disconnected() + lbclient.loop_stop() + + def test_request_response(self): + clientid = 'request response' + + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port) + lacallback.wait_connected() + laclient.loop_start() + + lbclient, lbcallback = self.new_client(f"{clientid} b") + lbclient.connect(host="localhost", port=self._test_broker_port) + lbcallback.wait_connected() + lbclient.loop_start() + + laclient.subscribe( + topics[0], options=SubscribeOptions(2, noLocal=True)) + lacallback.wait_subscribed() + + lbclient.subscribe( + topics[0], options=SubscribeOptions(2, noLocal=True)) + lbcallback.wait_subscribed() + + publish_properties = Properties(PacketTypes.PUBLISH) + publish_properties.ResponseTopic = topics[0] + publish_properties.CorrelationData = b"334" + # client a is the requester + laclient.publish(topics[0], b"request", 1, + properties=publish_properties) + + # client b is the responder + msg = lbcallback.messages.get(timeout=DEFAULT_TIMEOUT) + self.assertEqual(msg["message"].properties.ResponseTopic, topics[0], + msg["message"].properties) + self.assertEqual(msg["message"].properties.CorrelationData, b"334", + msg["message"].properties) + + lbclient.publish(msg["message"].properties.ResponseTopic, b"response", 1, + properties=msg["message"].properties) + + # client a gets the response + lacallback.messages.get(timeout=DEFAULT_TIMEOUT) + + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + lbclient.disconnect() + lbcallback.wait_disconnected() + lbclient.loop_stop() + + def test_client_topic_alias(self): + clientid = 'client topic alias' + + connect_properties = Properties(PacketTypes.CONNECT) + connect_properties.TopicAliasMaximum = 0 # server topic aliases not allowed + connect_properties.SessionExpiryInterval = 99999 + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port, properties=connect_properties) + connack = lacallback.wait_connected() + clientTopicAliasMaximum = 0 + if hasattr(connack["properties"], "TopicAliasMaximum"): + clientTopicAliasMaximum = connack["properties"].TopicAliasMaximum + + if clientTopicAliasMaximum == 0: + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + return + + laclient.subscribe(topics[0], qos=2) + lacallback.wait_subscribed() + + publish_properties = Properties(PacketTypes.PUBLISH) + publish_properties.TopicAlias = 1 + laclient.publish(topics[0], b"topic alias 1", + 1, properties=publish_properties) + lacallback.messages.get(timeout=DEFAULT_TIMEOUT) + + laclient.publish("", b"topic alias 2", 1, + properties=publish_properties) + lacallback.messages.get(timeout=DEFAULT_TIMEOUT) + + laclient.disconnect() # should get rid of the topic aliases but not subscriptions + lacallback.wait_disconnected() + laclient.loop_stop() + + # check aliases have been deleted + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port, clean_start=False, + properties=connect_properties) + + laclient.publish(topics[0], b"topic alias 3", 1) + lacallback.messages.get(timeout=DEFAULT_TIMEOUT) + + publish_properties = Properties(PacketTypes.PUBLISH) + publish_properties.TopicAlias = 1 + laclient.publish("", b"topic alias 4", 1, + properties=publish_properties) + + # should get back a disconnect with Topic alias invalid + lacallback.wait_disconnected() + laclient.loop_stop() + + def test_server_topic_alias(self): + clientid = 'server topic alias' + + serverTopicAliasMaximum = 1 # server topic alias allowed + connect_properties = Properties(PacketTypes.CONNECT) + connect_properties.TopicAliasMaximum = serverTopicAliasMaximum + + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port, properties=connect_properties) + lacallback.wait_connected() + laclient.loop_start() + + laclient.subscribe(topics[0], qos=2) + lacallback.wait_subscribed() + + for qos in range(3): + laclient.publish(topics[0], b"topic alias 1", qos) + msgs = lacallback.get_messages(3) + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + + # first message should set the topic alias + self.assertTrue(hasattr( + msgs[0]["message"].properties, "TopicAlias"), msgs[0]["message"].properties) + topicalias = msgs[0]["message"].properties.TopicAlias + + self.assertTrue(topicalias > 0) + self.assertEqual(msgs[0]["message"].topic, topics[0]) + + self.assertEqual( + msgs[1]["message"].properties.TopicAlias, topicalias) + self.assertEqual(msgs[1]["message"].topic, "") + + self.assertEqual( + msgs[2]["message"].properties.TopicAlias, topicalias) + self.assertEqual(msgs[2]["message"].topic, "") + + serverTopicAliasMaximum = 0 # no server topic alias allowed + connect_properties = Properties(PacketTypes.CONNECT) + # connect_properties.TopicAliasMaximum = serverTopicAliasMaximum # default is 0 + + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port, properties=connect_properties) + lacallback.wait_connected() + laclient.loop_start() + + laclient.subscribe(topics[0], qos=2) + lacallback.wait_subscribed() + + for qos in range(3): + laclient.publish(topics[0], b"topic alias 2", qos) + msgs = lacallback.get_messages(3) + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + + # No topic aliases + self.assertFalse(hasattr( + msgs[0]["message"].properties, "TopicAlias"), msgs[0]["message"].properties) + self.assertFalse(hasattr( + msgs[1]["message"].properties, "TopicAlias"), msgs[1]["message"].properties) + self.assertFalse(hasattr( + msgs[2]["message"].properties, "TopicAlias"), msgs[2]["message"].properties) + + serverTopicAliasMaximum = 0 # no server topic alias allowed + connect_properties = Properties(PacketTypes.CONNECT) + connect_properties.TopicAliasMaximum = serverTopicAliasMaximum # default is 0 + + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port, properties=connect_properties) + lacallback.wait_connected() + laclient.loop_start() + + laclient.subscribe(topics[0], qos=2) + lacallback.wait_subscribed() + + for qos in range(3): + laclient.publish(topics[0], b"topic alias 3", qos) + msgs = lacallback.get_messages(3) + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + + # No topic aliases + self.assertFalse(hasattr( + msgs[0]["message"].properties, "TopicAlias"), msgs[0]["message"].properties) + self.assertFalse(hasattr( + msgs[1]["message"].properties, "TopicAlias"), msgs[1]["message"].properties) + self.assertFalse(hasattr( + msgs[2]["message"].properties, "TopicAlias"), msgs[2]["message"].properties) + + def test_maximum_packet_size(self): + clientid = 'maximum packet size' + + # 1. server max packet size + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port) + connack = lacallback.wait_connected() + laclient.loop_start() + + serverMaximumPacketSize = 2**28-1 + if hasattr(connack["properties"], "MaximumPacketSize"): + serverMaximumPacketSize = connack["properties"].MaximumPacketSize + + if serverMaximumPacketSize < 65535: + # publish bigger packet than server can accept + payload = b"."*serverMaximumPacketSize + laclient.publish(topics[0], payload, 0) + # should get back a disconnect with packet size too big + response = lacallback.wait_disconnected() + self.assertEqual(response["reasonCode"].getName(), + "Packet too large", response["reasonCode"].getName()) + else: + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + + # 1. client max packet size + maximumPacketSize = 64 # max packet size we want to receive + connect_properties = Properties(PacketTypes.CONNECT) + connect_properties.MaximumPacketSize = maximumPacketSize + + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port, properties=connect_properties) + connack = lacallback.wait_connected() + laclient.loop_start() + + serverMaximumPacketSize = 2**28-1 + if hasattr(connack["properties"], "MaximumPacketSize"): + serverMaximumPacketSize = connack["properties"].MaximumPacketSize + + laclient.subscribe(topics[0], qos=2) + response = lacallback.wait_subscribed() + + # send a small enough packet, should get this one back + payload = b"."*(int(maximumPacketSize/2)) + laclient.publish(topics[0], payload, 0) + lacallback.messages.get(timeout=DEFAULT_TIMEOUT) + + # send a packet too big to receive + payload = b"."*maximumPacketSize + laclient.publish(topics[0], payload, 1) + try: + lacallback.messages.get(timeout=WAIT_NON_EVENT_TIMEOUT) + raise ValueError("unexpected message received") + except queue.Empty: + pass + + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + + """ + def test_server_keep_alive(self): + clientid = 'server keep alive' + + laclient, lacallback = self.new_client(clientid+" a") + laclient.connect(host="localhost", port=self._test_broker_port) + connack = lacallback.wait_connected() + laclient.loop_start() + + self.assertTrue(hasattr(connack["properties"], "ServerKeepAlive")) + self.assertEqual(connack["properties"].ServerKeepAlive, 60) + + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + """ + + def test_will_delay(self): + # the will message should be received earlier than the session expiry + + clientid = 'will delay' + + will_properties = Properties(PacketTypes.WILLMESSAGE) + connect_properties = Properties(PacketTypes.CONNECT) + + # set the will delay and session expiry to the same value - + # then both should occur at the same time + will_properties.WillDelayInterval = 3 # in seconds + connect_properties.SessionExpiryInterval = 5 + + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.will_set( + topics[0], payload=b"test_will_delay will message", properties=will_properties) + laclient.connect(host="localhost", port=self._test_broker_port, properties=connect_properties) + connack = lacallback.wait_connected() + self.assertEqual(connack["reasonCode"].getName(), "Success") + self.assertEqual(connack["flags"]["session present"], False) + laclient.loop_start() + + lbclient, lbcallback = self.new_client(f"{clientid} b") + lbclient.connect(host="localhost", port=self._test_broker_port, properties=connect_properties) + connack = lbcallback.wait_connected() + lbclient.loop_start() + # subscribe to will message topic + lbclient.subscribe(topics[0], qos=2) + lbcallback.wait_subscribed() + + # abort client a and wait for the will message + laclient.loop_stop() + laclient.socket().close() + start = time.time() + msg = lbcallback.messages.get(DEFAULT_TIMEOUT) + duration = time.time() - start + self.assertAlmostEqual(duration, 4, delta=1) + self.assertEqual(msg["message"].topic, topics[0]) + self.assertEqual( + msg["message"].payload, b"test_will_delay will message") + + lbclient.disconnect() + lbcallback.wait_disconnected() + lbclient.loop_stop() + + def test_shared_subscriptions(self): + clientid = 'shared subscriptions' + + shared_sub_topic = f"$share/sharename/{topic_prefix}x" + shared_pub_topic = f"{topic_prefix}x" + + laclient, lacallback = self.new_client(f"{clientid} a") + laclient.connect(host="localhost", port=self._test_broker_port) + connack = lacallback.wait_connected() + laclient.loop_start() + + self.assertEqual(connack["reasonCode"].getName(), "Success") + self.assertEqual(connack["flags"]["session present"], False) + + laclient.subscribe( + [(shared_sub_topic, SubscribeOptions(2)), (topics[0], SubscribeOptions(2))]) + lacallback.wait_subscribed() + + lbclient, lbcallback = self.new_client(f"{clientid} b") + lbclient.connect(host="localhost", port=self._test_broker_port) + connack = lbcallback.wait_connected() + lbclient.loop_start() + + self.assertEqual(connack["reasonCode"].getName(), "Success") + self.assertEqual(connack["flags"]["session present"], False) + + lbclient.subscribe( + [(shared_sub_topic, SubscribeOptions(2)), (topics[0], 2)]) + lbcallback.wait_subscribed() + + lacallback.clear() + lbcallback.clear() + + count = 1 + for i in range(count): + lbclient.publish(topics[0], f"message {i}", 0) + + lacallback.get_messages(count) + lbcallback.get_messages(count) + + self.assertTrue(lacallback.messages.empty()) + self.assertTrue(lbcallback.messages.empty()) + + lacallback.clear() + lbcallback.clear() + + for i in range(count): + lbclient.publish(shared_pub_topic, f"message {i}", 0) + # Each message should only be received once + result = [] + deadline = time.time() + DEFAULT_TIMEOUT + while len(result) < count and time.time() < deadline: + get_timeout = deadline - time.time() + try: + if get_timeout <= 0: + result.append(lacallback.messages.get_nowait()) + else: + result.append(lacallback.messages.get(timeout=get_timeout)) + except queue.Empty: + # The message could be sent to other client, so empty queue + # could be normal + pass + + try: + get_timeout = deadline - time.time() + if get_timeout <= 0: + result.append(lbcallback.messages.get_nowait()) + else: + result.append(lbcallback.messages.get(timeout=get_timeout)) + except queue.Empty: + # The message could be sent to other client, so empty queue + # could be normal + pass + + self.assertEqual( + {x["message"].payload for x in result}, + {f"message {i}".encode() for i in range(count)} + ) + + laclient.disconnect() + lacallback.wait_disconnected() + laclient.loop_stop() + + lbclient.disconnect() + lbcallback.wait_disconnected() + lbclient.loop_stop() + + +def setData(): + global topics, wildtopics, nosubscribe_topics, topic_prefix + topics = ("TopicA", "TopicA/B", "Topic/C", "TopicA/C", "/TopicA") + wildtopics = ("TopicA/+", "+/C", "#", "/#", "/+", "+/+", "TopicA/#") + nosubscribe_topics = ("test/nosubscribe",) + topic_prefix = "paho.mqtt.client.mqttv5/" diff --git a/phaoUtils/tests/test_reasoncodes.py b/phaoUtils/tests/test_reasoncodes.py new file mode 100644 index 0000000..24f7ff3 --- /dev/null +++ b/phaoUtils/tests/test_reasoncodes.py @@ -0,0 +1,47 @@ +import pytest +from paho.mqtt.packettypes import PacketTypes +from paho.mqtt.reasoncodes import ReasonCode, ReasonCodes + + +class TestReasonCode: + def test_equality(self): + rc_success = ReasonCode(PacketTypes.CONNACK, "Success") + assert rc_success == 0 + assert rc_success == "Success" + assert rc_success != "Protocol error" + assert rc_success == ReasonCode(PacketTypes.CONNACK, "Success") + + rc_protocol_error = ReasonCode(PacketTypes.CONNACK, "Protocol error") + assert rc_protocol_error == 130 + assert rc_protocol_error == "Protocol error" + assert rc_protocol_error != "Success" + assert rc_protocol_error == ReasonCode(PacketTypes.CONNACK, "Protocol error") + + def test_comparison(self): + rc_success = ReasonCode(PacketTypes.CONNACK, "Success") + rc_protocol_error = ReasonCode(PacketTypes.CONNACK, "Protocol error") + + assert not rc_success > 0 + assert rc_protocol_error > 0 + assert not rc_success != 0 + assert rc_protocol_error != 0 + + def test_compatibility(self): + rc_success = ReasonCode(PacketTypes.CONNACK, "Success") + with pytest.deprecated_call(): + rc_success_old = ReasonCodes(PacketTypes.CONNACK, "Success") + assert rc_success == rc_success_old + + assert isinstance(rc_success, ReasonCode) + assert isinstance(rc_success_old, ReasonCodes) + # User might use isinstance with the old name (plural) + # while the library give them a ReasonCode (singular) in the callbacks + assert isinstance(rc_success, ReasonCodes) + # The other way around is probably never used... but still support it + assert isinstance(rc_success_old, ReasonCode) + + # Check that isinstance implementation don't always return True + assert not isinstance(rc_success, dict) + assert not isinstance(rc_success_old, dict) + assert not isinstance({}, ReasonCode) + assert not isinstance({}, ReasonCodes) diff --git a/phaoUtils/tests/test_websocket_integration.py b/phaoUtils/tests/test_websocket_integration.py new file mode 100644 index 0000000..b44f347 --- /dev/null +++ b/phaoUtils/tests/test_websocket_integration.py @@ -0,0 +1,257 @@ +import base64 +import hashlib +import re +import socketserver +from collections import OrderedDict + +import paho.mqtt.client as client +import pytest +from paho.mqtt.client import WebsocketConnectionError + +from tests.testsupport.broker import fake_websocket_broker # noqa: F401 + + +@pytest.fixture +def init_response_headers(): + # "Normal" websocket response from server + response_headers = OrderedDict([ + ("Upgrade", "websocket"), + ("Connection", "Upgrade"), + ("Sec-WebSocket-Accept", "testwebsocketkey"), + ("Sec-WebSocket-Protocol", "chat"), + ]) + + return response_headers + + +def get_websocket_response(response_headers): + """ Takes headers and constructs HTTP response + + 'HTTP/1.1 101 Switching Protocols' is the headers for the response, + as expected in client.py + """ + response = "\r\n".join([ + "HTTP/1.1 101 Switching Protocols", + "\r\n".join(f"{i}: {j}" for i, j in response_headers.items()), + "\r\n", + ]).encode("utf8") + + return response + + +@pytest.mark.parametrize("proto_ver,proto_name", [ + (client.MQTTv31, "MQIsdp"), + (client.MQTTv311, "MQTT"), +]) +class TestInvalidWebsocketResponse: + def test_unexpected_response(self, proto_ver, proto_name, fake_websocket_broker): + """ Server responds with a valid code, but it's not what the client expected """ + + mqttc = client.Client( + client.CallbackAPIVersion.VERSION1, + "test_unexpected_response", + protocol=proto_ver, + transport="websockets" + ) + + class WebsocketHandler(socketserver.BaseRequestHandler): + def handle(_self): + # Respond with data passed in to serve() + _self.request.sendall(b"200 OK") + + with fake_websocket_broker.serve(WebsocketHandler), pytest.raises(WebsocketConnectionError) as exc: + mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10) + + assert str(exc.value) == "WebSocket handshake error" + + +@pytest.mark.parametrize("proto_ver,proto_name", [ + (client.MQTTv31, "MQIsdp"), + (client.MQTTv311, "MQTT"), +]) +class TestBadWebsocketHeaders: + """ Testing for basic functionality in checking for headers """ + + def _get_basic_handler(self, response_headers): + """ Get a basic BaseRequestHandler which returns the information in + self._response_headers + """ + + response = get_websocket_response(response_headers) + + class WebsocketHandler(socketserver.BaseRequestHandler): + def handle(_self): + self.data = _self.request.recv(1024).strip() + print('Received', self.data.decode('utf8')) + # Respond with data passed in to serve() + _self.request.sendall(response) + + return WebsocketHandler + + def test_no_upgrade(self, proto_ver, proto_name, fake_websocket_broker, + init_response_headers): + """ Server doesn't respond with 'connection: upgrade' """ + + mqttc = client.Client( + client.CallbackAPIVersion.VERSION1, + "test_no_upgrade", + protocol=proto_ver, + transport="websockets" + ) + + init_response_headers["Connection"] = "bad" + response = self._get_basic_handler(init_response_headers) + + with fake_websocket_broker.serve(response), pytest.raises(WebsocketConnectionError) as exc: + mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10) + + assert str(exc.value) == "WebSocket handshake error, connection not upgraded" + + def test_bad_secret_key(self, proto_ver, proto_name, fake_websocket_broker, + init_response_headers): + """ Server doesn't give anything after connection: upgrade """ + + mqttc = client.Client( + client.CallbackAPIVersion.VERSION1, + "test_bad_secret_key", + protocol=proto_ver, + transport="websockets" + ) + + response = self._get_basic_handler(init_response_headers) + + with fake_websocket_broker.serve(response), pytest.raises(WebsocketConnectionError) as exc: + mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10) + + assert str(exc.value) == "WebSocket handshake error, invalid secret key" + + +@pytest.mark.parametrize("proto_ver,proto_name", [ + (client.MQTTv31, "MQIsdp"), + (client.MQTTv311, "MQTT"), +]) +class TestValidHeaders: + """ Testing for functionality in request/response headers """ + + def _get_callback_handler(self, response_headers, check_request=None): + """ Get a basic BaseRequestHandler which returns the information in + self._response_headers + """ + + class WebsocketHandler(socketserver.BaseRequestHandler): + def handle(_self): + self.data = _self.request.recv(1024).strip() + print('Received', self.data.decode('utf8')) + + decoded = self.data.decode("utf8") + + if check_request is not None: + check_request(decoded) + + # Create server hash + GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + key = re.search("sec-websocket-key: ([A-Za-z0-9+/=]*)", decoded, re.IGNORECASE).group(1) + + to_hash = f"{key:s}{GUID:s}" + hashed = hashlib.sha1(to_hash.encode("utf8")) # noqa: S324 + encoded = base64.b64encode(hashed.digest()).decode("utf8") + + response_headers["Sec-WebSocket-Accept"] = encoded + + # Respond with the correct hash + response = get_websocket_response(response_headers) + + _self.request.sendall(response) + + return WebsocketHandler + + def test_successful_connection(self, proto_ver, proto_name, + fake_websocket_broker, + init_response_headers): + """ Connect successfully, on correct path """ + + mqttc = client.Client( + client.CallbackAPIVersion.VERSION1, + "test_successful_connection", + protocol=proto_ver, + transport="websockets" + ) + + response = self._get_callback_handler(init_response_headers) + + with fake_websocket_broker.serve(response): + mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10) + + mqttc.disconnect() + + @pytest.mark.parametrize("mqtt_path", [ + "/mqtt" + "/special", + None, + ]) + def test_correct_path(self, proto_ver, proto_name, fake_websocket_broker, + mqtt_path, init_response_headers): + """ Make sure it can connect on user specified paths """ + + mqttc = client.Client( + client.CallbackAPIVersion.VERSION1, + "test_correct_path", + protocol=proto_ver, + transport="websockets" + ) + + mqttc.ws_set_options( + path=mqtt_path, + ) + + def check_path_correct(decoded): + # Make sure it connects to the right path + if mqtt_path: + assert re.search(f"GET {mqtt_path} HTTP/1.1", decoded, re.IGNORECASE) is not None + + response = self._get_callback_handler( + init_response_headers, + check_request=check_path_correct, + ) + + with fake_websocket_broker.serve(response): + mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10) + + mqttc.disconnect() + + @pytest.mark.parametrize("auth_headers", [ + {"Authorization": "test123"}, + {"Authorization": "test123", "auth2": "abcdef"}, + # Won't be checked, but make sure it still works even if the user passes it + None, + ]) + def test_correct_auth(self, proto_ver, proto_name, fake_websocket_broker, + auth_headers, init_response_headers): + """ Make sure it sends the right auth headers """ + + mqttc = client.Client( + client.CallbackAPIVersion.VERSION1, + "test_correct_path", + protocol=proto_ver, + transport="websockets" + ) + + mqttc.ws_set_options( + headers=auth_headers, + ) + + def check_headers_used(decoded): + # Make sure it connects to the right path + if auth_headers: + for k, v in auth_headers.items(): + assert f"{k}: {v}" in decoded + + response = self._get_callback_handler( + init_response_headers, + check_request=check_headers_used, + ) + + with fake_websocket_broker.serve(response): + mqttc.connect("localhost", fake_websocket_broker.port, keepalive=10) + + mqttc.disconnect() diff --git a/phaoUtils/tests/test_websockets.py b/phaoUtils/tests/test_websockets.py new file mode 100644 index 0000000..78a7cd4 --- /dev/null +++ b/phaoUtils/tests/test_websockets.py @@ -0,0 +1,142 @@ +import socket +from unittest.mock import Mock + +import pytest +from paho.mqtt.client import WebsocketConnectionError, _WebsocketWrapper + + +class TestHeaders: + """ Make sure headers are used correctly """ + + @pytest.mark.parametrize("wargs,expected_sent", [ + ( + # HTTPS on non-default port + { + "host": "testhost.com", + "port": 1234, + "path": "/mqtt", + "extra_headers": None, + "is_ssl": True, + }, + [ + "GET /mqtt HTTP/1.1", + "Host: testhost.com:1234", + "Upgrade: websocket", + "Connection: Upgrade", + "Sec-Websocket-Protocol: mqtt", + "Sec-Websocket-Version: 13", + "Origin: https://testhost.com:1234", + ], + ), + ( + # HTTPS on default port + { + "host": "testhost.com", + "port": 443, + "path": "/mqtt", + "extra_headers": None, + "is_ssl": True, + }, + [ + "GET /mqtt HTTP/1.1", + "Host: testhost.com", + "Upgrade: websocket", + "Connection: Upgrade", + "Sec-Websocket-Protocol: mqtt", + "Sec-Websocket-Version: 13", + "Origin: https://testhost.com", + ], + ), + ( + # HTTP on default port + { + "host": "testhost.com", + "port": 80, + "path": "/mqtt", + "extra_headers": None, + "is_ssl": False, + }, + [ + "GET /mqtt HTTP/1.1", + "Host: testhost.com", + "Upgrade: websocket", + "Connection: Upgrade", + "Sec-Websocket-Protocol: mqtt", + "Sec-Websocket-Version: 13", + "Origin: http://testhost.com", + ], + ), + ( + # HTTP on non-default port + { + "host": "testhost.com", + "port": 443, # This isn't the default *HTTP* port. It's on purpose to use httpS port + "path": "/mqtt", + "extra_headers": None, + "is_ssl": False, + }, + [ + "GET /mqtt HTTP/1.1", + "Host: testhost.com:443", + "Upgrade: websocket", + "Connection: Upgrade", + "Sec-Websocket-Protocol: mqtt", + "Sec-Websocket-Version: 13", + "Origin: http://testhost.com:443", + ], + ), + ]) + def test_normal_headers(self, wargs, expected_sent): + """ Normal headers as specified in RFC 6455 """ + + response = [ + "HTTP/1.1 101 Switching Protocols", + "Upgrade: websocket", + "Connection: Upgrade", + "Sec-WebSocket-Accept: badreturnvalue=", + "Sec-WebSocket-Protocol: chat", + "\r\n", + ] + + def iter_response(): + for i in "\r\n".join(response).encode("utf8"): + yield i + + for i in b"\r\n": + yield i + + it = iter_response() + + def fakerecv(*args): + return bytes([next(it)]) + + mocksock = Mock( + spec_set=socket.socket, + recv=fakerecv, + send=Mock(), + ) + + # Do a copy to avoid modifying input + wargs_with_socket = dict(wargs) + wargs_with_socket["socket"] = mocksock + + with pytest.raises(WebsocketConnectionError) as exc: + _WebsocketWrapper(**wargs_with_socket) + + # We're not creating the response hash properly so it should raise this + # error + assert str(exc.value) == "WebSocket handshake error, invalid secret key" + + # Only sends the header once + assert mocksock.send.call_count == 1 + + got_lines = mocksock.send.call_args[0][0].decode("utf8").splitlines() + + # First line must be the GET line + # 2nd line is required to be Host (rfc9110 said that it SHOULD be first header) + assert expected_sent[0] == got_lines[0] + assert expected_sent[1] == got_lines[1] + + # Other line order don't matter + for line in expected_sent: + assert line in got_lines diff --git a/phaoUtils/tests/testsupport/__init__.py b/phaoUtils/tests/testsupport/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/phaoUtils/tests/testsupport/broker.py b/phaoUtils/tests/testsupport/broker.py new file mode 100644 index 0000000..e08cf73 --- /dev/null +++ b/phaoUtils/tests/testsupport/broker.py @@ -0,0 +1,130 @@ +import contextlib +import os +import socket +import socketserver +import threading + +import pytest + +from tests import paho_test + + +class FakeBroker: + def __init__(self, transport): + if transport == "tcp": + # Bind to "localhost" for maximum performance, as described in: + # http://docs.python.org/howto/sockets.html#ipc + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(("localhost", 0)) + self.port = sock.getsockname()[1] + elif transport == "unix": + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind("localhost") + self.port = 1883 + else: + raise ValueError(f"unsupported transport {transport}") + + sock.settimeout(5) + sock.listen(1) + + self._sock = sock + self._conn = None + self.transport = transport + + def start(self): + if self._sock is None: + raise ValueError('Socket is not open') + + (conn, address) = self._sock.accept() + conn.settimeout(5) + self._conn = conn + + def finish(self): + if self._conn is not None: + self._conn.close() + self._conn = None + + if self._sock is not None: + self._sock.close() + self._sock = None + + if self.transport == 'unix': + try: + os.unlink('localhost') + except OSError: + pass + + def receive_packet(self, num_bytes): + if self._conn is None: + raise ValueError('Connection is not open') + + packet_in = self._conn.recv(num_bytes) + return packet_in + + def send_packet(self, packet_out): + if self._conn is None: + raise ValueError('Connection is not open') + + count = self._conn.send(packet_out) + return count + + def expect_packet(self, name, packet): + if self._conn is None: + raise ValueError('Connection is not open') + + paho_test.expect_packet(self._conn, name, packet) + + +@pytest.fixture(params=["tcp"] + (["unix"] if hasattr(socket, 'AF_UNIX') else [])) +def fake_broker(request): + # print('Setup broker') + broker = FakeBroker(request.param) + + yield broker + + # print('Teardown broker') + broker.finish() + + +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + pass + + +class FakeWebsocketBroker(threading.Thread): + def __init__(self): + super().__init__() + + self.host = "localhost" + self.port = -1 # Will be set by `serve()` + + self._server = None + self._running = True + self.handler_cls = False + + @contextlib.contextmanager + def serve(self, tcphandler): + self._server = ThreadedTCPServer((self.host, 0), tcphandler) + + try: + self.start() + self.port = self._server.server_address[1] + + if not self._running: + raise RuntimeError("Error starting server") + yield + finally: + if self._server: + self._server.shutdown() + self._server.server_close() + + def run(self): + self._running = True + self._server.serve_forever() + + +@pytest.fixture +def fake_websocket_broker(): + broker = FakeWebsocketBroker() + + yield broker diff --git a/pyserial-master.zip b/pyserial-master.zip new file mode 100644 index 0000000000000000000000000000000000000000..1074a7854dce79bc825472f07ea817bae91a686d GIT binary patch literal 202391 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%0Axkqm;3Ui1Hb=v$n%@seY5}`yt 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 QSVT}*KvaxZ04xCd9~Z4jEdT%j literal 0 HcmV?d00001 diff --git a/pyserial-master/pyserial-master/.gitignore b/pyserial-master/pyserial-master/.gitignore new file mode 100644 index 0000000..fdbfde0 --- /dev/null +++ b/pyserial-master/pyserial-master/.gitignore @@ -0,0 +1,13 @@ +**/__pycache__ +*.pyc +*.pyo +documentation/_build +build +dist +*.egg-info + +/MANIFEST + +.idea +venv +xxx* diff --git a/pyserial-master/pyserial-master/.travis.yml b/pyserial-master/pyserial-master/.travis.yml new file mode 100644 index 0000000..ff48704 --- /dev/null +++ b/pyserial-master/pyserial-master/.travis.yml @@ -0,0 +1,16 @@ +# Copyright Roger Meier +# SPDX-License-Identifier: BSD-3-Clause + +language: python + +python: + - 2.7 + - 3.4 + - 3.5 + - 3.6 + - pypy + - pypy3 + +script: + - python setup.py install + - python test/run_all_tests.py loop:// diff --git a/pyserial-master/pyserial-master/CHANGES.rst b/pyserial-master/pyserial-master/CHANGES.rst new file mode 100644 index 0000000..ab5a1d5 --- /dev/null +++ b/pyserial-master/pyserial-master/CHANGES.rst @@ -0,0 +1,825 @@ +======================== + pySerial Release Notes +======================== + +Version 1.0 13 Feb 2002 +--------------------------- +- First public release. +- Split from the pybsl application (see http://mspgcc.sourceforge.net) + +New Features: + +- Added Jython support + + +Version 1.1 14 Feb 2002 +--------------------------- +Bugfixes: + +- Win32, when not specifying a timeout +- Typos in the Docs + +New Features: + +- added ``serialutil`` which provides a base class for the ``Serial`` + objects. + +- ``readline``, ``readlines``, ``writelines`` and ``flush`` are now supported + see README.txt for deatils. + + +Version 1.11 14 Feb 2002 +--------------------------- +Same as 1.1 but added missing files. + + +Version 1.12 18 Feb 2002 +--------------------------- +Removed unneeded constants to fix RH7.x problems. + + +Version 1.13 09 Apr 2002 +--------------------------- +Added alternate way for enabling rtscts (CNEW_RTSCTS is tried too) +If port opening fails, a ``SerialException`` is raised on all platforms + + +Version 1.14 29 May 2002 +--------------------------- +Added examples to archive +Added non-blocking mode for ``timeout=0`` (tnx Mat Martineau) + +Bugfixes: + +- win32 does now return the remaining characters on timeout + + +Version 1.15 04 Jun 2002 +--------------------------- +Bugfixes (win32): + +- removed debug messages +- compatibility to win9x improved + + +Version 1.16 02 Jul 2002 +--------------------------- +Added implementation of RI and corrected RTS/CTS on Win32 + + +Version 1.17 03 Jul 2002 +--------------------------- +Silly mix of two versions in win32 code corrected + + +Version 1.18 06 Dec 2002 +--------------------------- +Bugfixes (general): + +- remove the mapping of flush to the destructive flushOutput as + this is not the expected behaviour. +- readline: EOL character for lines can be chosen idea by + John Florian. + +Bugfixes (posix): + +- cygwin port numbering fixed +- test each and every constant for it's existence in termios module, + use default if not existent (fix for Bug item #640214) +- wrong exception on nonexistent ports with /dev file. bug report + by Louis Cordier + +Bugfixes (win32): + +- RTS/CTS handling as suggested in Bug #635072 +- bugfix of timeouts brought up by Markus Hoffrogge + + +Version 1.19 19 Mar 2003 +--------------------------- +Bugfixes (posix): + +- removed ``dgux`` entry which actually had a wrong comment and is + probably not in use anywhere. + +Bugfixes (win32): + +- added ``int()`` conversion, [Bug 702120] +- remove code to set control lines in close method of win32 + version. [Bug 669625] + + +Version 1.20 28 Aug 2003 +--------------------------- +- Added ``serial.device()`` for all platforms + +Bugfixes (win32): + +- don't recreate overlapped structures and events on each + read/write. +- don't set unneeded event masks. +- don't use DOS device names for ports > 9. +- remove send timeout (it's not used in the linux impl. anyway). + + +Version 1.21 30 Sep 2003 +--------------------------- +Bugfixes (win32): + +- name for COM10 was not built correctly, found by Norm Davis. + +Bugfixes (examples): + +- small change in ``miniterm.py`` that should mage it run on cygwin, + [Bug 809904] submitted by Rolf Campbell. + + +Version 2.0b1 1 Oct 2003 +--------------------------- +Transition to the Python 2.0 series: + +- New implementation only supports Python 2.2+, backwards compatibility + should be maintained almost everywhere. + The OS handles (like the ``hComPort`` or ``fd`` attribute) were prefixed + with an underscore. The different names stay, as anyone that uses one of + these has to write platform specific code anyway. +- Common base class ``serialutil.SerialBase`` for all implementations. +- ``PARITY_NONE``, ``PARITY_EVEN``, ``PARITY_ODD`` constants changed and all + these constants moved to ``serialutil.py`` (still available as + ``serial.PARITY_NONE`` etc. and they should be used that way) +- Added ``serial.PARITY_NAMES`` (implemented in ``serialutil.PARITY_NAMES``). + This dictionary can be used to convert parity constants to meaningful + strings. +- Each Serial class and instance has a list of supported values: + ``BAUDRATES``, ``BYTESIZES``, ``PARITIES``, ``STOPBITS``Ggg + (i.e. ``serial.Serial.BAUDRATES or s = serial.Serial; s.BAUDRATES``) + these values can be used to fill in value sin GUI dialogs etc. +- Creating a ``Serial()`` object without port spec returns an unconfigured, + closed port. Useful if a GUI dialog should take a port and configure + it. +- New methods for ``serial.Serial`` instances: ``open()``, ``isOpen()`` +- A port can be opened and closed as many times as desired. +- Instances of ``serial.Serial`` have ``baudrate``, ``bytesize``, ``timeout`` + etc. attributes implemented as properties, all can be set while the port is + opened. It will then be reconfigured. +- Improved ``__doc__``'s. +- New ``test_advanced.py`` for the property setting/getting testing. +- Small bugfix on posix with get* methods (return value should be true a + boolean). +- added a ``__repr__`` that returns a meaningful string will all the serial + setting, easy for debugging. +- The serialposix module does not throw an exception on unsupported + platforms, the message is still printed. The idea that it may still + work even if the platform itself s not known, it simply tries to do + the posix stuff anyway (It's likely that opening ports by number + fails, but by name it should work). + + +Version 2.0b2 4 Oct 2003 +--------------------------- +- Added serial port configuration dialog for wxPython to the examples. +- Added terminal application for wxPython with wxGlade design file + to the examples. +- Jython support is currently broken as Jython does not have a Python 2.2 + compatible release out yet + + +Version 2.0 6 Nov 2003 +--------------------------- +- Fixes ``setup.py`` for older distutils + + +Version 2.1 28 Jul 2004 +--------------------------- +Bugfixes: + +- Fix XON/XOFF values [Bug 975250] + +Bugfixes (posix): + +- ``fd == 0`` fix from Vsevolod Lobko +- netbsd fixes from Erik Lindgren +- Dynamically lookup baudrates and some cleanups + +Bugfixes (examples): + +- CRLF handling of ``miniterm.py`` should be more consistent on Win32 + and others. Added LF only command line option +- Multithreading fixes to ``wxTerminal.py`` (helps with wxGTK) +- Small change for wxPython 2.5 in ``wxSerialConfigDialog.py`` [Bug 994856] + +New Features: + +- Implement write timeouts (``writeTimeout`` parameter) + + +Version 2.2 31 Jul 2005 +--------------------------- +Bugfixes: + +- [Bug 1014227]: property broken +- [Bug 1105687]: ``serial_tcp_example.py``: ``--localport`` option +- [Bug 1106313]: device (port) strings cannot be unicode + +Bugfixes (posix): + +- [Patch 1043436] Fix for [Bug 1043420] (OSError: EAGAIN) +- [Patch 1102700] ``fileno()`` added +- ensure disabled PARMRK + +Bugfixes (win32): + +- [Patch 983106]: keep RTS/CTS state on port setting changes + +New Features: + +- ``dsrdtr`` setting to enable/disable DSR/DTR flow control independently + from the ``rtscts`` setting. (Currently Win32 only, ignored on other + platforms) + + +Version 2.3 19 Jun 2008 +--------------------------- +New Features: + +- iterator interface. ``for line in Serial(...): ...`` is now possible + Suggested by Bernhard Bender +- ``sendBreak()`` accepts a ``duration`` argument. Default duration increased. +- win32 handles \\.\COMx format automatically for com ports of higher number + (COM10 is internally translated to \\.\COM10 etc.) +- miniterm.py has a new feature to send a file (upload) and configurable + special characters for exit and upload. Refactored internals to class based + structure (upload and class refactoring by Colin D Bennett) + +Bugfixes: + +- [Bug 1451535] TCP/serial redirect example "--help" +- update VERSION variable +- update wxSerialConfigDialog.py and wxTerminal.py compatibility with + wxPython 2.8 (Peleg) +- Check for string in write function. Using unicode causes errors, this + helps catching errors early (Tom Lynn) + +Bugfixes (posix): + +- [Bug 1554183] setRTS/setDTR reference to non existing local "on" +- [Bug 1513653] file descriptor not closed when exception is thrown +- FreeBSD now uses cuadX instead of cuaaX (Patrick Phalen) + +Bugfixes (win32): + +- [Bug 1520357] Handle leak +- [Bug 1679013] Ignore exception raised by SetCommTimeout() in close(). +- [Bug 1938118] process hang forever under XP + + +Version 2.4 6 Jul 2008 +--------------------------- +New Features: + +- [Patch 1616790] pyserial: Add inter-character timeout feature +- [Patch 1924805] add a setBreak function +- Add mark/space parity +- Add .NET/Mono backend (IronPython) + +Bugfixes (posix): + +- [Bug 1783159] Arbitrary baud rates (Linux/Posix) + +Bugfixes (win32): + +- [Patch 1561423] Add mark/space parity, Win32 +- [Bug 2000771] serial port CANNOT be specified by number on windows +- examples/scanwin32.py does no longer return \\.\ names +- fix \\.\ handling for some cases + +Bugfixes (jython): + + - The Jython backend tries javax.comm and gnu.io (Seo Sanghyeon) + + +Version 2.5-rc1 2009-07-30 +--------------------------- +New Features: + +- Python 3.x support (through 2to3) +- compatible with Python io library (Python 2.6+) +- Support for Win32 is now written on the top of ctypes (bundled with + Python 2.5+) instead of pywin32 (patch by Giovanni Bajo). +- 1.5 stop bits (STOPBITS_ONE_POINT_FIVE, implemented on all platforms) +- miniterm application extended (CTRL+T -> menu) +- miniterm.py is now installed as "script" +- add scanlinux.py example +- add port_publisher example +- experimental RFC-2217 server support (examples/rfc2217_server.py) +- add ``getSettingsDict`` and ``applySettingsDict`` serial object methods +- use a ``poll`` based implementation on Posix, instead of a ``select`` based, + provides better error handling [removed again in later releases]. + +Bugfixes: + +- Improve and fix tcp_serial_redirector example. +- [Bug 2603052] 5-bit mode (needs 1.5 stop bits in some cases) + +Bugfixes (posix): + +- [Bug 2810169] Propagate exceptions raised in serialposix _reconfigure +- [Bug 2562610] setting non standard baud rates on Darwin (Emmanuel Blot) + +Bugfixes (win32): + +- [Bug 2469098] parity PARITY_MARK, PARITY_SPACE isn't supported on win32 +- [SF 2446218] outWaiting implemented +- [Bug 2392892] scanwin32.py better exception handling +- [Bug 2505422] scanwin32.py Vista 64bit compatibility + + +Version 2.5-rc2 2010-01-02 +--------------------------- +New Features: + +- Documentation update, now written with Sphinx/ReST +- Updated miniterm.py example +- experimental RFC-2217 client support (serial.rfc2217.Serial, see docs) +- add ``loop://`` device for testing. +- add ``serial.serial_for_url`` factory function (support for native ports and + ``rfc2217``, ``socket`` and ``loop`` URLs) +- add new example: ``rfc2217_server.py`` +- tests live in their own directory now (no longer in examples) + +Bugfixes: + +- [Bug 2915810] Fix for suboption parsing in rfc2217 +- Packaging bug (missed some files) + +Bugfixes (posix): + +- improve write timeout behavior +- [Bug 2836297] move Linux specific constants to not break other platforms +- ``poll`` based implementation for ``read`` is in a separate class + ``PosixPollSerial``, as it is not supported well on all platforms (the + default ``Serial`` class uses select). +- changed error handling in ``read`` so that disconnected devices are + detected. + + +Bugfixes (win32): + +- [Bug 2886763] hComPort doesn't get initialized for Serial(port=None) + + +Version 2.5 2010-07-22 +--------------------------- +New Features: + +- [Bug 2976262] dsrdtr should default to False + ``dsrdtr`` parameter default value changed from ``None`` (follow ``rtscts`` + setting) to ``False``. This means ``rtscts=True`` enables hardware flow + control on RTS/CTS but no longer also on DTR/DSR. This change mostly + affects Win32 as on other platforms, that setting was ignored anyway. +- Improved xreadlines, it is now a generator function that yields lines as they + are received (previously it called readlines which would only return all + lines read after a read-timeout). However xreadlines is deprecated and not + available when the io module is used. Use ``for line in Serial(...):`` + instead. + +Bugfixes: + +- [Bug 2925854] test.py produces exception with python 3.1 +- [Bug 3029812] 2.5rc2 readline(s) doesn't work + +Bugfixes (posix): + +- [BUG 3006606] Nonblocking error - Unix platform + +Bugfixes (win32): + +- [Bug 2998169] Memory corruption at faster transmission speeds. + (bug introduced in 2.5-rc1) + + +Version 2.6 2011-11-02 +--------------------------- +New Features: + +- Moved some of the examples to serial.tools so that they can be used + with ``python -m`` +- serial port enumeration now included as ``serial.tools.list_ports`` +- URL handlers for ``serial_for_url`` are now imported dynamically. This allows + to add protocols w/o editing files. The list + ``serial.protocol_handler_packages`` can be used to add or remove user + packages with protocol handlers (see docs for details). +- new URL type: hwgrep:// uses list_ports module to search for ports + by their description +- several internal changes to improve Python 3.x compatibility (setup.py, + use of absolute imports and more) + +Bugfixes: + +- [Bug 3093882] calling open() on an already open port now raises an exception +- [Bug 3245627] connection-lost let rfc2217 hangs in closed loop +- [Patch 3147043] readlines() to support multi-character eol + +Bugfixes (posix): + +- [Patch 3316943] Avoid unneeded termios.tcsetattr calls in serialposix.py +- [Patch 2912349] Serial Scan as a Module with Mac Support + +Bugfixes (win32): + +- [Bug 3057499] writeTimeoutError when write Timeout is 0 +- [Bug 3414327] Character out of range in list_ports_windows +- [Patch 3036175] Windows 98 Support fix +- [Patch 3054352] RTS automatic toggle, for RS485 functionality. +- Fix type definitions for 64 bit Windows compatibility + + +Version 2.7 2013-10-17 +--------------------------- +- Win32: setRTS and setDTR can be called before the port is opened and it will + set the initial state on port open. +- Posix: add platform specific method: outWaiting (already present for Win32) +- Posix: rename flowControl to setXON to match name on Win32, add + flowControlOut function +- rfc2217: zero polls value (baudrate, data size, stop bits, parity) (Erik + Lundh) +- Posix: [Patch pyserial:28] Accept any speed on Linux [update] +- Posix: [Patch pyserial:29] PosixSerial.read() should "ignore" errno.EINTR +- OSX: [Patch pyserial:27] Scan by VendorID/Product ID for USB Serial devices +- Ensure working with bytes in write() calls + +Bugfixes: + +- [Bug 3540332] SerialException not returned +- [Bug pyserial:145] Error in socket_connection.py +- [Bug pyserial:135] reading from socket with timeout=None causes TypeError +- [Bug pyserial:130] setup.py should not append py3k to package name +- [Bug pyserial:117] no error on lost conn w/socket:// + +Bugfixes (posix): + +- [Patch 3462364] Fix: NameError: global name 'base' is not defined +- list_ports and device() for BSD updated (Anders Langworthy) +- [Bug 3518380] python3.2 -m serial.tools.list_ports error +- [Bug pyserial:137] Patch to add non-standard baudrates to Cygwin +- [Bug pyserial:141] open: Pass errno from IOError to SerialException +- [Bug pyserial:125] Undefined 'base' on list_ports_posix.py, function usb_lsusb +- [Bug pyserial:151] Serial.write() without a timeout uses 100% CPU on POSIX +- [Patch pyserial:30] [PATCH 1/1] serial.Serial() should not raise IOError. + +Bugfixes (win32): + +- [Bug 3444941] ctypes.WinError() unicode error +- [Bug 3550043] on Windows in tools global name 'GetLastError' is not defined +- [Bug pyserial:146] flush() does nothing in windows (despite docs) +- [Bug pyserial:144] com0com ports ignored due to missing "friendly name" +- [Bug pyserial:152] Cannot configure port, some setting was wrong. Can leave + port handle open but port not accessible + + +Version 3.0a0 2015-09-22 +-------------------------- +- Starting from this release, only Python 2.7 and 3.2 (or newer) are supported. + The source code is compatible to the 2.x and 3.x series without any changes. + The support for earlier Python versions than 2.7 is removed, please refer to + the pyserial-legacy (V2.x) series if older Python versions are a + requirement). +- Development moved to github, update links in docs. +- API changes: properties for ``rts``, ``dtr``, ``cts``, ``dsr``, ``cd``, ``ri``, + ``in_waiting`` (instead of get/set functions) +- remove file ``FileLike`` class, add ``read_until`` and ``iread_until`` to + ``SerialBase`` +- RS485 support changed (``rts_toggle`` removed, added ``serial.rs485`` module + and ``rs485_mode`` property) +- ``socket://`` and ``rfc2217://`` handlers use the IPv6 compatible + ``socket.create_connection`` +- New URL handler: ``spy:://``. +- URL handlers now require the proper format (``?`` and ``&``) for arguments + instead of ``/`` (e.g. ``rfc2217://localhost:7000?ign_set_control&timeout=5.5``) +- Remove obsolete examples. +- Finish update to BSD license. +- Use setuptools if available, fall back to distutils if unavailable. +- miniterm: changed command line options +- miniterm: support encodings on serial port +- miniterm: new transformations, by default escape/convert all control characters +- list_ports: improved, added USB location (Linux, Win32) +- refactored code +- [FTR pyserial:37] Support fileno() function in the socket protocol +- Posix: [Patch pyserial:31] Mark/space parity on Linux +- Linux: [Patch pyserial:32] Module list_ports for linux should include the + product information as description. +- Java: fix 2 bugs (stop bits if/else and non-integer timeouts) (Torsten + Roemer) +- Update wxSerialConfigDialog.py to use serial.tools.list_ports. +- [Patch pyserial:34] Improvements to port_publisher.py example +- [Feature pyserial:39] Support BlueTooth serial port discovery on Linux + +Bugfixes: + +- [Bug pyserial:157] Implement inWaiting in protocol_socket +- [Bug pyserial:166] RFC2217 connections always fail +- [Bug pyserial:172] applySettingsDict() throws an error if the settings dictionary is not complete +- [Bug pyserial:185] SocketSerial.read() never returns data when timeout==0 + +Bugfixes (posix): + +- [Bug pyserial:156] PosixSerial.open raises OSError rather than + SerialException when port open fails +- [Bug pyserial:163] serial.tools.list_ports.grep() fails if it encounters None type +- fix setXON +- [Patch pyserial:36 / 38] Make USB information work in python 3.4 and 2.7 +- clear OCRNL/ONLCR flags (CR/LF translation settings) +- [Feature pyserial:38] RS485 Support +- [Bug pyserial:170] list_ports_posix not working properly for Cygwin +- [Bug pyserial:187] improve support for FreeBSD (list_ports_posix) + +Bugfixes (win32): + +- [Bug pyserial:169] missing "import time" in serialwin32.py + +Bugfixes (cli): + +- [Bug pyserial:159] write() in serialcli.py not working with IronPython 2.7.4 + + +Version 3.0b1 2015-10-19 +-------------------------- +- list_ports: add ``vid``, ``pid``, ``serial_number``, ``product``, + ``manufacturer`` and ``location`` attribute for USB devices. +- list_ports: update OSX implementation. +- list_ports: Raspberry Pi: internal port is found. +- serial_for_url: fix import (multiple packages in list) +- threaded: added new module implementing a reader thread +- tweak examples/wx* +- posix: add experimental implementation ``VTIMESerial`` +- new URL handler ``alt://`` to select alternative implementations + + +Version 3.0 2015-12-28 +------------------------ +- minor fixes to setup.py (file list), inter_byte_timeout (not stored when + passed to __init__), rfc2217 (behavior of close when open failed), + list_ports (__str__), loop://, renamed ReaderThread +- hwgrep:// added options to pick n'th port, skip busy ports +- miniterm: --ask option added + +Bugfixes (posix): + +- [#26/#30] always call tcsettattr on open +- [#42] fix disregard read timeout if there is more data +- [#45] check for write timeout, even if EAGAIN was raised + +Bugfixes (win32): + +- [#27] fix race condition in ``read()``, fix minimal timeout issue +- race condition in nonblocking case +- [#49] change exception type in case SetCommState fails +- [#50] fixed issue with 0 timeout on windows 10 + + +Version 3.0.1 2016-01-11 +-------------------------- +- special case for FDTIBUS in list_ports on win32 (#61) + +Bugfixes: + +- ``Serial`` keyword arguments, more on backward compatibility, fix #55 +- list_ports: return name if product is None, fix for #54 +- port_publisher: restore some sorting of ports + + +Version 3.1.0 2016-05-27 +-------------------------- +Improvements: + +- improve error handling in ``alt://`` handler +- ``socket://`` internally used select, improves timeout behavior +- initial state of RTS/DTR: ignore error when setting on open posix + (support connecting to pty's) +- code style updates +- posix: remove "number_to_device" which is not called anymore +- add cancel_read and cancel_write to win32 and posix implementations + +Bugfixes: + +- [#68] aio: catch errors and close connection +- [#87] hexlify: update codec for Python 2 +- [#100] setPort not implemented +- [#101] bug in serial.threaded.Packetizer with easy fix +- [#104] rfc2217 and socket: set timeout in create_connection +- [#107] miniterm.py fails to exit on failed serial port + +Bugfixes (posix): + +- [#59] fixes for RTS/DTR handling on open +- [#77] list_ports_osx: add missing import +- [#85] serialposix.py _set_rs485_mode() tries to read non-existing + rs485_settings.delay_rts_before_send +- [#96] patch: native RS485 is never enabled + +Bugfixes (win32): + +- fix bad super call and duplicate old-style __init__ call +- [#80] list_ports: Compatibility issue between Windows/Linux + + +Version 3.1.1 2016-06-12 +-------------------------- +Improvements: + +- deprecate ``nonblocking()`` method on posix, the port is already in this + mode. +- style: use .format() in various places instead of "%" formatting + +Bugfixes: + +- [#122] fix bug in FramedPacket +- [#127] The Serial class in the .NET/Mono (IronPython) backend does not + implement the _reconfigure_port method +- [#123, #128] Avoid Python 3 syntax in aio module + +Bugfixes (posix): + +- [#126] PATCH: Check delay_before_tx/rx for None in serialposix.py +- posix: retry if interrupted in Serial.read + +Bugfixes (win32): + +- win32: handle errors of GetOverlappedResult in read(), fixes #121 + + +Version 3.2.0 2016-10-14 +-------------------------- +See 3.2.1, this one missed a merge request related to removing aio. + + +Version 3.2.1 2016-10-14 +-------------------------- +Improvements: + +- remove ``serial.aio`` in favor of separate package, ``pyserial-asyncio`` +- add client mode to example ``tcp_serial_redirect.py`` +- use of monotonic clock for timeouts, when available (Python 3.3 and up) +- [#169] arbitrary baud rate support for BSD family +- improve tests, improve ``loop://`` + +Bugfixes: + +- [#137] Exception while cancel in miniterm (python3) +- [#143] Class Serial in protocol_loop.py references variable before assigning + to it +- [#149] Python 3 fix for threaded.FramedPacket + +Bugfixes (posix): + +- [#133] _update_dtr_state throws Inappropriate ioctl for virtual serial + port created by socat on OS X +- [#157] Broken handling of CMSPAR in serialposix.py + +Bugfixes (win32): + +- [#144] Use Unicode API for list_ports +- [#145] list_ports_windows: support devices with only VID +- [#162] Write in non-blocking mode returns incorrect value on windows + + +Version 3.3 2017-03-08 +------------------------ +Improvements: + +- [#206] Exclusive access on POSIX. ``exclusive`` flag added. +- [#172] list_ports_windows: list_ports with 'manufacturer' info property +- [#174] miniterm: change cancel impl. for console +- [#182] serialutil: add overall timeout for read_until +- socket: use non-blocking socket and new Timeout class +- socket: implement a functional a reset_input_buffer +- rfc2217: improve read timeout implementation +- win32: include error message from system in ClearCommError exception +- and a few minor changes, docs + +Bugfixes: + +- [#183] rfc2217: Fix broken calls to to_bytes on Python3. +- [#188] rfc2217: fix auto-open use case when port is given as parameter + +Bugfixes (posix): + +- [#178] in read, count length of converted data +- [#189] fix return value of write + +Bugfixes (win32): + +- [#194] spurious write fails with ERROR_SUCCESS + + +Version 3.4 2017-07-22 +------------------------ +Improvements: + +- miniterm: suspend function (temporarily release port, :kbd:`Ctrl-T s`) +- [#240] context manager automatically opens port on ``__enter__`` +- [#141] list_ports: add interface number to location string +- [#225] protocol_socket: Retry if ``BlockingIOError`` occurs in + ``reset_input_buffer``. + +Bugfixes: + +- [#153] list_ports: option to include symlinked devices +- [#237] list_ports: workaround for special characters in port names + +Bugfixes (posix): + +- allow calling cancel functions w/o error if port is closed +- [#220] protocol_socket: sync error handling with posix version +- [#227] posix: ignore more blocking errors and EINTR, timeout only + applies to blocking I/O +- [#228] fix: port_publisher typo + + +Version 3.5b0 2020-09-21 +------------------------ +New Features: + +- [#411] Add a backend for Silicon Labs CP2110/4 HID-to-UART bridge. + (depends on `hid` module) + +Improvements: + +- [#315] Use absolute import everywhere +- [#351] win32: miniterm Working CMD.exe terminal using Windows 10 ANSI support +- [#354] Make ListPortInfo hashable +- [#372] threaded: "write" returns byte count +- [#400] Add bytesize and stopbits argument parser to tcp_serial_redirect +- [#408] loop: add out_waiting +- [#495] list_ports_linux: Correct "interface" property on Linux hosts +- [#500] Remove Python 3.2 and 3.3 from test +- [#261, #285, #296, #320, #333, #342, #356, #358, #389, #397, #510] doc updates +- miniterm: add :kbd:`CTRL+T Q` as alternative to exit +- miniterm: suspend function key changed to :kbd:`CTRL-T Z` +- add command line tool entries ``pyserial-miniterm`` (replaces ``miniterm.py``) + and ``pyserial-ports`` (runs ``serial.tools.list_ports``). +- ``python -m serial`` opens miniterm (use w/o args and it will print port + list too) [experimental] + +Bugfixes: + +- [#371] Don't open port if self.port is not set while entering context manager +- [#437, #502] refactor: raise new instances for PortNotOpenError and SerialTimeoutException +- [#261, #263] list_ports: set default `name` attribute +- [#286] fix: compare only of the same type in list_ports_common.ListPortInfo +- rfc2217/close(): fix race-condition +- [#305] return b'' when connection closes on rfc2217 connection +- [#386] rfc2217/close(): fix race condition +- Fixed flush_input_buffer() for situations where the remote end has closed the socket. +- [#441] reset_input_buffer() can hang on sockets +- examples: port_publisher python 3 fixes +- [#324] miniterm: Fix miniterm constructor exit_character and menu_character +- [#326] miniterm: use exclusive access for native serial ports by default +- [#497] miniterm: fix double use of CTRL-T + s use z for suspend instead +- [#443, #444] examples: refactor wx example, use Bind to avoid deprecated + warnings, IsChecked, unichr + +Bugfixes (posix): + +- [#265] posix: fix PosixPollSerial with timeout=None and add cancel support +- [#290] option for low latency mode on linux +- [#335] Add support to xr-usb-serial ports +- [#494] posix: Don't catch the SerialException we just raised +- [#519] posix: Fix custom baud rate to not temporarily set 38400 baud rates on linux +- [#509 #518] list_ports: use hardcoded path to library on osx + +Bugfixes (win32): + +- [#481] win32: extend RS485 error messages +- [#303] win32: do not check for links in serial.tools.list_ports +- [#430] Add WaitCommEvent function to win32 +- [#314, #433] tools/list_ports_windows: Scan both 'Ports' and 'Modem' device classes +- [#414] Serial number support for composite USB devices +- Added recursive search for device USB serial number to support composite devices + +Bugfixes (MacOS): + +- [#364] MacOS: rework list_ports to support unicode product descriptors. +- [#367] Mac and bsd fix _update_break_state + + +Version 3.5 2020-11-23 +---------------------- +See above (3.5b0) for what's all new in this release + +Bugfixes: + +- spy: ensure bytes in write() + +Bugfixes (posix): + +- [#540] serialposix: Fix inconsistent state after exception in open() + +Bugfixes (win32): + +- [#530] win32: Fix exception for composite serial number search on Windows + +Bugfixes (MacOS): + +- [#542] list_ports_osx: kIOMasterPortDefault no longer exported on Big Sur +- [#545, #545] list_ports_osx: getting USB info on BigSur/AppleSilicon diff --git a/pyserial-master/pyserial-master/LICENSE.txt b/pyserial-master/pyserial-master/LICENSE.txt new file mode 100644 index 0000000..8920d4e --- /dev/null +++ b/pyserial-master/pyserial-master/LICENSE.txt @@ -0,0 +1,39 @@ +Copyright (c) 2001-2020 Chris Liechti +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--------------------------------------------------------------------------- +Note: +Individual files contain the following tag instead of the full license text. + + SPDX-License-Identifier: BSD-3-Clause + +This enables machine processing of license information based on the SPDX +License Identifiers that are here available: http://spdx.org/licenses/ diff --git a/pyserial-master/pyserial-master/MANIFEST.in b/pyserial-master/pyserial-master/MANIFEST.in new file mode 100644 index 0000000..696a944 --- /dev/null +++ b/pyserial-master/pyserial-master/MANIFEST.in @@ -0,0 +1,39 @@ +include README.rst +include LICENSE.txt +include CHANGES.rst +include MANIFEST.in +include setup.py +include setup.cfg +include pylintrc + +include examples/at_protocol.py +include examples/port_publisher.py +include examples/port_publisher.sh +include examples/rfc2217_server.py +include examples/setup-miniterm-py2exe.py +include examples/setup-rfc2217_server-py2exe.py +include examples/setup-wxTerminal-py2exe.py +include examples/tcp_serial_redirect.py +include examples/wxSerialConfigDialog.py +include examples/wxSerialConfigDialog.wxg +include examples/wxTerminal.py +include examples/wxTerminal.wxg + +include test/handlers/__init__.py +include test/handlers/protocol_test.py +include test/run_all_tests.py +include test/test_advanced.py +include test/test_high_load.py +include test/test_iolib.py +include test/test.py +include test/test_readline.py +include test/test_rfc2217.py +include test/test_rs485.py +include test/test_settings_dict.py +include test/test_url.py + +include documentation/*.rst +include documentation/pyserial.png +include documentation/conf.py +include documentation/Makefile + diff --git a/pyserial-master/pyserial-master/README.rst b/pyserial-master/pyserial-master/README.rst new file mode 100644 index 0000000..2e793ca --- /dev/null +++ b/pyserial-master/pyserial-master/README.rst @@ -0,0 +1,56 @@ +================================= + pySerial |build-status| |docs| +================================= + +Overview +======== +This module encapsulates the access for the serial port. It provides backends +for Python_ running on Windows, OSX, Linux, BSD (possibly any POSIX compliant +system) and IronPython. The module named "serial" automatically selects the +appropriate backend. + +- Project Homepage: https://github.com/pyserial/pyserial +- Download Page: https://pypi.python.org/pypi/pyserial + +BSD license, (C) 2001-2020 Chris Liechti + + +Documentation +============= +For API documentation, usage and examples see files in the "documentation" +directory. The ".rst" files can be read in any text editor or being converted to +HTML or PDF using Sphinx_. An HTML version is online at +https://pythonhosted.org/pyserial/ + +Examples +======== +Examples and unit tests are in the directory examples_. + + +Installation +============ +``pip install pyserial`` should work for most users. + +Detailed information can be found in `documentation/pyserial.rst`_. + +The usual setup.py for Python_ libraries is used for the source distribution. +Windows installers are also available (see download link above). + +or + +To install this package with conda run: + +``conda install -c conda-forge pyserial`` + +conda builds are available for linux, mac and windows. + +.. _`documentation/pyserial.rst`: https://github.com/pyserial/pyserial/blob/master/documentation/pyserial.rst#installation +.. _examples: https://github.com/pyserial/pyserial/blob/master/examples +.. _Python: http://python.org/ +.. _Sphinx: http://sphinx-doc.org/ +.. |build-status| image:: https://travis-ci.org/pyserial/pyserial.svg?branch=master + :target: https://travis-ci.org/pyserial/pyserial + :alt: Build status +.. |docs| image:: https://readthedocs.org/projects/pyserial/badge/?version=latest + :target: http://pyserial.readthedocs.io/ + :alt: Documentation diff --git a/pyserial-master/pyserial-master/documentation/Makefile b/pyserial-master/pyserial-master/documentation/Makefile new file mode 100644 index 0000000..8384360 --- /dev/null +++ b/pyserial-master/pyserial-master/documentation/Makefile @@ -0,0 +1,88 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf _build/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html + @echo + @echo "Build finished. The HTML pages are in _build/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml + @echo + @echo "Build finished. The HTML pages are in _build/dirhtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in _build/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in _build/qthelp, like this:" + @echo "# qcollectiongenerator _build/qthelp/pySerial.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile _build/qthelp/pySerial.qhc" + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex + @echo + @echo "Build finished; the LaTeX files are in _build/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes + @echo + @echo "The overview file is in _build/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in _build/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in _build/doctest/output.txt." diff --git a/pyserial-master/pyserial-master/documentation/appendix.rst b/pyserial-master/pyserial-master/documentation/appendix.rst new file mode 100644 index 0000000..fe03954 --- /dev/null +++ b/pyserial-master/pyserial-master/documentation/appendix.rst @@ -0,0 +1,148 @@ +========== + Appendix +========== + +How To +====== + +Enable :rfc:`2217` (and other URL handlers) in programs using pySerial. + Patch the code where the :class:`serial.Serial` is instantiated. + E.g. replace:: + + s = serial.Serial(...) + + it with:: + + s = serial.serial_for_url(...) + + or for backwards compatibility to old pySerial installations:: + + try: + s = serial.serial_for_url(...) + except AttributeError: + s = serial.Serial(...) + + Assuming the application already stores port names as strings that's all + that is required. The user just needs a way to change the port setting of + your application to an ``rfc2217://`` :ref:`URL ` (e.g. by editing a + configuration file, GUI dialog etc.). + + Please note that this enables all :ref:`URL ` types supported by + pySerial and that those involving the network are unencrypted and not + protected against eavesdropping. + +Test your setup. + Is the device not working as expected? Maybe it's time to check the + connection before proceeding. :ref:`miniterm` from the :ref:`examples` + can be used to open the serial port and do some basic tests. + + To test cables, connecting RX to TX (loop back) and typing some characters + in :ref:`miniterm` is a simple test. When the characters are displayed + on the screen, then at least RX and TX work (they still could be swapped + though). + + There is also a ``spy:://`` URL handler. It prints all calls (read/write, + control lines) to the serial port to a file or stderr. See :ref:`spy` + for details. + + +FAQ +=== +Example works in :ref:`miniterm` but not in script. + The RTS and DTR lines are switched when the port is opened. This may cause + some processing or reset on the connected device. In such a cases an + immediately following call to :meth:`write` may not be received by the + device. + + A delay after opening the port, before the first :meth:`write`, is + recommended in this situation. E.g. a ``time.sleep(1)`` + + +Application works when .py file is run, but fails when packaged (py2exe etc.) + py2exe and similar packaging programs scan the sources for import + statements and create a list of modules that they package. pySerial may + create two issues with that: + + - implementations for other modules are found. On Windows, it's safe to + exclude 'serialposix', 'serialjava' and 'serialcli' as these are not + used. + + - :func:`serial.serial_for_url` does a dynamic lookup of protocol handlers + at runtime. If this function is used, the desired handlers have to be + included manually (e.g. 'serial.urlhandler.protocol_socket', + 'serial.urlhandler.protocol_rfc2217', etc.). This can be done either with + the "includes" option in ``setup.py`` or by a dummy import in one of the + packaged modules. + +User supplied URL handlers + :func:`serial.serial_for_url` can be used to access "virtual" serial ports + identified by an :ref:`URL ` scheme. E.g. for the :rfc:`2217`: + ``rfc2217://``. + + Custom :ref:`URL ` handlers can be added by extending the module + search path in :data:`serial.protocol_handler_packages`. This is possible + starting from pySerial V2.6. + +``Permission denied`` errors + On POSIX based systems, the user usually needs to be in a special group to + have access to serial ports. + + On Debian based systems, serial ports are usually in the group ``dialout``, + so running ``sudo adduser $USER dialout`` (and logging-out and -in) enables + the user to access the port. + +Parity on Raspberry Pi + The Raspi has one full UART and a restricted one. On devices with built + in wireless (WIFI/BT) use the restricted one on the GPIO header pins. + If enhanced features are required, it is possible to swap UARTs, see + https://www.raspberrypi.org/documentation/configuration/uart.md + +Support for Python 2.6 or earlier + Support for older Python releases than 2.7 will not return to pySerial 3.x. + Python 2.7 is now many years old (released 2010). If you insist on using + Python 2.6 or earlier, it is recommend to use pySerial `2.7`_ + (or any 2.x version). + +.. _`2.7`: https://pypi.python.org/pypi/pyserial/2.7 + + +Related software +================ + +com0com - http://com0com.sourceforge.net/ + Provides virtual serial ports for Windows. + + +License +======= +Copyright (c) 2001-2020 Chris Liechti +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/pyserial-master/pyserial-master/documentation/conf.py b/pyserial-master/pyserial-master/documentation/conf.py new file mode 100644 index 0000000..d878ea4 --- /dev/null +++ b/pyserial-master/pyserial-master/documentation/conf.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# +# pySerial documentation build configuration file, created by +# sphinx-quickstart on Tue Jul 21 00:27:45 2009. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.append(os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.intersphinx'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'pySerial' +copyright = u'2001-2020, Chris Liechti' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '3.4' +# The full version, including alpha/beta/rc tags. +release = '3.4' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +#html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +html_logo = 'pyserial.png' + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +#htmlhelp_basename = 'pySerialdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'pySerial.tex', u'pySerial Documentation', + u'Chris Liechti', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +latex_logo = 'pyserial.png' + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True + +# for external links to standard library +intersphinx_mapping = { + #~ 'python': ('http://docs.python.org', None), + 'py': ('http://docs.python.org', None), + } diff --git a/pyserial-master/pyserial-master/documentation/examples.rst b/pyserial-master/pyserial-master/documentation/examples.rst new file mode 100644 index 0000000..197c015 --- /dev/null +++ b/pyserial-master/pyserial-master/documentation/examples.rst @@ -0,0 +1,274 @@ +.. _examples: + +========== + Examples +========== + + +Miniterm +======== +Miniterm is now available as module instead of example. +see :ref:`miniterm` for details. + +miniterm.py_ + The miniterm program. + +setup-miniterm-py2exe.py_ + This is a py2exe setup script for Windows. It can be used to create a + standalone ``miniterm.exe``. + +.. _miniterm.py: https://github.com/pyserial/pyserial/blob/master/serial/tools/miniterm.py +.. _setup-miniterm-py2exe.py: https://github.com/pyserial/pyserial/blob/master/examples/setup-miniterm-py2exe.py + + +TCP/IP - serial bridge +====================== +This program opens a TCP/IP port. When a connection is made to that port (e.g. +with telnet) it forwards all data to the serial port and vice versa. + +This example only exports a raw socket connection. The next example +below gives the client much more control over the remote serial port. + +- The serial port settings are set on the command line when starting the + program. +- There is no possibility to change settings from remote. +- All data is passed through as-is. + +:: + + usage: tcp_serial_redirect.py [-h] [-q] [--parity {N,E,O,S,M}] [--rtscts] + [--xonxoff] [--rts RTS] [--dtr DTR] + [-P LOCALPORT] + SERIALPORT [BAUDRATE] + + Simple Serial to Network (TCP/IP) redirector. + + positional arguments: + SERIALPORT serial port name + BAUDRATE set baud rate, default: 9600 + + optional arguments: + -h, --help show this help message and exit + -q, --quiet suppress non error messages + + serial port: + --parity {N,E,O,S,M} set parity, one of {N E O S M}, default: N + --rtscts enable RTS/CTS flow control (default off) + --xonxoff enable software flow control (default off) + --rts RTS set initial RTS line state (possible values: 0, 1) + --dtr DTR set initial DTR line state (possible values: 0, 1) + + network settings: + -P LOCALPORT, --localport LOCALPORT + local TCP port + + NOTE: no security measures are implemented. Anyone can remotely connect to + this service over the network. Only one connection at once is supported. When + the connection is terminated it waits for the next connect. + + +tcp_serial_redirect.py_ + Main program. + +.. _tcp_serial_redirect.py: https://github.com/pyserial/pyserial/blob/master/examples/tcp_serial_redirect.py + + +Single-port TCP/IP - serial bridge (RFC 2217) +============================================= +Simple cross platform :rfc:`2217` serial port server. It uses threads and is +portable (runs on POSIX, Windows, etc). + +- The port settings and control lines (RTS/DTR) can be changed at any time + using :rfc:`2217` requests. The status lines (DSR/CTS/RI/CD) are polled every + second and notifications are sent to the client. +- Telnet character IAC (0xff) needs to be doubled in data stream. IAC followed + by another value is interpreted as Telnet command sequence. +- Telnet negotiation commands are sent when connecting to the server. +- RTS/DTR are activated on client connect and deactivated on disconnect. +- Default port settings are set again when client disconnects. + +:: + + usage: rfc2217_server.py [-h] [-p TCPPORT] [-v] SERIALPORT + + RFC 2217 Serial to Network (TCP/IP) redirector. + + positional arguments: + SERIALPORT + + optional arguments: + -h, --help show this help message and exit + -p TCPPORT, --localport TCPPORT + local TCP port, default: 2217 + -v, --verbose print more diagnostic messages (option can be given + multiple times) + + NOTE: no security measures are implemented. Anyone can remotely connect to + this service over the network. Only one connection at once is supported. When + the connection is terminated it waits for the next connect. + +.. versionadded:: 2.5 + +rfc2217_server.py_ + Main program. + +setup-rfc2217_server-py2exe.py_ + This is a py2exe setup script for Windows. It can be used to create a + standalone ``rfc2217_server.exe``. + +.. _rfc2217_server.py: https://github.com/pyserial/pyserial/blob/master/examples/rfc2217_server.py +.. _setup-rfc2217_server-py2exe.py: https://github.com/pyserial/pyserial/blob/master/examples/setup-rfc2217_server-py2exe.py + + +Multi-port TCP/IP - serial bridge (RFC 2217) +============================================ +This example implements a TCP/IP to serial port service that works with +multiple ports at once. It uses select, no threads, for the serial ports and +the network sockets and therefore runs on POSIX systems only. + +- Full control over the serial port with :rfc:`2217`. +- Check existence of ``/tty/USB0...8``. This is done every 5 seconds using + ``os.path.exists``. +- Send zeroconf announcements when port appears or disappears (uses + python-avahi and dbus). Service name: ``_serial_port._tcp``. +- Each serial port becomes available as one TCP/IP server. e.g. + ``/dev/ttyUSB0`` is reachable at ``:7000``. +- Single process for all ports and sockets (not per port). +- The script can be started as daemon. +- Logging to stdout or when run as daemon to syslog. +- Default port settings are set again when client disconnects. +- modem status lines (CTS/DSR/RI/CD) are not polled periodically and the server + therefore does not send NOTIFY_MODEMSTATE on its own. However it responds to + request from the client (i.e. use the ``poll_modem`` option in the URL when + using a pySerial client.) + +:: + + usage: port_publisher.py [options] + + Announce the existence of devices using zeroconf and provide + a TCP/IP <-> serial port gateway (implements RFC 2217). + + If running as daemon, write to syslog. Otherwise write to stdout. + + optional arguments: + -h, --help show this help message and exit + + serial port settings: + --ports-regex REGEX specify a regex to search against the serial devices + and their descriptions (default: /dev/ttyUSB[0-9]+) + + network settings: + --tcp-port PORT specify lowest TCP port number (default: 7000) + + daemon: + -d, --daemon start as daemon + --pidfile FILE specify a name for the PID file + + diagnostics: + -o FILE, --logfile FILE + write messages file instead of stdout + -q, --quiet suppress most diagnostic messages + -v, --verbose increase diagnostic messages + + NOTE: no security measures are implemented. Anyone can remotely connect to + this service over the network. Only one connection at once, per port, is + supported. When the connection is terminated, it waits for the next connect. + +Requirements: + +- Python (>= 2.4) +- python-avahi +- python-dbus +- python-serial (>= 2.5) + +Installation as daemon: + +- Copy the script ``port_publisher.py`` to ``/usr/local/bin``. +- Copy the script ``port_publisher.sh`` to ``/etc/init.d``. +- Add links to the runlevels using ``update-rc.d port_publisher.sh defaults 99`` +- That's it :-) the service will be started on next reboot. Alternatively run + ``invoke-rc.d port_publisher.sh start`` as root. + +.. versionadded:: 2.5 new example + +port_publisher.py_ + Multi-port TCP/IP-serial converter (RFC 2217) for POSIX environments. + +port_publisher.sh_ + Example init.d script. + +.. _port_publisher.py: https://github.com/pyserial/pyserial/blob/master/examples/port_publisher.py +.. _port_publisher.sh: https://github.com/pyserial/pyserial/blob/master/examples/port_publisher.sh + + +wxPython examples +================= +A simple terminal application for wxPython and a flexible serial port +configuration dialog are shown here. + +wxTerminal.py_ + A simple terminal application. Note that the length of the buffer is + limited by wx and it may suddenly stop displaying new input. + +wxTerminal.wxg_ + A wxGlade design file for the terminal application. + +wxSerialConfigDialog.py_ + A flexible serial port configuration dialog. + +wxSerialConfigDialog.wxg_ + The wxGlade design file for the configuration dialog. + +setup-wxTerminal-py2exe.py_ + A py2exe setup script to package the terminal application. + +.. _wxTerminal.py: https://github.com/pyserial/pyserial/blob/master/examples/wxTerminal.py +.. _wxTerminal.wxg: https://github.com/pyserial/pyserial/blob/master/examples/wxTerminal.wxg +.. _wxSerialConfigDialog.py: https://github.com/pyserial/pyserial/blob/master/examples/wxSerialConfigDialog.py +.. _wxSerialConfigDialog.wxg: https://github.com/pyserial/pyserial/blob/master/examples/wxSerialConfigDialog.wxg +.. _setup-wxTerminal-py2exe.py: https://github.com/pyserial/pyserial/blob/master/examples/setup-wxTerminal-py2exe.py + + + +Unit tests +========== +The project uses a number of unit test to verify the functionality. They all +need a loop back connector. The scripts itself contain more information. All +test scripts are contained in the directory ``test``. + +The unit tests are performed on port ``loop://`` unless a different device +name or URL is given on the command line (``sys.argv[1]``). e.g. to run the +test on an attached USB-serial converter ``hwgrep://USB`` could be used or +the actual name such as ``/dev/ttyUSB0`` or ``COM1`` (depending on platform). + +run_all_tests.py_ + Collect all tests from all ``test*`` files and run them. By default, the + ``loop://`` device is used. + +test.py_ + Basic tests (binary capabilities, timeout, control lines). + +test_advanced.py_ + Test more advanced features (properties). + +test_high_load.py_ + Tests involving sending a lot of data. + +test_readline.py_ + Tests involving ``readline``. + +test_iolib.py_ + Tests involving the :mod:`io` library. Only available for Python 2.6 and + newer. + +test_url.py_ + Tests involving the :ref:`URL ` feature. + +.. _run_all_tests.py: https://github.com/pyserial/pyserial/blob/master/test/run_all_tests.py +.. _test.py: https://github.com/pyserial/pyserial/blob/master/test/test.py +.. _test_advanced.py: https://github.com/pyserial/pyserial/blob/master/test/test_advanced.py +.. _test_high_load.py: https://github.com/pyserial/pyserial/blob/master/test/test_high_load.py +.. _test_readline.py: https://github.com/pyserial/pyserial/blob/master/test/test_readline.py +.. _test_iolib.py: https://github.com/pyserial/pyserial/blob/master/test/test_iolib.py +.. _test_url.py: https://github.com/pyserial/pyserial/blob/master/test/test_url.py diff --git a/pyserial-master/pyserial-master/documentation/index.rst b/pyserial-master/pyserial-master/documentation/index.rst new file mode 100644 index 0000000..c3ca19d --- /dev/null +++ b/pyserial-master/pyserial-master/documentation/index.rst @@ -0,0 +1,44 @@ +.. pySerial documentation master file +.. _welcome: + +Welcome to pySerial's documentation +=================================== + + +This module encapsulates the access for the serial port. It provides backends +for Python_ running on Windows, OSX, Linux, BSD (possibly any POSIX compliant +system) and IronPython. The module named "serial" automatically selects the +appropriate backend. + +Other pages (online) + +- `project page on GitHub`_ +- `Download Page`_ with releases +- This page, when viewed online is at https://pyserial.readthedocs.io/en/latest/ or + http://pythonhosted.org/pyserial/ . + +.. _Python: http://python.org/ +.. _`project page on GitHub`: https://github.com/pyserial/ +.. _`Download Page`: http://pypi.python.org/pypi/pyserial + + +Contents: + +.. toctree:: + :maxdepth: 2 + + pyserial + shortintro + pyserial_api + tools + url_handlers + examples + appendix + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/pyserial-master/pyserial-master/documentation/pyserial.png b/pyserial-master/pyserial-master/documentation/pyserial.png new file mode 100644 index 0000000000000000000000000000000000000000..7fd45f46eb11b8ec36fb51bb5643832d959a4820 GIT binary patch literal 7050 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 + +Other pages (online) + +- `project page on GitHub`_ +- `Download Page`_ with releases (PyPi) +- This page, when viewed online is at https://pyserial.readthedocs.io/en/latest/ or + http://pythonhosted.org/pyserial/ . + +.. _Python: http://python.org/ +.. _LICENSE: appendix.html#license +.. _`project page on GitHub`: https://github.com/pyserial/pyserial/ +.. _`Download Page`: http://pypi.python.org/pypi/pyserial + + +Features +======== +- Same class based interface on all supported platforms. +- Access to the port settings through Python properties. +- Support for different byte sizes, stop bits, parity and flow control with + RTS/CTS and/or Xon/Xoff. +- Working with or without receive timeout. +- File like API with "read" and "write" ("readline" etc. also supported). +- The files in this package are 100% pure Python. +- The port is set up for binary transmission. No NULL byte stripping, CR-LF + translation etc. (which are many times enabled for POSIX.) This makes this + module universally useful. +- Compatible with :mod:`io` library +- RFC 2217 client (experimental), server provided in the examples. + + +Requirements +============ +- Python 2.7 or Python 3.4 and newer + +- If running on Windows: Windows 7 or newer + +- If running on Jython: "Java Communications" (JavaComm) or compatible + extension for Java + +For older installations (older Python versions or older operating systems), see +`older versions`_ below. + + +Installation +============ + +This installs a package that can be used from Python (``import serial``). + +To install for all users on the system, administrator rights (root) +may be required. + +From PyPI +--------- +pySerial can be installed from PyPI:: + + python -m pip install pyserial + +Using the `python`/`python3` executable of the desired version (2.7/3.x). + +Developers also may be interested to get the source archive, because it +contains examples, tests and the this documentation. + +From Conda +---------- +pySerial can be installed from Conda:: + + conda install pyserial + + or + + conda install -c conda-forge pyserial + +Currently the default conda channel will provide version 3.4 whereas the +conda-forge channel provides the current 3.x version. + +Conda: https://www.continuum.io/downloads + +From source (zip/tar.gz or checkout) +------------------------------------ +Download the archive from http://pypi.python.org/pypi/pyserial or +https://github.com/pyserial/pyserial/releases. +Unpack the archive, enter the ``pyserial-x.y`` directory and run:: + + python setup.py install + +Using the `python`/`python3` executable of the desired version (2.7/3.x). + +Packages +-------- +There are also packaged versions for some Linux distributions: + +- Debian/Ubuntu: "python-serial", "python3-serial" +- Fedora / RHEL / CentOS / EPEL: "pyserial" +- Arch Linux: "python-pyserial" +- Gentoo: "dev-python/pyserial" + +Note that some distributions may package an older version of pySerial. +These packages are created and maintained by developers working on +these distributions. + +.. _PyPi: http://pypi.python.org/pypi/pyserial + + +References +========== +* Python: http://www.python.org/ +* Jython: http://www.jython.org/ +* IronPython: http://www.codeplex.com/IronPython + + +Older Versions +============== +Older versions are still available on the current download_ page or the `old +download`_ page. The last version of pySerial's 2.x series was `2.7`_, +compatible with Python 2.3 and newer and partially with early Python 3.x +versions. + +pySerial `1.21`_ is compatible with Python 2.0 on Windows, Linux and several +un*x like systems, MacOSX and Jython. + +On Windows, releases older than 2.5 will depend on pywin32_ (previously known as +win32all). WinXP is supported up to 3.0.1. + + +.. _`old download`: https://sourceforge.net/projects/pyserial/files/pyserial/ +.. _download: https://pypi.python.org/simple/pyserial/ +.. _pywin32: http://pypi.python.org/pypi/pywin32 +.. _`2.7`: https://pypi.python.org/pypi/pyserial/2.7 +.. _`1.21`: https://sourceforge.net/projects/pyserial/files/pyserial/1.21/pyserial-1.21.zip/download diff --git a/pyserial-master/pyserial-master/documentation/pyserial_api.rst b/pyserial-master/pyserial-master/documentation/pyserial_api.rst new file mode 100644 index 0000000..e1ce049 --- /dev/null +++ b/pyserial-master/pyserial-master/documentation/pyserial_api.rst @@ -0,0 +1,1309 @@ +============== + pySerial API +============== + +.. module:: serial + +Classes +======= + +Native ports +------------ + +.. class:: Serial + + .. method:: __init__(port=None, baudrate=9600, bytesize=EIGHTBITS, parity=PARITY_NONE, stopbits=STOPBITS_ONE, timeout=None, xonxoff=False, rtscts=False, write_timeout=None, dsrdtr=False, inter_byte_timeout=None, exclusive=None) + + :param port: + Device name or :const:`None`. + + :param int baudrate: + Baud rate such as 9600 or 115200 etc. + + :param bytesize: + Number of data bits. Possible values: + :const:`FIVEBITS`, :const:`SIXBITS`, :const:`SEVENBITS`, + :const:`EIGHTBITS` + + :param parity: + Enable parity checking. Possible values: + :const:`PARITY_NONE`, :const:`PARITY_EVEN`, :const:`PARITY_ODD` + :const:`PARITY_MARK`, :const:`PARITY_SPACE` + + :param stopbits: + Number of stop bits. Possible values: + :const:`STOPBITS_ONE`, :const:`STOPBITS_ONE_POINT_FIVE`, + :const:`STOPBITS_TWO` + + :param float timeout: + Set a read timeout value in seconds. + + :param bool xonxoff: + Enable software flow control. + + :param bool rtscts: + Enable hardware (RTS/CTS) flow control. + + :param bool dsrdtr: + Enable hardware (DSR/DTR) flow control. + + :param float write_timeout: + Set a write timeout value in seconds. + + :param float inter_byte_timeout: + Inter-character timeout, :const:`None` to disable (default). + + :param bool exclusive: + Set exclusive access mode (POSIX only). A port cannot be opened in + exclusive access mode if it is already open in exclusive access mode. + + :exception ValueError: + Will be raised when parameter are out of range, e.g. baud rate, data bits. + + :exception SerialException: + In case the device can not be found or can not be configured. + + + The port is immediately opened on object creation, when a *port* is + given. It is not opened when *port* is :const:`None` and a successive call + to :meth:`open` is required. + + *port* is a device name: depending on operating system. e.g. + ``/dev/ttyUSB0`` on GNU/Linux or ``COM3`` on Windows. + + The parameter *baudrate* can be one of the standard values: + 50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, + 9600, 19200, 38400, 57600, 115200. + These are well supported on all platforms. + + Standard values above 115200, such as: 230400, 460800, 500000, 576000, + 921600, 1000000, 1152000, 1500000, 2000000, 2500000, 3000000, 3500000, + 4000000 also work on many platforms and devices. + + Non-standard values are also supported on some platforms (GNU/Linux, MAC + OSX >= Tiger, Windows). Though, even on these platforms some serial + ports may reject non-standard values. + + Possible values for the parameter *timeout* which controls the behavior + of :meth:`read`: + + - ``timeout = None``: wait forever / until requested number of bytes + are received + - ``timeout = 0``: non-blocking mode, return immediately in any case, + returning zero or more, up to the requested number of bytes + - ``timeout = x``: set timeout to ``x`` seconds (float allowed) + returns immediately when the requested number of bytes are available, + otherwise wait until the timeout expires and return all bytes that + were received until then. + + :meth:`write` is blocking by default, unless *write_timeout* is set. + For possible values refer to the list for *timeout* above. + + Note that enabling both flow control methods (*xonxoff* and *rtscts*) + together may not be supported. It is common to use one of the methods + at once, not both. + + *dsrdtr* is not supported by all platforms (silently ignored). Setting + it to ``None`` has the effect that its state follows *rtscts*. + + Also consider using the function :func:`serial_for_url` instead of + creating Serial instances directly. + + .. versionchanged:: 2.5 + *dsrdtr* now defaults to ``False`` (instead of *None*) + .. versionchanged:: 3.0 numbers as *port* argument are no longer supported + .. versionadded:: 3.3 ``exclusive`` flag + + .. method:: open() + + Open port. The state of :attr:`rts` and :attr:`dtr` is applied. + + .. note:: + + Some OS and/or drivers may activate RTS and or DTR automatically, + as soon as the port is opened. There may be a glitch on RTS/DTR + when :attr:`rts` or :attr:`dtr` are set differently from their + default value (``True`` / active). + + .. note:: + + For compatibility reasons, no error is reported when applying + :attr:`rts` or :attr:`dtr` fails on POSIX due to EINVAL (22) or + ENOTTY (25). + + .. method:: close() + + Close port immediately. + + .. method:: __del__() + + Destructor, close port when serial port instance is freed. + + + The following methods may raise :exc:`SerialException` when applied to a closed + port. + + .. method:: read(size=1) + + :param size: Number of bytes to read. + :return: Bytes read from the port. + :rtype: bytes + + Read *size* bytes from the serial port. If a timeout is set it may + return fewer characters than requested. With no timeout it will block + until the requested number of bytes is read. + + .. versionchanged:: 2.5 + Returns an instance of :class:`bytes` when available (Python 2.6 + and newer) and :class:`str` otherwise. + + .. method:: read_until(expected=LF, size=None) + + :param expected: The byte string to search for. + :param size: Number of bytes to read. + :return: Bytes read from the port. + :rtype: bytes + + Read until an expected sequence is found ('\\n' by default), the size + is exceeded or until timeout occurs. If a timeout is set it may + return fewer characters than requested. With no timeout it will block + until the requested number of bytes is read. + + .. versionchanged:: 2.5 + Returns an instance of :class:`bytes` when available (Python 2.6 + and newer) and :class:`str` otherwise. + + .. versionchanged:: 3.5 + First argument was called ``terminator`` in previous versions. + + .. method:: write(data) + + :param data: Data to send. + :return: Number of bytes written. + :rtype: int + :exception SerialTimeoutException: + In case a write timeout is configured for the port and the time is + exceeded. + + Write the bytes *data* to the port. This should be of type ``bytes`` + (or compatible such as ``bytearray`` or ``memoryview``). Unicode + strings must be encoded (e.g. ``'hello'.encode('utf-8')``. + + .. versionchanged:: 2.5 + Accepts instances of :class:`bytes` and :class:`bytearray` when + available (Python 2.6 and newer) and :class:`str` otherwise. + + .. versionchanged:: 2.5 + Write returned ``None`` in previous versions. + + .. method:: flush() + + Flush of file like objects. In this case, wait until all data is + written. + + .. attribute:: in_waiting + + :getter: Get the number of bytes in the input buffer + :type: int + + Return the number of bytes in the receive buffer. + + .. versionchanged:: 3.0 changed to property from ``inWaiting()`` + + .. attribute:: out_waiting + + :getter: Get the number of bytes in the output buffer + :type: int + :platform: Posix + :platform: Windows + + Return the number of bytes in the output buffer. + + .. versionchanged:: 2.7 (Posix support added) + .. versionchanged:: 3.0 changed to property from ``outWaiting()`` + + .. method:: reset_input_buffer() + + Flush input buffer, discarding all its contents. + + .. versionchanged:: 3.0 renamed from ``flushInput()`` + + .. method:: reset_output_buffer() + + Clear output buffer, aborting the current output and + discarding all that is in the buffer. + + Note, for some USB serial adapters, this may only flush the buffer of + the OS and not all the data that may be present in the USB part. + + .. versionchanged:: 3.0 renamed from ``flushOutput()`` + + .. method:: send_break(duration=0.25) + + :param float duration: Time in seconds, to activate the BREAK condition. + + Send break condition. Timed, returns to idle state after given + duration. + + + .. attribute:: break_condition + + :getter: Get the current BREAK state + :setter: Control the BREAK state + :type: bool + + When set to ``True`` activate BREAK condition, else disable. + Controls TXD. When active, no transmitting is possible. + + .. attribute:: rts + + :setter: Set the state of the RTS line + :getter: Return the state of the RTS line + :type: bool + + Set RTS line to specified logic level. It is possible to assign this + value before opening the serial port, then the value is applied upon + :meth:`open` (with restrictions, see :meth:`open`). + + .. attribute:: dtr + + :setter: Set the state of the DTR line + :getter: Return the state of the DTR line + :type: bool + + Set DTR line to specified logic level. It is possible to assign this + value before opening the serial port, then the value is applied upon + :meth:`open` (with restrictions, see :meth:`open`). + + Read-only attributes: + + .. attribute:: name + + :getter: Device name. + :type: str + + .. versionadded:: 2.5 + + .. attribute:: cts + + :getter: Get the state of the CTS line + :type: bool + + Return the state of the CTS line. + + .. attribute:: dsr + + :getter: Get the state of the DSR line + :type: bool + + Return the state of the DSR line. + + .. attribute:: ri + + :getter: Get the state of the RI line + :type: bool + + Return the state of the RI line. + + .. attribute:: cd + + :getter: Get the state of the CD line + :type: bool + + Return the state of the CD line + + .. attribute:: is_open + + :getter: Get the state of the serial port, whether it's open. + :type: bool + + New values can be assigned to the following attributes (properties), the + port will be reconfigured, even if it's opened at that time: + + + .. attribute:: port + + :type: str + + Read or write port. When the port is already open, it will be closed + and reopened with the new setting. + + .. attribute:: baudrate + + :getter: Get current baud rate + :setter: Set new baud rate + :type: int + + Read or write current baud rate setting. + + .. attribute:: bytesize + + :getter: Get current byte size + :setter: Set new byte size. Possible values: + :const:`FIVEBITS`, :const:`SIXBITS`, :const:`SEVENBITS`, + :const:`EIGHTBITS` + :type: int + + Read or write current data byte size setting. + + .. attribute:: parity + + :getter: Get current parity setting + :setter: Set new parity mode. Possible values: + :const:`PARITY_NONE`, :const:`PARITY_EVEN`, :const:`PARITY_ODD` + :const:`PARITY_MARK`, :const:`PARITY_SPACE` + + Read or write current parity setting. + + .. attribute:: stopbits + + :getter: Get current stop bit setting + :setter: Set new stop bit setting. Possible values: + :const:`STOPBITS_ONE`, :const:`STOPBITS_ONE_POINT_FIVE`, + :const:`STOPBITS_TWO` + + Read or write current stop bit width setting. + + .. attribute:: timeout + + :getter: Get current read timeout setting + :setter: Set read timeout + :type: float (seconds) + + Read or write current read timeout setting. + + .. attribute:: write_timeout + + :getter: Get current write timeout setting + :setter: Set write timeout + :type: float (seconds) + + Read or write current write timeout setting. + + .. versionchanged:: 3.0 renamed from ``writeTimeout`` + + .. attribute:: inter_byte_timeout + + :getter: Get current inter byte timeout setting + :setter: Disable (``None``) or enable the inter byte timeout + :type: float or None + + Read or write current inter byte timeout setting. + + .. versionchanged:: 3.0 renamed from ``interCharTimeout`` + + .. attribute:: xonxoff + + :getter: Get current software flow control setting + :setter: Enable or disable software flow control + :type: bool + + Read or write current software flow control rate setting. + + .. attribute:: rtscts + + :getter: Get current hardware flow control setting + :setter: Enable or disable hardware flow control + :type: bool + + Read or write current hardware flow control setting. + + .. attribute:: dsrdtr + + :getter: Get current hardware flow control setting + :setter: Enable or disable hardware flow control + :type: bool + + Read or write current hardware flow control setting. + + .. attribute:: rs485_mode + + :getter: Get the current RS485 settings + :setter: Disable (``None``) or enable the RS485 settings + :type: :class:`rs485.RS485Settings` or ``None`` + :platform: Posix (Linux, limited set of hardware) + :platform: Windows (only RTS on TX possible) + + Attribute to configure RS485 support. When set to an instance of + :class:`rs485.RS485Settings` and supported by OS, RTS will be active + when data is sent and inactive otherwise (for reception). The + :class:`rs485.RS485Settings` class provides additional settings + supported on some platforms. + + .. versionadded:: 3.0 + + + The following constants are also provided: + + .. attribute:: BAUDRATES + + A list of valid baud rates. The list may be incomplete, such that higher + and/or intermediate baud rates may also be supported by the device + (Read Only). + + .. attribute:: BYTESIZES + + A list of valid byte sizes for the device (Read Only). + + .. attribute:: PARITIES + + A list of valid parities for the device (Read Only). + + .. attribute:: STOPBITS + + A list of valid stop bit widths for the device (Read Only). + + + The following methods are for compatibility with the :mod:`io` library. + + .. method:: readable() + + :return: True + + .. versionadded:: 2.5 + + .. method:: writable() + + :return: True + + .. versionadded:: 2.5 + + .. method:: seekable() + + :return: False + + .. versionadded:: 2.5 + + .. method:: readinto(b) + + :param b: bytearray or array instance + :return: Number of byte read + + Read up to len(b) bytes into :class:`bytearray` *b* and return the + number of bytes read. + + .. versionadded:: 2.5 + + .. method:: readline(size=-1) + + Provided via :meth:`io.IOBase.readline` See also :ref:`shortintro_readline`. + + .. method:: readlines(hint=-1) + + Provided via :meth:`io.IOBase.readlines`. See also :ref:`shortintro_readline`. + + .. method:: writelines(lines) + + Provided via :meth:`io.IOBase.writelines` + + The port settings can be read and written as dictionary. The following + keys are supported: ``write_timeout``, ``inter_byte_timeout``, + ``dsrdtr``, ``baudrate``, ``timeout``, ``parity``, ``bytesize``, + ``rtscts``, ``stopbits``, ``xonxoff`` + + .. method:: get_settings() + + :return: a dictionary with current port settings. + :rtype: dict + + Get a dictionary with port settings. This is useful to backup the + current settings so that a later point in time they can be restored + using :meth:`apply_settings`. + + Note that the state of control lines (RTS/DTR) are not part of the + settings. + + .. versionadded:: 2.5 + .. versionchanged:: 3.0 renamed from ``getSettingsDict`` + + .. method:: apply_settings(d) + + :param dict d: a dictionary with port settings. + + Applies a dictionary that was created by :meth:`get_settings`. Only + changes are applied and when a key is missing, it means that the + setting stays unchanged. + + Note that control lines (RTS/DTR) are not changed. + + .. versionadded:: 2.5 + .. versionchanged:: 3.0 renamed from ``applySettingsDict`` + + + .. _context-manager: + + This class can be used as context manager. The serial port is closed when + the context is left. + + .. method:: __enter__() + + :returns: Serial instance + + Returns the instance that was used in the ``with`` statement. + + Example: + + >>> with serial.serial_for_url(port) as s: + ... s.write(b'hello') + + The port is opened automatically: + + >>> port = serial.Serial() + >>> port.port = '...' + >>> with port as s: + ... s.write(b'hello') + + Which also means that ``with`` statements can be used repeatedly, + each time opening and closing the port. + + .. versionchanged:: 3.4 the port is automatically opened + + + .. method:: __exit__(exc_type, exc_val, exc_tb) + + Closes serial port (exceptions are not handled by ``__exit__``). + + + Platform specific methods. + + .. warning:: Programs using the following methods and attributes are not + portable to other platforms! + + .. method:: nonblocking() + + :platform: Posix + + .. deprecated:: 3.2 + The serial port is already opened in this mode. This method is not + needed and going away. + + + .. method:: fileno() + + :platform: Posix + :return: File descriptor. + + Return file descriptor number for the port that is opened by this object. + It is useful when serial ports are used with :mod:`select`. + + .. method:: set_input_flow_control(enable) + + :platform: Posix + :param bool enable: Set flow control state. + + Manually control flow - when software flow control is enabled. + + This will send XON (true) and XOFF (false) to the other device. + + .. versionadded:: 2.7 (Posix support added) + .. versionchanged:: 3.0 renamed from ``flowControlOut`` + + .. method:: set_output_flow_control(enable) + + :platform: Posix (HW and SW flow control) + :platform: Windows (SW flow control only) + :param bool enable: Set flow control state. + + Manually control flow of outgoing data - when hardware or software flow + control is enabled. + + Sending will be suspended when called with ``False`` and enabled when + called with ``True``. + + .. versionchanged:: 2.7 (renamed on Posix, function was called ``flowControl``) + .. versionchanged:: 3.0 renamed from ``setXON`` + + .. method:: cancel_read() + + :platform: Posix + :platform: Windows + + Cancel a pending read operation from another thread. A blocking + :meth:`read` call is aborted immediately. :meth:`read` will not report + any error but return all data received up to that point (similar to a + timeout). + + On Posix a call to `cancel_read()` may cancel a future :meth:`read` call. + + .. versionadded:: 3.1 + + .. method:: cancel_write() + + :platform: Posix + :platform: Windows + + Cancel a pending write operation from another thread. The + :meth:`write` method will return immediately (no error indicated). + However the OS may still be sending from the buffer, a separate call to + :meth:`reset_output_buffer` may be needed. + + On Posix a call to `cancel_write()` may cancel a future :meth:`write` call. + + .. versionadded:: 3.1 + + .. note:: The following members are deprecated and will be removed in a + future release. + + .. attribute:: portstr + + .. deprecated:: 2.5 use :attr:`name` instead + + .. method:: inWaiting() + + .. deprecated:: 3.0 see :attr:`in_waiting` + + .. method:: isOpen() + + .. deprecated:: 3.0 see :attr:`is_open` + + .. attribute:: writeTimeout + + .. deprecated:: 3.0 see :attr:`write_timeout` + + .. attribute:: interCharTimeout + + .. deprecated:: 3.0 see :attr:`inter_byte_timeout` + + .. method:: sendBreak(duration=0.25) + + .. deprecated:: 3.0 see :meth:`send_break` + + .. method:: flushInput() + + .. deprecated:: 3.0 see :meth:`reset_input_buffer` + + .. method:: flushOutput() + + .. deprecated:: 3.0 see :meth:`reset_output_buffer` + + .. method:: setBreak(level=True) + + .. deprecated:: 3.0 see :attr:`break_condition` + + .. method:: setRTS(level=True) + + .. deprecated:: 3.0 see :attr:`rts` + + .. method:: setDTR(level=True) + + .. deprecated:: 3.0 see :attr:`dtr` + + .. method:: getCTS() + + .. deprecated:: 3.0 see :attr:`cts` + + .. method:: getDSR() + + .. deprecated:: 3.0 see :attr:`dsr` + + .. method:: getRI() + + .. deprecated:: 3.0 see :attr:`ri` + + .. method:: getCD() + + .. deprecated:: 3.0 see :attr:`cd` + + .. method:: getSettingsDict() + + .. deprecated:: 3.0 see :meth:`get_settings` + + .. method:: applySettingsDict(d) + + .. deprecated:: 3.0 see :meth:`apply_settings` + + .. method:: outWaiting() + + .. deprecated:: 3.0 see :attr:`out_waiting` + + .. method:: setXON(level=True) + + .. deprecated:: 3.0 see :meth:`set_output_flow_control` + + .. method:: flowControlOut(enable) + + .. deprecated:: 3.0 see :meth:`set_input_flow_control` + + .. attribute:: rtsToggle + + :platform: Windows + + Attribute to configure RTS toggle control setting. When enabled and + supported by OS, RTS will be active when data is available and inactive + if no data is available. + + .. versionadded:: 2.6 + .. versionchanged:: 3.0 (removed, see :attr:`rs485_mode` instead) + + +Implementation detail: some attributes and functions are provided by the +class :class:`serial.SerialBase` which inherits from :class:`io.RawIOBase` +and some by the platform specific class and others by the base class +mentioned above. + + +RS485 support +------------- +The :class:`Serial` class has a :attr:`Serial.rs485_mode` attribute which allows to +enable RS485 specific support on some platforms. Currently Windows and Linux +(only a small number of devices) are supported. + +:attr:`Serial.rs485_mode` needs to be set to an instance of +:class:`rs485.RS485Settings` to enable or to ``None`` to disable this feature. + +Usage:: + + import serial + import serial.rs485 + ser = serial.Serial(...) + ser.rs485_mode = serial.rs485.RS485Settings(...) + ser.write(b'hello') + +There is a subclass :class:`rs485.RS485` available to emulate the RS485 support +on regular serial ports (``serial.rs485`` needs to be imported). + + +.. class:: rs485.RS485Settings + + A class that holds RS485 specific settings which are supported on + some platforms. + + .. versionadded:: 3.0 + + .. method:: __init__(rts_level_for_tx=True, rts_level_for_rx=False, loopback=False, delay_before_tx=None, delay_before_rx=None): + + :param bool rts_level_for_tx: + RTS level for transmission + + :param bool rts_level_for_rx: + RTS level for reception + + :param bool loopback: + When set to ``True`` transmitted data is also received. + + :param float delay_before_tx: + Delay after setting RTS but before transmission starts + + :param float delay_before_rx: + Delay after transmission ends and resetting RTS + + .. attribute:: rts_level_for_tx + + RTS level for transmission. + + .. attribute:: rts_level_for_rx + + RTS level for reception. + + .. attribute:: loopback + + When set to ``True`` transmitted data is also received. + + .. attribute:: delay_before_tx + + Delay after setting RTS but before transmission starts (seconds as float). + + .. attribute:: delay_before_rx + + Delay after transmission ends and resetting RTS (seconds as float). + + +.. class:: rs485.RS485 + + A subclass that replaces the :meth:`Serial.write` method with one that toggles RTS + according to the RS485 settings. + + Usage:: + + ser = serial.rs485.RS485(...) + ser.rs485_mode = serial.rs485.RS485Settings(...) + ser.write(b'hello') + + .. warning:: This may work unreliably on some serial ports (control signals not + synchronized or delayed compared to data). Using delays may be unreliable + (varying times, larger than expected) as the OS may not support very fine + grained delays (no smaller than in the order of tens of milliseconds). + + .. note:: Some implementations support this natively in the class + :class:`Serial`. Better performance can be expected when the native version + is used. + + .. note:: The loopback property is ignored by this implementation. The actual + behavior depends on the used hardware. + + + +:rfc:`2217` Network ports +------------------------- + +.. warning:: This implementation is currently in an experimental state. Use + at your own risk. + +.. class:: rfc2217.Serial + + This implements a :rfc:`2217` compatible client. Port names are :ref:`URL + ` in the form: ``rfc2217://:[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 literal 0 HcmV?d00001 diff --git a/sunspec2/tests/test_device.py b/sunspec2/tests/test_device.py new file mode 100644 index 0000000..38417e0 --- /dev/null +++ b/sunspec2/tests/test_device.py @@ -0,0 +1,3336 @@ +import sunspec2.device as device +import sunspec2.mdef as mdef +import sunspec2.mb as mb +import pytest + + +@pytest.fixture +def model_705_data(): + return { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": 3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": 4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": 2000 + } + ] + } + ] + } + + +def test_get_model_info(): + model_info = device.get_model_info(705) + assert model_info[0]['id'] == 705 + assert model_info[1] + assert model_info[2] == 15 + + +def test_check_group_count(): + gdef = {'count': 'NPt'} + gdef2 = {'groups': [{'test': 'test'}, {'count': 'NPt'}]} + gdef3 = {'test1': 'test2', 'groups': [{'test3': 'test3'}, {'test4': 'test4'}]} + assert device.check_group_count(gdef) is True + assert device.check_group_count(gdef2) is True + assert device.check_group_count(gdef3) is False + + +def test_get_model_def(): + with pytest.raises(mdef.ModelDefinitionError) as exc1: + device.get_model_def('z') + assert 'Invalid model id' in str(exc1.value) + + with pytest.raises(Exception) as exc2: + device.get_model_def('000') + assert 'Model definition not found for model' in str(exc2.value) + + assert device.get_model_def(704)['id'] == 704 + + +def test_add_mappings(): + group_def = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + }, + ], + "type": "sync" + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "access": "RW", + "desc": "Reversion power factor setpoint when injecting active power.", + "label": "Reversion Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + ], + "type": "sync" + } + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "static": "S", + "type": "uint16", + "value": 704 + } + ], + "type": "group" + }, + "id": 704 + } + + group_def_w_mappings = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + ], + "type": "sync", + "point_defs": { + "PF": { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + }, + "group_defs": {} + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "access": "RW", + "desc": "Reversion power factor setpoint when injecting active power.", + "label": "Reversion Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + ], + "type": "sync", + "point_defs": { + "PF": { + "access": "RW", + "desc": "Reversion power factor setpoint when injecting active power.", + "label": "Reversion Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + }, + "group_defs": {} + } + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "static": "S", + "type": "uint16", + "value": 704 + } + ], + "type": "group", + "point_defs": { + "ID": { + "name": "ID", + "static": "S", + "type": "uint16", + "value": 704 + } + }, + "group_defs": { + "PFWInj": { + "name": "PFWInj", + "points": [ + { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + ], + "type": "sync", + "point_defs": { + "PF": { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + }, + "group_defs": {} + }, + "PFWInjRvrt": { + "name": "PFWInjRvrt", + "points": [ + { + "access": "RW", + "desc": "Reversion power factor setpoint when injecting active power.", + "label": "Reversion Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + ], + "type": "sync", + "point_defs": { + "PF": { + "access": "RW", + "desc": "Reversion power factor setpoint when injecting active power.", + "label": "Reversion Power Factor (W Inj) ", + "mandatory": "O", + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + } + }, + "group_defs": {} + } + } + }, + "id": 704 + } + device.add_mappings(group_def['group']) + assert group_def == group_def_w_mappings + + +class TestPoint: + def test___init__(self): + p_def = { + "name": "Ena", + "type": "enum16", + "sf": 'test sf', + "static": "S" + } + + p = device.Point(p_def) + assert p.model is None + assert p.pdef == p_def + assert p.info == mb.point_type_info[mdef.TYPE_ENUM16] + assert p.len == 1 + assert p.offset == 0 + assert p.value is None + assert p.dirty is False + assert p.sf == 'test sf' + assert p.sf_required is True + assert p.sf_value is None + assert p.static + + def test__set_data(self): + p_def = { + "name": 'TestPoint', + "type": "uint16" + } + + # bytes + p = device.Point(p_def) + p._set_data(b'\x00\x03') + assert p.value == 3 + assert not p.dirty + + # dict + data = {"TestPoint": 3} + p2 = device.Point(p_def) + p2._set_data(data) + assert p2.value == 3 + + def test_value_getter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + } + p = device.Point(p_def) + p.value = 4 + assert p.value == 4 + + def test_value_setter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + } + p = device.Point(p_def) + p.value = 4 + assert p.value == 4 + + def test_cvalue_getter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + "sf": "TestSF" + } + p = device.Point(p_def) + p.sf_required = True + p.sf_value = 3 + p.value = 4 + setattr(p, "static", True) + assert p.cvalue == 4000.0 + + p_sf = device.Point() + points = {'TestSF': p_sf} + m = device.Model() + setattr(m, 'points', points) + p2 = device.Point(p_def, model=m) + setattr(p2, "static", True) + p2.sf_value = -2 + p2.cvalue = 1.16 + assert p2.cvalue == 1.16 + assert p2.value == 116 + + # test static true + p_sf = device.Point() + setattr(p_sf, "_value", 3) + points = {'TestSF': p_sf} + m = device.Model() + g = device.Group() + setattr(m, 'points', points) + setattr(g, 'points', points) + p5 = device.Point(p_def, model=m) + setattr(p5, "static", True) + setattr(p5, "group", g) + p5.value = 4 + assert p5.cvalue == 4000.0 + + setattr(p_sf, "_value", 4) + assert p5.cvalue == 4000.0 + + # test static false + p_sf = device.Point() + setattr(p_sf, "_value", 3) + points = {'TestSF': p_sf} + m = device.Model() + g = device.Group() + setattr(m, 'points', points) + setattr(g, 'points', points) + p4 = device.Point(p_def, model=m) + setattr(p4, "group", g) + p4.value = 4 + assert p4.cvalue == 4000.0 + + setattr(p_sf, "_value", 4) + assert p4.cvalue == 40000.0 + + def test_cvalue_setter(self): + p_def = { + "name": "TestPoint", + "type": "uint16" + } + p = device.Point(p_def) + p.sf_required = True + p.sf_value = 3 + p.cvalue = 3000 + assert p.value == 3 + + p_sf = device.Point() + points = {'TestSF': p_sf} + m = device.Model() + setattr(m, 'points', points) + p2 = device.Point(p_def, model=m) + p2.sf_value = -2 + p2.cvalue = 1.38 + assert p2.cvalue == 1.38 + assert p2.value == 138 + + # test static true + p_def2 = { + "name": "TestPoint", + "type": "uint16", + "sf": "TestSF" + } + p_sf = device.Point() + setattr(p_sf, "_value", 3) + points = {'TestSF': p_sf} + m = device.Model() + g = device.Group() + setattr(m, 'points', points) + setattr(g, 'points', points) + p5 = device.Point(p_def2, model=m) + setattr(p5, "static", True) + setattr(p5, "group", g) + p5.cvalue = 4000.0 + assert p5.value == 4 + + setattr(p_sf, "_value", 4) + p5.cvalue = 4000.0 + assert p5.value == 4 + + # test static false + p_sf = device.Point() + setattr(p_sf, "_value", 3) + points = {'TestSF': p_sf} + m = device.Model() + g = device.Group() + setattr(m, 'points', points) + setattr(g, 'points', points) + p4 = device.Point(p_def2, model=m) + setattr(p4, "group", g) + p4.cvalue = 4000.0 + assert p4.value == 4 + + setattr(p_sf, "_value", 2) + p4.cvalue = 4000.0 + assert p4.value == 40 + + def test_get_value(self): + p_def = { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "name": "PF", + "type": "uint16", + } + p = device.Point(p_def) + setattr(p, "static", True) + p.value = 3 + assert p.get_value() == 3 + + p2 = device.Point(p_def) + setattr(p2, "static", True) + assert p2.get_value() is None + + # pdef w/ sf + pdef_sf = { + "name": "TestPoint", + "type": "uint16", + "sf": "TestSF" + } + # sf point + sf_p = { + "name": "TestSF", + "value": 3, + "type": "sunssf" + } + + # computed + p_sf = device.Point(sf_p) + p_sf.value = 3 + points = {} + points['TestSF'] = p_sf + m2 = device.Model() + setattr(m2, 'points', points) + + g = device.Group() + setattr(g, 'points', points) + + p9 = device.Point(pdef_sf, model=m2) + setattr(p9, "static", True) + p9.group = g + p9.value = 2020 + assert p9.get_value(computed=True) == 2020000.0 + + p9.sf_value = -2 + p9.cvalue = 1.16 + assert p9.get_value(computed=True) == 1.16 + assert p9.get_value() == 116 + + # computed exception + m3 = device.Model() + points2 = {} + setattr(m3, 'points', points2) + + p10 = device.Point(pdef_sf, model=m3) + setattr(p10, "static", True) + g2 = device.Group() + setattr(g2, 'points', {}) + p10.group = g2 + p10.value = 2020 + with pytest.raises(device.ModelError) as exc: + p10.get_value(computed=True) + assert 'Scale factor TestSF for point TestPoint not found' in str(exc.value) + + def test_set_value(self): + p_def = { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "name": "PF", + "type": "uint16" + } + p = device.Point(p_def) + setattr(p, "static", True) + p.set_value(3) + assert p.value == 3 + + # test computed + pdef_computed = { + "name": "TestingComputed", + "type": "uint16", + "sf": "TestSF" + } + p_SF = device.Point() + p_SF.value = 2 + + points = {} + points['TestSF'] = p_SF + m = device.Model() + setattr(m, 'points', points) + + p3 = device.Point(pdef_computed, model=m) + setattr(p3, "static", True) + g = device.Group + setattr(g, 'points', {}) + p3.group = g + p3.set_value(1000, computed=True, dirty=True) + assert p3.value == 10 + assert p3.dirty + + # test exceptions + p2_sf = device.Point() + m2 = device.Model() + points2 = {} + points2['TestSF'] = p2_sf + setattr(m2, 'points', points2) + + p4 = device.Point(pdef_computed, model=m2) + setattr(p4, "static", True) + p4.group = g + with pytest.raises(device.ModelError) as exc: + p4.set_value(1000, computed=True) + assert 'SF field TestSF value not initialized for point TestingComputed' in str(exc.value) + + # test computed float rounding + p5 = device.Point(pdef_computed, model=m2) + setattr(p5, "static", True) + p5.sf_value = -2 + p5.set_value(1.16, computed=True) + assert p5.get_value(computed=True) == 1.16 + assert p5.get_value() == 116 + + def test_get_mb(self): + p_def = { + "name": "ESVLo", + "type": "uint16", + "sf": "TestSF" + } + p = device.Point(p_def) + setattr(p, "static", True) + p.value = 3 + assert p.get_mb() == b'\x00\x03' + p.value = None + assert p.get_mb() == b'\xff\xff' + assert p.get_mb(computed=True) == b'\xff\xff' + + # computed + p.value = 3 + p.sf_required = True + p.sf_value = 4 + assert p.get_mb(computed=True) == b'\x75\x30' + + # test static true + p_sf = device.Point() + setattr(p_sf, "_value", 3) + points = {'TestSF': p_sf} + m = device.Model() + g = device.Group() + setattr(m, 'points', points) + setattr(g, 'points', points) + p2 = device.Point(p_def, model=m) + setattr(p2, "static", True) + setattr(p2, "group", g) + p2.value = 4 + assert p2.get_mb(computed=True) == b'\x0f\xa0' + + setattr(p_sf, "_value", 4) + assert p2.get_mb(computed=True) == b'\x0f\xa0' + + # test static false + p_sf = device.Point() + setattr(p_sf, "_value", 3) + points = {'TestSF': p_sf} + m = device.Model() + g = device.Group() + setattr(m, 'points', points) + setattr(g, 'points', points) + p4 = device.Point(p_def, model=m) + setattr(p4, "group", g) + p4.value = 4 + assert p4.get_mb(computed=True) == b'\x0f\xa0' + + setattr(p_sf, "_value", 4) + assert p4.get_mb(computed=True) == b'\x9c\x40' + + def test_set_mb(self): + p_def = { + "name": "ESVLo", + "type": "uint16", + } + m = device.Model() + g = device.Group() + g.points = {} + p3 = device.Point(p_def, m) + p3.group = g + p3.set_mb(None) + assert p3.model.error_info == "Error setting value for ESVLo: object of type 'NoneType' has no len()\n" + + # exceptions + p_def2 = { + "name": "ESVLo", + "type": "uint16", + "sf": "TestSF" + } + p_sf = device.Point() + points = {} + points['TestSF'] = p_sf + setattr(m, 'points', points) + + m.error_info = '' + p4 = device.Point(p_def2, model=m) + p4.group = g + p4.set_mb(b'\x00\x03', computed=True) + assert m.error_info == 'Error setting value for ESVLo: SF field TestSF value not initialized for point ESVLo\n' + + del m.points['TestSF'] + m.error_info = '' + p5 = device.Point(p_def2, model=m) + p5.group = g + p5.set_mb(b'\x00\x04', computed=True) + assert m.error_info == 'Error setting value for ESVLo: Scale factor TestSF for point ESVLo not found\n' + + # test computed + pdef_computed = { + "name": "TestingComputed", + "type": "uint16", + "sf": "TestSF" + } + p_SF = device.Point() + p_SF.value = 2 + + points = {} + points['TestSF'] = p_SF + m = device.Model() + setattr(m, 'points', points) + + p6 = device.Point(pdef_computed, model=m) + p6.group = g + p6.set_mb(b'\x0b\xb8', computed=True, dirty=True) + assert p6.value == 30 + assert p6.dirty + + # test static true + p_def2 = { + "name": "TestPoint", + "type": "uint16", + "sf": "TestSF" + } + p_sf = device.Point() + setattr(p_sf, "_value", 3) + points = {'TestSF': p_sf} + m = device.Model() + g = device.Group() + setattr(m, 'points', points) + setattr(g, 'points', points) + p7 = device.Point(p_def2, model=m) + setattr(p7, "static", True) + setattr(p7, "group", g) + p7.set_mb(b'\x0f\xa0', computed=True, dirty=True) + assert p7.value == 4 + + setattr(p_sf, "_value", 4) + p7.set_mb(b'\x0f\xa0', computed=True, dirty=True) + assert p7.value == 4 + + # test static false + p_sf = device.Point() + setattr(p_sf, "_value", 3) + points = {'TestSF': p_sf} + m = device.Model() + g = device.Group() + setattr(m, 'points', points) + setattr(g, 'points', points) + p8 = device.Point(p_def2, model=m) + setattr(p8, "group", g) + p8.set_mb(b'\x9c\x40', computed=True, dirty=True) + assert p8.value == 40 + + setattr(p_sf, "_value", 4) + p8.set_mb(b'\x9c\x40', computed=True, dirty=True) + assert p8.value == 4 + + def test_get_text(self, model_705_data): + m = device.Model(705, data=model_705_data) + p = m.NPt + expected_output = ' NPt 4\n' + assert p.get_text() == expected_output + + +class TestGroup: + def test___init__(self): + g_704 = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + }, + ], + "type": "sync" + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "name": "Ext", + "type": "enum16" + } + ], + "type": "sync" + }, + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 704 + }, + { + "name": "L", + "static": "S", + "type": "uint16" + }, + + { + "name": "PFWInjRvrtTms", + "type": "uint32", + }, + { + "name": "PFWInjRvrtRem", + "type": "uint32", + }, + { + "name": "PFWAbsEna", + "type": "enum16" + }, + { + "name": "PF_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 704 + } + g = device.Group(g_704['group']) + + assert g.gdef == g_704['group'] + assert g.model is None + assert g.gname == 'DERCtlAC' + assert g.offset == 0 + assert g.len == 10 + assert len(g.points) == 6 + assert len(g.groups) == 2 + assert g.points_len == 8 + assert g.group_class == device.Group + + def test___getattr__(self): + g_704 = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + }, + ], + "type": "sync" + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "name": "Ext", + "type": "enum16" + } + ], + "type": "sync" + }, + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 704 + }, + { + "name": "L", + "static": "S", + "type": "uint16" + }, + + { + "name": "PFWInjRvrtTms", + "type": "uint32", + }, + { + "name": "PFWInjRvrtRem", + "type": "uint32", + }, + { + "name": "PFWAbsEna", + "type": "enum16" + }, + { + "name": "PF_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 704 + } + g = device.Group(g_704['group']) + with pytest.raises(AttributeError) as exc: + g.qwerty + assert "Group object has no attribute qwerty" in str(exc.value) + assert g.ID + assert g.PFWAbsEna + + def test__group_data(self): + gdef_705 = { + "group": { + "groups": [ + { + "count": "NCrv", + "groups": [ + { + "count": "NPt", + "name": "Pt", + "points": [ + { + "name": "V", + "sf": "V_SF", + "type": "uint16", + }, + { + "name": "Var", + "sf": "DeptRef_SF", + "type": "int16", + "units": "VarPct" + } + ], + "type": "group" + } + ], + "name": "Crv", + "points": [ + { + "name": "ActPt", + "type": "uint16" + }, + { + "name": "DeptRef", + "symbols": [ + { + "name": "W_MAX_PCT", + "value": 1 + }, + { + "name": "VAR_MAX_PCT", + "value": 2 + }, + { + "name": "VAR_AVAL_PCT", + "value": 3 + } + ], + "type": "enum16" + }, + { + "name": "Pri", + "symbols": [ + { + "name": "ACTIVE", + "value": 1 + }, + { + "name": "REACTIVE", + "value": 2 + }, + { + "name": "IEEE_1547", + "value": 3 + }, + { + "name": "PF", + "value": 4 + }, + { + "name": "VENDOR", + "value": 5 + } + ], + "type": "enum16" + }, + { + "name": "VRef", + "type": "uint16" + }, + { + "name": "VRefAuto", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "VRefTms", + "type": "uint16" + }, + { + "name": "RspTms", + "type": "uint16" + }, + { + "name": "ReadOnly", + "symbols": [ + { + "name": "RW", + "value": 0 + }, + { + "name": "R", + "value": 1 + } + ], + "type": "enum16" + } + ], + "type": "group" + } + ], + "name": "DERVoltVar", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 705 + }, + { + "name": "L", + "type": "uint16" + }, + { + "name": "Ena", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "CrvSt", + "symbols": [ + { + "name": "INACTIVE", + "value": 0 + }, + { + "name": "ACTIVE", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "AdptCrvReq", + "type": "uint16" + }, + { + "name": "AdptCrvRslt", + "symbols": [ + { + "name": "IN_PROGRESS", + "value": 0 + }, + { + "name": "COMPLETED", + "value": 1 + }, + { + "name": "FAILED", + "value": 2 + } + ], + "type": "enum16" + }, + { + "name": "NPt", + "type": "uint16" + }, + { + "name": "NCrv", + "type": "uint16" + }, + { + "name": "RvrtTms", + "type": "uint32" + }, + { + "name": "RvrtRem", + "type": "uint32" + }, + { + "name": "RvrtCrv", + "type": "uint16" + }, + { + "name": "V_SF", + "type": "sunssf" + }, + { + "name": "DeptRef_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 705 + } + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + g = device.Group() + assert g._group_data(gdata_705, 'Crv') == [{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, + {'V': 10200, 'Var': 0}, {'V': 10600, 'Var': -4000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, + {'V': 10500, 'Var': 0}, {'V': 10800, 'Var': -2000}]}] + + assert g._group_data(gdata_705['Crv'], index=0) == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, + 'VRefAuto': 0, 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} + + def test__get_data_group_count(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + g = device.Group() + assert g._get_data_group_count(gdata_705['Crv']) == 3 + + def test__init_repeating_group(self): + gdef_705 = { + "group": { + "groups": [ + { + "count": "NCrv", + "groups": [ + { + "count": "NPt", + "name": "Pt", + "points": [ + { + "name": "V", + "sf": "V_SF", + "type": "uint16", + }, + { + "name": "Var", + "sf": "DeptRef_SF", + "type": "int16", + "units": "VarPct" + } + ], + "type": "group" + } + ], + "name": "Crv", + "points": [ + { + "name": "ActPt", + "type": "uint16" + }, + { + "name": "DeptRef", + "symbols": [ + { + "name": "W_MAX_PCT", + "value": 1 + }, + { + "name": "VAR_MAX_PCT", + "value": 2 + }, + { + "name": "VAR_AVAL_PCT", + "value": 3 + } + ], + "type": "enum16" + }, + { + "name": "Pri", + "symbols": [ + { + "name": "ACTIVE", + "value": 1 + }, + { + "name": "REACTIVE", + "value": 2 + }, + { + "name": "IEEE_1547", + "value": 3 + }, + { + "name": "PF", + "value": 4 + }, + { + "name": "VENDOR", + "value": 5 + } + ], + "type": "enum16" + }, + { + "name": "VRef", + "type": "uint16" + }, + { + "name": "VRefAuto", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "VRefTms", + "type": "uint16" + }, + { + "name": "RspTms", + "type": "uint16" + }, + { + "name": "ReadOnly", + "symbols": [ + { + "name": "RW", + "value": 0 + }, + { + "name": "R", + "value": 1 + } + ], + "type": "enum16" + } + ], + "type": "group" + } + ], + "name": "DERVoltVar", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 705 + }, + { + "name": "L", + "type": "uint16" + }, + { + "name": "Ena", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "CrvSt", + "symbols": [ + { + "name": "INACTIVE", + "value": 0 + }, + { + "name": "ACTIVE", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "AdptCrvReq", + "type": "uint16" + }, + { + "name": "AdptCrvRslt", + "symbols": [ + { + "name": "IN_PROGRESS", + "value": 0 + }, + { + "name": "COMPLETED", + "value": 1 + }, + { + "name": "FAILED", + "value": 2 + } + ], + "type": "enum16" + }, + { + "name": "NPt", + "type": "uint16" + }, + { + "name": "NCrv", + "type": "uint16" + }, + { + "name": "RvrtTms", + "type": "uint32" + }, + { + "name": "RvrtRem", + "type": "uint32" + }, + { + "name": "RvrtCrv", + "type": "uint16" + }, + { + "name": "V_SF", + "type": "sunssf" + }, + { + "name": "DeptRef_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 705 + } + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + + m = device.Model(705, data=gdata_705) + + pdef_NPt = {"name": "NPt", "type": "uint16"} + p_NPt = device.Point(pdef_NPt) + p_NPt.value = 4 + + pdef_NCrv = {"name": "NCrv", "type": "uint16"} + p_NCrv = device.Point(pdef_NCrv) + points = {'NPt': p_NPt, 'NCrv': p_NCrv} + setattr(m, 'points', points) + + g2 = device.Group(gdef_705['group']['groups'][0], m) + + with pytest.raises(device.ModelError) as exc: + g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) + assert 'Count field NCrv value not initialized for group Crv' in str(exc.value) + + # set value for NCrv count and reset the points attribute on model + p_NCrv.value = 3 + setattr(m, 'points', points) + groups = g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) + assert len(groups) == 3 + assert len(groups[0].groups['Pt']) == 4 + + def test_get_dict(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m2 = device.Model(705, data=gdata_705) + assert m2.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefAutoEna': None, 'VRefAutoTms': None, + 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, + {'V': 10700, 'Var': -3000}]} + + # test computed + m2.groups['Crv'][0].points['DeptRef'].sf_required = True + m2.groups['Crv'][0].points['DeptRef'].sf_value = -2 + m2.groups['Crv'][0].DeptRef.cvalue = 1.16 + m2.groups['Crv'][0].points['Pri'].sf_required = True + m2.groups['Crv'][0].points['Pri'].sf_value = 3 + computed_dict = m2.groups['Crv'][0].get_dict(computed=True) + assert computed_dict['DeptRef'] == 1.16 + assert computed_dict['Pri'] == 1000.0 + + def test_set_dict(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "RspTms_SF": 1, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + assert m.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefAutoEna': None, 'VRefAutoTms': None, + 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, + {'V': 10700, 'Var': -3000}]} + new_dict = {'ActPt': 4, 'DeptRef': 4000, 'Pri': 5000, 'VRef': 3, 'VRefAuto': 2, + 'VRefAutoEna': None, 'VRefAutoTms': None, + 'RspTms': 2, 'ReadOnly': 2, + 'Pt': [{'V': 111, 'Var': 111}, {'V': 123, 'Var': 1112}, + {'V': 111, 'Var': 111}, + {'V': 123, 'Var': -1112}]} + + m.groups['Crv'][0].set_dict(new_dict, dirty=True) + assert m.groups['Crv'][0].get_dict() == new_dict + assert m.groups['Crv'][0].VRef.value == 3 + assert m.groups['Crv'][0].VRef.dirty + assert m.groups['Crv'][0].Pri.dirty + + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + m.groups['Crv'][0].set_dict(new_dict, computed=True) + computed_dict = m.groups['Crv'][0].get_dict() + assert computed_dict['DeptRef'] == 4.0 + assert computed_dict['Pri'] == 5.0 + + m.groups['Crv'][0].DeptRef.sf_value = -2 + float_dict = {'DeptRef': 1.16} + m.groups['Crv'][0].set_dict(float_dict, computed=True) + assert m.groups['Crv'][0].DeptRef.value == 116 + assert m.groups['Crv'][0].DeptRef.cvalue == 1.16 + + def test_get_json(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \ + ''' "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 1,''' + \ + ''' "Pt": [{"V": 9200, "Var": 3000}, {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0},''' + \ + ''' {"V": 10700, "Var": -3000}]}''' + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + assert m.groups['Crv'][0].get_json(computed=True) == '''{"ActPt": 4, "DeptRef": 1000.0, "Pri": 1000.0,''' + \ + ''' "VRef": 0.01, "VRefAuto": 0.0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6,''' + \ + ''' "ReadOnly": 1, "Pt": [{"V": 92.0, "Var": 30.0}, {"V": 96.7, "Var": 0.0},''' + \ + ''' {"V": 103.0, "Var": 0.0}, {"V": 107.0, "Var": -30.0}]}''' + + def test_set_json(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "RspTms_SF": 1, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \ + ''' "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6,''' + \ + ''' "ReadOnly": 1, "Pt": [{"V": 9200, "Var": 3000}, {"V": 9670, "Var": 0},''' + \ + ''' {"V": 10300, "Var": 0}, {"V": 10700, "Var": -3000}]}''' + + json_to_set = '''{"ActPt": 4, "DeptRef": 9999, "Pri": 9999, "VRef": 99, "VRefAuto": 88,''' + \ + ''' "VRefAutoEna": null, "VRefAutoTms": 2, "RspTms": 2, "ReadOnly": 77,''' + \ + ''' "Pt": [{"V": 77, "Var": 66}, {"V": 55, "Var": 44}, {"V": 33, "Var": 22},''' + \ + ''' {"V": 111, "Var": -2222}]}''' + + m.groups['Crv'][0].set_json(json_to_set) + assert m.groups['Crv'][0].get_json() == json_to_set + assert m.groups['Crv'][0].DeptRef.value == 9999 + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + m.groups['Crv'][0].set_json(json_to_set, computed=True, dirty=True) + assert m.groups['Crv'][0].points['DeptRef'].value == 10 + assert m.groups['Crv'][0].points['DeptRef'].dirty + assert m.groups['Crv'][0].points['Pri'].value == 10 + assert m.groups['Crv'][0].points['Pri'].dirty + + def test_get_mb(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00' \ + b'\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + assert m.groups['Crv'][0].get_mb(computed=True) == b'\x00\x04\x03\xe8\x03\xe8\x00\x00\x00\x00\xff\xff\xff' \ + b'\xff\x00\x00\x00\x06\x00\x01\x00\\\x00\x1e\x00`\x00' \ + b'\x00\x00g\x00\x00\x00k\xff\xe2' + + def test_set_mb(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00' \ + b'\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' + + bs = b'\x00\x04\x03\xe7\x03x\x03\t\x02\x9a\x02+\x01\xbc\x01M\x00\xde\x00o' \ + b'\x00\xde\x01M\x01\xbc\x02+\x02\x9a\xfc\xf7\xf4H\x0b\xb8' + + m.groups['Crv'][0].set_mb(bs, dirty=True) + assert m.groups['Crv'][0].get_mb() == bs + assert m.groups['Crv'][0].DeptRef.value == 999 + assert m.groups['Crv'][0].DeptRef.dirty + + # test computed + # set points DeptRef and Pri to 3000 w/ byte string + computed_bs = b'\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<' \ + b'\x00\x00)\xcc\xf4H' + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + m.groups['Crv'][0].set_mb(computed_bs, computed=True) + assert m.groups['Crv'][0].points['DeptRef'].value == 3 + assert m.groups['Crv'][0].points['Pri'].value == 3 + + def test_get_text(self, model_705_data): + m = device.Model(705, data=model_705_data) + g = m.groups['Crv'][0] + expected_output = ''' ActPt 4\n''' + \ + ''' DeptRef 1\n''' + \ + ''' Pri 1\n''' + \ + ''' VRef 1 VNomPct\n''' + \ + ''' VRefAuto 0 VNomPct\n''' + \ + ''' VRefAutoEna None\n''' + \ + ''' VRefAutoTms None Secs\n''' + \ + ''' RspTms 6 Secs\n''' + \ + ''' ReadOnly 1\n''' + \ + ''' 01:V 9200 VNomPct\n''' + \ + ''' 01:Var 3000 DeptRef\n''' + \ + ''' 02:V 9670 VNomPct\n''' + \ + ''' 02:Var 0 DeptRef\n''' + \ + ''' 03:V 10300 VNomPct\n''' + \ + ''' 03:Var 0 DeptRef\n''' + \ + ''' 04:V 10700 VNomPct\n''' + \ + ''' 04:Var 3000 DeptRef\n''' + assert g.get_text() == expected_output + + +class TestModel: + + def test__init__(self): + m = device.Model(704) + assert m.model_id == 704 + assert m.model_addr == 0 + assert m.model_len == 0 + assert m.model_def['id'] == 704 + assert m.error_info == '' + assert m.gdef['name'] == 'DERCtlAC' + assert m.mid is None + assert m.device is None + + assert m.model == m + m2 = device.Model('abc') + assert m2.error_info == 'Invalid model id: abc\n' + + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + # test repeating group model + m2 = device.Model(705, data=gdata_705) + assert m2.model_id == 705 + assert m2.model_addr == 0 + assert m2.model_len == 0 + assert m2.model_def['id'] == 705 + assert m2.error_info == '' + assert m2.gdef['name'] == 'DERVoltVar' + assert m2.mid is None + assert m2.device is None + + +def test_model_1(): + mdata = { + "ID": 1, + "L": 68, + "Mn": "Test manuf", + "Md": "Test model", + "Opt": "Test options", + "Vr": "Test version", + "SN": "Test serial num", + "DA": 12, + "Pad": 0 + } + m = device.Model(1, data=mdata) + + +def test__error(): + m = device.Model(704) + m.add_error('test error') + assert m.error_info == 'test error\n' + + +def test_get_text(model_705_data): + m = device.Model(705, data=model_705_data) + expected_output = ''' ID 705\n''' + \ + ''' L 67\n''' + \ + ''' Ena 1\n''' + \ + ''' AdptCrvReq 0\n''' + \ + ''' AdptCrvRslt 0\n''' + \ + ''' NPt 4\n''' + \ + ''' NCrv 3\n''' + \ + ''' RvrtTms 0 Secs\n''' + \ + ''' RvrtRem 0 Secs\n''' + \ + ''' RvrtCrv 0\n''' + \ + ''' V_SF -2\n''' + \ + ''' DeptRef_SF -2\n''' + \ + ''' RspTms_SF None\n''' + \ + ''' 01:ActPt 4\n''' + \ + ''' 01:DeptRef 1\n''' + \ + ''' 01:Pri 1\n''' + \ + ''' 01:VRef 1 VNomPct\n''' + \ + ''' 01:VRefAuto 0 VNomPct\n''' + \ + ''' 01:VRefAutoEna None\n''' + \ + ''' 01:VRefAutoTms None Secs\n''' + \ + ''' 01:RspTms 6 Secs\n''' + \ + ''' 01:ReadOnly 1\n''' + \ + '''01:01:V 9200 VNomPct\n''' + \ + '''01:01:Var 3000 DeptRef\n''' + \ + '''01:02:V 9670 VNomPct\n''' + \ + '''01:02:Var 0 DeptRef\n''' + \ + '''01:03:V 10300 VNomPct\n''' + \ + '''01:03:Var 0 DeptRef\n''' + \ + '''01:04:V 10700 VNomPct\n''' + \ + '''01:04:Var 3000 DeptRef\n''' + \ + ''' 02:ActPt 4\n''' + \ + ''' 02:DeptRef 1\n''' + \ + ''' 02:Pri 1\n''' + \ + ''' 02:VRef 1 VNomPct\n''' + \ + ''' 02:VRefAuto 0 VNomPct\n''' + \ + ''' 02:VRefAutoEna None\n''' + \ + ''' 02:VRefAutoTms None Secs\n''' + \ + ''' 02:RspTms 6 Secs\n''' + \ + ''' 02:ReadOnly 0\n''' + \ + '''02:01:V 9300 VNomPct\n''' + \ + '''02:01:Var 3000 DeptRef\n''' + \ + '''02:02:V 9570 VNomPct\n''' + \ + '''02:02:Var 0 DeptRef\n''' + \ + '''02:03:V 10200 VNomPct\n''' + \ + '''02:03:Var 0 DeptRef\n''' + \ + '''02:04:V 10600 VNomPct\n''' + \ + '''02:04:Var 4000 DeptRef\n''' + \ + ''' 03:ActPt 4\n''' + \ + ''' 03:DeptRef 1\n''' + \ + ''' 03:Pri 1\n''' + \ + ''' 03:VRef 1 VNomPct\n''' + \ + ''' 03:VRefAuto 0 VNomPct\n''' + \ + ''' 03:VRefAutoEna None\n''' + \ + ''' 03:VRefAutoTms None Secs\n''' + \ + ''' 03:RspTms 6 Secs\n''' + \ + ''' 03:ReadOnly 0\n''' + \ + '''03:01:V 9400 VNomPct\n''' + \ + '''03:01:Var 2000 DeptRef\n''' + \ + '''03:02:V 9570 VNomPct\n''' + \ + '''03:02:Var 0 DeptRef\n''' + \ + '''03:03:V 10500 VNomPct\n''' + \ + '''03:03:Var 0 DeptRef\n''' + \ + '''03:04:V 10800 VNomPct\n''' + \ + '''03:04:Var 2000 DeptRef\n''' + assert expected_output == m.get_text() + + +class TestDevice: + def test__init__(self): + d = device.Device() + assert d.name is None + assert d.did is None + assert d.models == {} + assert d.model_list == [] + assert d.model_class == device.Model + + def test__get_attr__(self): + d = device.Device() + m = device.Model() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + d.add_model(m) + assert d.mid_test + + with pytest.raises(AttributeError) as exc: + d.foo + assert "\'Device\' object has no attribute \'foo\'" in str(exc.value) + + def test_scan(self): + pass + + def test_add_model(self): + d = device.Device() + m = device.Model() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + d.add_model(m) + assert d.models['mid_test'] + assert d.models['group_test'] + assert m.device == d + + def test_get_dict(self): + d = device.Device() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + d.add_model(m) + assert d.get_dict() == {'name': None, 'did': None, 'models': [ + {'ID': 705, 'L': 67, 'Ena': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, 'RvrtTms': 0, + 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'RspTms_SF': None, 'Crv': [ + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefAutoTms': None, + 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, {'V': 10300, 'Var': 0}, + {'V': 10700, 'Var': -3000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefAutoTms': None, + 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, {'V': 10200, 'Var': 0}, + {'V': 10600, 'Var': -4000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefAutoTms': None, + 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, {'V': 10500, 'Var': 0}, + {'V': 10800, 'Var': -2000}]}], 'mid': None, 'error': '', 'model_id': 705}]} + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + assert d.get_dict(computed=True) == {'name': None, 'did': None, 'models': [ + {'ID': 705, 'L': 67, 'Ena': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, 'RvrtTms': 0, + 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'RspTms_SF': None, 'Crv': [ + {'ActPt': 4, 'DeptRef': 1000.0, 'Pri': 1000.0, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None, + 'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 92.0, 'Var': 30.0}, {'V': 96.7, 'Var': 0.0}, {'V': 103.0, 'Var': 0.0}, + {'V': 107.0, 'Var': -30.0}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None, + 'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 93.0, 'Var': 30.0}, {'V': 95.7, 'Var': 0.0}, {'V': 102.0, 'Var': 0.0}, + {'V': 106.0, 'Var': -40.0}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None, + 'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 94.0, 'Var': 20.0}, {'V': 95.7, 'Var': 0.0}, {'V': 105.0, 'Var': 0.0}, + {'V': 108.0, 'Var': -20.0}]}], 'mid': None, 'error': '', 'model_id': 705}]} + + def test_get_json(self): + d = device.Device() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + d.add_model(m) + assert d.get_json() == '''{"name": null, "did": null, "models": [{"ID": 705, "L": 67, "Ena": 1,''' + \ + ''' "AdptCrvReq": 0, "AdptCrvRslt": 0, "NPt": 4, "NCrv": 3, "RvrtTms": 0, "RvrtRem": 0,''' + \ + ''' "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2, "RspTms_SF": null, "Crv": [{"ActPt": 4,''' + \ + ''' "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null,''' + \ + ''' "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 9200, "Var": 3000}, {"V": 9670, "Var": 0},''' + \ + ''' {"V": 10300, "Var": 0}, {"V": 10700, "Var": -3000}]}, {"ActPt": 4, "DeptRef": 1,''' + \ + ''' "Pri": 1, "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null,''' + \ + ''' "RspTms": 6, "ReadOnly": 0, "Pt": [{"V": 9300, "Var": 3000}, {"V": 9570, "Var": 0},''' + \ + ''' {"V": 10200, "Var": 0}, {"V": 10600, "Var": -4000}]}, {"ActPt": 4, "DeptRef": 1,''' + \ + ''' "Pri": 1, "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null,''' + \ + ''' "RspTms": 6, "ReadOnly": 0, "Pt": [{"V": 9400, "Var": 2000}, {"V": 9570, "Var": 0},''' + \ + ''' {"V": 10500, "Var": 0}, {"V": 10800, "Var": -2000}]}], "mid": null, "error": "",''' + \ + ''' "model_id": 705}]}''' + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + assert d.get_json(computed=True) == '''{"name": null, "did": null, "models":''' + \ + ''' [{"ID": 705, "L": 67, "Ena": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0,''' + \ + ''' "NPt": 4, "NCrv": 3, "RvrtTms": 0, "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2,''' + \ + ''' "DeptRef_SF": -2, "RspTms_SF": null, "Crv": [{"ActPt": 4, "DeptRef": 1000.0,''' + \ + ''' "Pri": 1000.0, "VRef": 0.01, "VRefAuto": 0.0, "VRefAutoEna": null,''' + \ + ''' "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 92.0, "Var": 30.0},''' + \ + ''' {"V": 96.7, "Var": 0.0}, {"V": 103.0, "Var": 0.0}, {"V": 107.0, "Var": -30.0}]},''' + \ + ''' {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 0.01, "VRefAuto": 0.0,''' + \ + ''' "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 0,''' + \ + ''' "Pt": [{"V": 93.0, "Var": 30.0}, {"V": 95.7, "Var": 0.0}, {"V": 102.0, "Var": 0.0},''' + \ + ''' {"V": 106.0, "Var": -40.0}]}, {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 0.01,''' + \ + ''' "VRefAuto": 0.0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 0,''' + \ + ''' "Pt": [{"V": 94.0, "Var": 20.0}, {"V": 95.7, "Var": 0.0}, {"V": 105.0, "Var": 0.0},''' + \ + ''' {"V": 108.0, "Var": -20.0}]}], "mid": null, "error": "", "model_id": 705}]}''' + + def test_get_mb(self): + d = device.Device() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": 3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": 4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": 2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + d.add_model(m) + assert d.get_mb() == b"\x02\xc1\x00C\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\xff\xfe\xff\xfe\x80\x00\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff" \ + b"\xff\x00\x00\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\x0b\xb8\x00\x04" \ + b"\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x00$T\x0b\xb8%b" \ + b"\x00\x00'\xd8\x00\x00)h\x0f\xa0\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff" \ + b"\xff\x00\x00\x00\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\x07\xd0" + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + assert d.get_mb(computed=True) == b'\x02\xc1\x00C\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\xff\xfe\xff\xfe\x80\x00\x00\x04\x03\xe8\x03\xe8\x00' \ + b'\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x01\x00\\\x00\x1e\x00`' \ + b'\x00\x00\x00g\x00\x00\x00k\x00\x1e\x00\x04\x00\x01\x00\x01\x00\x00\x00' \ + b'\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x00\x00]\x00\x1e\x00_\x00\x00' \ + b'\x00f\x00\x00\x00j\x00(\x00\x04\x00\x01\x00\x01\x00\x00\x00\x00\xff\xff' \ + b'\xff\xff\x00\x00\x00\x06\x00\x00\x00^\x00\x14\x00_\x00\x00\x00i\x00\x00' \ + b'\x00l\x00\x14' + + def test_set_mb(self): + d = device.Device() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": 3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": 4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": 2000 + } + ] + } + ] + } + m = device.Model(705, data=gdata_705) + d.add_model(m) + assert d.get_mb() == b"\x02\xc1\x00C\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\xff\xfe\xff\xfe\x80\x00\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff" \ + b"\xff\xff\xff\x00\x00\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\x0b" \ + b"\xb8\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00" \ + b"\x00$T\x0b\xb8%b\x00\x00'\xd8\x00\x00)h\x0f\xa0\x00\x04\x00\x01\x00\x01\x00\x01\x00" \ + b"\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0" \ + b"\x07\xd0" + + # DeptRef and Pri set to 3000 in byte string + bs = b"\x02\xc1\x00?\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00" \ + b"\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04\x00\x01\x00\x01\x00" \ + b"\x01\x00\x00\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00\x00'\xd8\x00\x00)h\xf0`\x00\x04" \ + b"\x00\x01\x00\x01\x00\x01\x00\x00\x00\x05\x00\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" + d.set_mb(bs, dirty=True) + assert m.groups['Crv'][0].DeptRef.value == 3000 + assert m.groups['Crv'][0].DeptRef.dirty + assert m.groups['Crv'][0].Pri.value == 3000 + assert m.groups['Crv'][0].Pri.dirty + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + d.set_mb(bs, computed=True, dirty=False) + assert m.groups['Crv'][0].DeptRef.value == 3 + assert not m.groups['Crv'][0].DeptRef.dirty + assert m.groups['Crv'][0].Pri.value == 3 + assert not m.groups['Crv'][0].Pri.dirty + + def test_find_mid(self): + d = device.Device() + m = device.Model() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + setattr(m, 'mid', 'mid_test') + d.add_model(m) + assert d.find_mid('mid_test') == m + + def test_get_text(self, model_705_data): + d = device.Device() + m = device.Model(705, data=model_705_data) + d.add_model(m) + get_text_expected_output = '''Model: DERVoltVar (705)\n\n''' + \ + ''' ID 705\n''' + \ + ''' L 67\n''' + \ + ''' Ena 1\n''' + \ + ''' AdptCrvReq 0\n''' + \ + ''' AdptCrvRslt 0\n''' + \ + ''' NPt 4\n''' + \ + ''' NCrv 3\n''' + \ + ''' RvrtTms 0 Secs\n''' + \ + ''' RvrtRem 0 Secs\n''' + \ + ''' RvrtCrv 0\n''' + \ + ''' V_SF -2\n''' + \ + ''' DeptRef_SF -2\n''' + \ + ''' RspTms_SF None\n''' + \ + ''' 01:ActPt 4\n''' + \ + ''' 01:DeptRef 1\n''' + \ + ''' 01:Pri 1\n''' + \ + ''' 01:VRef 1 VNomPct\n''' + \ + ''' 01:VRefAuto 0 VNomPct\n''' + \ + ''' 01:VRefAutoEna None\n''' + \ + ''' 01:VRefAutoTms None Secs\n''' + \ + ''' 01:RspTms 6 Secs\n''' + \ + ''' 01:ReadOnly 1\n''' + \ + '''01:01:V 9200 VNomPct\n''' + \ + '''01:01:Var 3000 DeptRef\n''' + \ + '''01:02:V 9670 VNomPct\n''' + \ + '''01:02:Var 0 DeptRef\n''' + \ + '''01:03:V 10300 VNomPct\n''' + \ + '''01:03:Var 0 DeptRef\n''' + \ + '''01:04:V 10700 VNomPct\n''' + \ + '''01:04:Var 3000 DeptRef\n''' + \ + ''' 02:ActPt 4\n''' + \ + ''' 02:DeptRef 1\n''' + \ + ''' 02:Pri 1\n''' + \ + ''' 02:VRef 1 VNomPct\n''' + \ + ''' 02:VRefAuto 0 VNomPct\n''' + \ + ''' 02:VRefAutoEna None\n''' + \ + ''' 02:VRefAutoTms None Secs\n''' + \ + ''' 02:RspTms 6 Secs\n''' + \ + ''' 02:ReadOnly 0\n''' + \ + '''02:01:V 9300 VNomPct\n''' + \ + '''02:01:Var 3000 DeptRef\n''' + \ + '''02:02:V 9570 VNomPct\n''' + \ + '''02:02:Var 0 DeptRef\n''' + \ + '''02:03:V 10200 VNomPct\n''' + \ + '''02:03:Var 0 DeptRef\n''' + \ + '''02:04:V 10600 VNomPct\n''' + \ + '''02:04:Var 4000 DeptRef\n''' + \ + ''' 03:ActPt 4\n''' + \ + ''' 03:DeptRef 1\n''' + \ + ''' 03:Pri 1\n''' + \ + ''' 03:VRef 1 VNomPct\n''' + \ + ''' 03:VRefAuto 0 VNomPct\n''' + \ + ''' 03:VRefAutoEna None\n''' + \ + ''' 03:VRefAutoTms None Secs\n''' + \ + ''' 03:RspTms 6 Secs\n''' + \ + ''' 03:ReadOnly 0\n''' + \ + '''03:01:V 9400 VNomPct\n''' + \ + '''03:01:Var 2000 DeptRef\n''' + \ + '''03:02:V 9570 VNomPct\n''' + \ + '''03:02:Var 0 DeptRef\n''' + \ + '''03:03:V 10500 VNomPct\n''' + \ + '''03:03:Var 0 DeptRef\n''' + \ + '''03:04:V 10800 VNomPct\n''' + \ + '''03:04:Var 2000 DeptRef\n''' + get_text_output = d.get_text() + # dont compare timestamps + assert get_text_output[get_text_output.index('Model'):] == get_text_expected_output diff --git a/sunspec2/tests/test_file_client.py b/sunspec2/tests/test_file_client.py new file mode 100644 index 0000000..9fa53ea --- /dev/null +++ b/sunspec2/tests/test_file_client.py @@ -0,0 +1,2932 @@ +import sunspec2.file.client as file_client +import sunspec2.mdef as mdef +import sunspec2.mb as mb +import sunspec2.device as device +import pytest + +@pytest.fixture +def model_705_data(): + return { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": 3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": 4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": 2000 + } + ] + } + ] + } + + +class TestFileClientPoint: + def test___init__(self): + p_def = { + "name": "Ena", + "type": "enum16", + "sf": 'test sf' + } + + p = file_client.FileClientPoint(p_def) + assert p.model is None + assert p.pdef == p_def + assert p.info == mb.point_type_info[mdef.TYPE_ENUM16] + assert p.len == 1 + assert p.offset == 0 + assert p.value is None + assert p.dirty is False + assert p.sf == 'test sf' + assert p.sf_required is True + assert p.sf_value is None + + def test__set_data(self): + p_def = { + "name": 'TestPoint', + "type": "uint16" + } + + # bytes + p = file_client.FileClientPoint(p_def) + p._set_data(b'\x00\x03') + assert p.value == 3 + assert not p.dirty + + # dict + data = {"TestPoint": 3} + p2 = file_client.FileClientPoint(p_def) + p2._set_data(data) + assert p2.value == 3 + + def test_value_getter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + } + p = file_client.FileClientPoint(p_def) + p.value = 4 + assert p.value == 4 + + def test_value_setter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + } + p = file_client.FileClientPoint(p_def) + p.value = 4 + assert p.value == 4 + + def test_cvalue_getter(self): + p_def = { + "name": "TestPoint", + "type": "uint16", + } + p = file_client.FileClientPoint(p_def) + p.sf_required = True + p.sf_value = 3 + p.value = 4 + assert p.cvalue == 4000.0 + + def test_cvalue_setter(self): + p_def = { + "name": "TestPoint", + "type": "uint16" + } + p = file_client.FileClientPoint(p_def) + p.sf_required = True + p.sf_value = 3 + p.cvalue = 3000 + assert p.value == 3 + + def test_get_value(self): + p_def = { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "name": "PF", + "type": "uint16" + } + p = file_client.FileClientPoint(p_def) + p.value = 3 + assert p.get_value() == 3 + + p2 = file_client.FileClientPoint(p_def) + assert p2.get_value() is None + + # pdef w/ sf + pdef_sf = { + "name": "TestPoint", + "type": "uint16", + "sf": "TestSF" + } + # sf point + sf_p = { + "name": "TestSF", + "value": 3, + "type": "sunssf" + } + + # computed + p_sf = file_client.FileClientPoint(sf_p) + p_sf.value = 3 + points = {} + points['TestSF'] = p_sf + m2 = file_client.FileClientModel() + setattr(m2, 'points', points) + + p9 = file_client.FileClientPoint(pdef_sf, model=m2) + g = file_client.FileClientGroup() + g.points = {} + p9.group = g + p9.value = 2020 + assert p9.get_value(computed=True) == 2020000.0 + + # computed exception + m3 = file_client.FileClientModel() + points2 = {} + setattr(m3, 'points', points2) + + p10 = file_client.FileClientPoint(pdef_sf, model=m3) + p10.value = 2020 + p10.group = g + with pytest.raises(device.ModelError) as exc: + p10.get_value(computed=True) + assert 'Scale factor TestSF for point TestPoint not found' in str(exc.value) + + def test_set_value(self): + p_def = { + "access": "RW", + "desc": "Power factor setpoint when injecting active power.", + "label": "Power Factor (W Inj) ", + "name": "PF", + "type": "uint16" + } + p = file_client.FileClientPoint(p_def) + p.set_value(3) + assert p.value == 3 + + # test computed + pdef_computed = { + "name": "TestingComputed", + "type": "uint16", + "sf": "TestSF" + } + p_SF = file_client.FileClientPoint() + p_SF.value = 2 + + points = {} + points['TestSF'] = p_SF + m = file_client.FileClientModel() + setattr(m, 'points', points) + + g = file_client.FileClientGroup() + g.points = {} + + p3 = file_client.FileClientPoint(pdef_computed, model=m, group=g) + p3.set_value(1000, computed=True, dirty=True) + assert p3.value == 10 + assert p3.dirty + + # test exceptions + p2_sf = file_client.FileClientPoint() + m2 = file_client.FileClientModel() + points2 = {} + points2['TestSF'] = p2_sf + setattr(m2, 'points', points2) + + p4 = file_client.FileClientPoint(pdef_computed, model=m2, group=g) + with pytest.raises(device.ModelError) as exc: + p4.set_value(1000, computed=True) + assert 'SF field TestSF value not initialized for point TestingComputed' in str(exc.value) + + del m2.points['TestSF'] + with pytest.raises(device.ModelError) as exc: + p4.set_value(1000, computed=True) + assert 'Scale factor TestSF for point TestingComputed not found' in str(exc.value) + + def test_get_mb(self): + p_def = { + "name": "ESVLo", + "type": "uint16", + } + p = file_client.FileClientPoint(p_def) + p.value = 3 + assert p.get_mb() == b'\x00\x03' + p.value = None + assert p.get_mb() == b'\xff\xff' + assert p.get_mb(computed=True) == b'\xff\xff' + + # computed + p.value = 3 + p.sf_required = True + p.sf_value = 4 + assert p.get_mb(computed=True) == b'\x75\x30' + + def test_set_mb(self): + p_def = { + "name": "ESVLo", + "type": "uint16", + } + p = file_client.FileClientPoint(p_def) + + p.set_mb(b'\x00\x03', dirty=True) + assert p.value == 3 + assert p.dirty is True + + # unimplemented + p.set_mb(b'\xff\xff') + assert p.value is None + assert p.sf_value is None + + p2 = file_client.FileClientPoint(p_def) + p2.len = 100 + assert p2.set_mb(b'\x00\x03') == 2 + assert p2.value is None + + m = file_client.FileClientModel() + p3 = file_client.FileClientPoint(p_def, m) + p3.set_mb(None) + assert p3.model.error_info == '''Error setting value for ESVLo: object of type 'NoneType' has no len()\n''' + + # exceptions + p_def2 = { + "name": "ESVLo", + "type": "uint16", + "sf": "TestSF" + } + p_sf = file_client.FileClientPoint() + points = {} + points['TestSF'] = p_sf + setattr(m, 'points', points) + + g = file_client.FileClientGroup() + g.points = {} + + m.error_info = '' + p4 = file_client.FileClientPoint(p_def2, model=m, group=g) + p4.set_mb(b'\x00\x03', computed=True) + assert p4.model.error_info == "Error setting value for ESVLo: SF field TestSF value not initialized for point ESVLo\n" + + m.error_info = '' + del m.points['TestSF'] + p5 = file_client.FileClientPoint(p_def2, model=m, group=g) + p5.set_mb(b'\x00\x04', computed=True) + assert p5.model.error_info == '''Error setting value for ESVLo: Scale factor TestSF for point ESVLo not found\n''' + + # test computed + pdef_computed = { + "name": "TestingComputed", + "type": "uint16", + "sf": "TestSF" + } + p_SF = file_client.FileClientPoint() + p_SF.value = 2 + + points = {} + points['TestSF'] = p_SF + m = file_client.FileClientModel() + setattr(m, 'points', points) + + p3 = file_client.FileClientPoint(pdef_computed, model=m, group=g) + p3.set_mb(b'\x0b\xb8', computed=True, dirty=True) + assert p3.value == 30 + assert p3.dirty + + def test_get_text(self, model_705_data): + m = file_client.FileClientModel(705, data=model_705_data) + p = m.NPt + expected_output = ' NPt 4\n' + assert p.get_text() == expected_output + + +class TestFileClientGroup: + def test___init__(self): + g_704 = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + }, + ], + "type": "sync" + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "name": "Ext", + "type": "enum16" + } + ], + "type": "sync" + }, + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 704 + }, + { + "name": "L", + "static": "S", + "type": "uint16" + }, + + { + "name": "PFWInjRvrtTms", + "type": "uint32", + }, + { + "name": "PFWInjRvrtRem", + "type": "uint32", + }, + { + "name": "PFWAbsEna", + "type": "enum16" + }, + { + "name": "PF_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 704 + } + g = file_client.FileClientGroup(g_704['group']) + + assert g.gdef == g_704['group'] + assert g.model is None + assert g.gname == 'DERCtlAC' + assert g.offset == 0 + assert g.len == 10 + assert len(g.points) == 6 + assert len(g.groups) == 2 + assert g.points_len == 8 + assert g.group_class == file_client.FileClientGroup + + def test___getattr__(self): + g_704 = { + "group": { + "groups": [ + { + "name": "PFWInj", + "points": [ + { + "name": "PF", + "sf": "PF_SF", + "type": "uint16" + }, + ], + "type": "sync" + }, + { + "name": "PFWInjRvrt", + "points": [ + { + "name": "Ext", + "type": "enum16" + } + ], + "type": "sync" + }, + ], + "name": "DERCtlAC", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 704 + }, + { + "name": "L", + "static": "S", + "type": "uint16" + }, + + { + "name": "PFWInjRvrtTms", + "type": "uint32", + }, + { + "name": "PFWInjRvrtRem", + "type": "uint32", + }, + { + "name": "PFWAbsEna", + "type": "enum16" + }, + { + "name": "PF_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 704 + } + g = file_client.FileClientGroup(g_704['group']) + with pytest.raises(AttributeError) as exc: + g.qwerty + assert "Group object has no attribute qwerty" in str(exc.value) + assert g.ID + assert g.PFWAbsEna + + def test__group_data(self): + gdef_705 = { + "group": { + "groups": [ + { + "count": "NCrv", + "groups": [ + { + "count": "NPt", + "name": "Pt", + "points": [ + { + "name": "V", + "sf": "V_SF", + "type": "uint16", + }, + { + "name": "Var", + "sf": "DeptRef_SF", + "type": "int16", + "units": "VarPct" + } + ], + "type": "group" + } + ], + "name": "Crv", + "points": [ + { + "name": "ActPt", + "type": "uint16" + }, + { + "name": "DeptRef", + "symbols": [ + { + "name": "W_MAX_PCT", + "value": 1 + }, + { + "name": "VAR_MAX_PCT", + "value": 2 + }, + { + "name": "VAR_AVAL_PCT", + "value": 3 + } + ], + "type": "enum16" + }, + { + "name": "Pri", + "symbols": [ + { + "name": "ACTIVE", + "value": 1 + }, + { + "name": "REACTIVE", + "value": 2 + }, + { + "name": "IEEE_1547", + "value": 3 + }, + { + "name": "PF", + "value": 4 + }, + { + "name": "VENDOR", + "value": 5 + } + ], + "type": "enum16" + }, + { + "name": "VRef", + "type": "uint16" + }, + { + "name": "VRefAuto", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "VRefTms", + "type": "uint16" + }, + { + "name": "RspTms", + "type": "uint16" + }, + { + "name": "ReadOnly", + "symbols": [ + { + "name": "RW", + "value": 0 + }, + { + "name": "R", + "value": 1 + } + ], + "type": "enum16" + } + ], + "type": "group" + } + ], + "name": "DERVoltVar", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 705 + }, + { + "name": "L", + "type": "uint16" + }, + { + "name": "Ena", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "CrvSt", + "symbols": [ + { + "name": "INACTIVE", + "value": 0 + }, + { + "name": "ACTIVE", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "AdptCrvReq", + "type": "uint16" + }, + { + "name": "AdptCrvRslt", + "symbols": [ + { + "name": "IN_PROGRESS", + "value": 0 + }, + { + "name": "COMPLETED", + "value": 1 + }, + { + "name": "FAILED", + "value": 2 + } + ], + "type": "enum16" + }, + { + "name": "NPt", + "type": "uint16" + }, + { + "name": "NCrv", + "type": "uint16" + }, + { + "name": "RvrtTms", + "type": "uint32" + }, + { + "name": "RvrtRem", + "type": "uint32" + }, + { + "name": "RvrtCrv", + "type": "uint16" + }, + { + "name": "V_SF", + "type": "sunssf" + }, + { + "name": "DeptRef_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 705 + } + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + g = file_client.FileClientGroup() + assert g._group_data(gdata_705, 'Crv') == [{'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, + {'V': 10200, 'Var': 0}, {'V': 10600, 'Var': -4000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, + {'V': 10500, 'Var': 0}, {'V': 10800, 'Var': -2000}]}] + + assert g._group_data(gdata_705['Crv'], index=0) == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, + 'VRefAuto': 0, 'VRefTms': 5, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} + + def test__get_data_group_count(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + g = file_client.FileClientGroup() + assert g._get_data_group_count(gdata_705['Crv']) == 3 + + def test__init_repeating_group(self): + gdef_705 = { + "group": { + "groups": [ + { + "count": "NCrv", + "groups": [ + { + "count": "NPt", + "name": "Pt", + "points": [ + { + "name": "V", + "sf": "V_SF", + "type": "uint16", + }, + { + "name": "Var", + "sf": "DeptRef_SF", + "type": "int16", + "units": "VarPct" + } + ], + "type": "group" + } + ], + "name": "Crv", + "points": [ + { + "name": "ActPt", + "type": "uint16" + }, + { + "name": "DeptRef", + "symbols": [ + { + "name": "W_MAX_PCT", + "value": 1 + }, + { + "name": "VAR_MAX_PCT", + "value": 2 + }, + { + "name": "VAR_AVAL_PCT", + "value": 3 + } + ], + "type": "enum16" + }, + { + "name": "Pri", + "symbols": [ + { + "name": "ACTIVE", + "value": 1 + }, + { + "name": "REACTIVE", + "value": 2 + }, + { + "name": "IEEE_1547", + "value": 3 + }, + { + "name": "PF", + "value": 4 + }, + { + "name": "VENDOR", + "value": 5 + } + ], + "type": "enum16" + }, + { + "name": "VRef", + "type": "uint16" + }, + { + "name": "VRefAuto", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "VRefTms", + "type": "uint16" + }, + { + "name": "RspTms", + "type": "uint16" + }, + { + "name": "ReadOnly", + "symbols": [ + { + "name": "RW", + "value": 0 + }, + { + "name": "R", + "value": 1 + } + ], + "type": "enum16" + } + ], + "type": "group" + } + ], + "name": "DERVoltVar", + "points": [ + { + "name": "ID", + "type": "uint16", + "value": 705 + }, + { + "name": "L", + "type": "uint16" + }, + { + "name": "Ena", + "symbols": [ + { + "name": "DISABLED", + "value": 0 + }, + { + "name": "ENABLED", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "CrvSt", + "symbols": [ + { + "name": "INACTIVE", + "value": 0 + }, + { + "name": "ACTIVE", + "value": 1 + } + ], + "type": "enum16" + }, + { + "name": "AdptCrvReq", + "type": "uint16" + }, + { + "name": "AdptCrvRslt", + "symbols": [ + { + "name": "IN_PROGRESS", + "value": 0 + }, + { + "name": "COMPLETED", + "value": 1 + }, + { + "name": "FAILED", + "value": 2 + } + ], + "type": "enum16" + }, + { + "name": "NPt", + "type": "uint16" + }, + { + "name": "NCrv", + "type": "uint16" + }, + { + "name": "RvrtTms", + "type": "uint32" + }, + { + "name": "RvrtRem", + "type": "uint32" + }, + { + "name": "RvrtCrv", + "type": "uint16" + }, + { + "name": "V_SF", + "type": "sunssf" + }, + { + "name": "DeptRef_SF", + "type": "sunssf" + } + ], + "type": "group" + }, + "id": 705 + } + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + g = file_client.FileClientGroup() + + with pytest.raises(device.ModelError) as exc: + g._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) + assert 'Count field NCrv undefined for group Crv' in str(exc.value) + + m = file_client.FileClientModel() + pdef_NPt = {"name": "NPt", "type": "uint16"} + p_NPt = file_client.FileClientPoint(pdef_NPt) + p_NPt.value = 4 + + pdef_NCrv = {"name": "NCrv", "type": "uint16"} + p_NCrv = file_client.FileClientPoint(pdef_NCrv) + points = {'NPt': p_NPt, 'NCrv': p_NCrv} + setattr(m, 'points', points) + + g2 = file_client.FileClientGroup(gdef_705['group']['groups'][0], m) + + with pytest.raises(device.ModelError) as exc: + g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) + assert 'Count field NCrv value not initialized for group Crv' in str(exc.value) + + # set value for NCrv count and reset the points attribute on model + p_NCrv.value = 3 + setattr(m, 'points', points) + groups = g2._init_repeating_group(gdef_705['group']['groups'][0], 0, gdata_705, 0) + assert len(groups) == 3 + assert len(groups[0].groups['Pt']) == 4 + + def test_get_dict(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m2 = file_client.FileClientModel(705, data=gdata_705) + assert m2.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefAutoEna': None, 'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} + + # test computed + m2.groups['Crv'][0].points['DeptRef'].sf_required = True + m2.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m2.groups['Crv'][0].points['Pri'].sf_required = True + m2.groups['Crv'][0].points['Pri'].sf_value = 3 + computed_dict = m2.groups['Crv'][0].get_dict(computed=True) + assert computed_dict['DeptRef'] == 1000.0 + assert computed_dict['Pri'] == 1000.0 + + def test_set_dict(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "RspTms_SF": 1, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + assert m.groups['Crv'][0].get_dict() == {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, + 'VRefAutoEna': None, 'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, + {'V': 10300, 'Var': 0}, {'V': 10700, 'Var': -3000}]} + + new_dict = {'ActPt': 4, 'DeptRef': 4000, 'Pri': 5000, 'VRef': 3, 'VRefAuto': 2, 'VRefAutoEna': None, + 'VRefAutoTms': None, 'RspTms': 2, 'ReadOnly': 2, + 'Pt': [{'V': 111, 'Var': 111}, {'V': 123, 'Var': 1112}, {'V': 111, 'Var': 111}, + {'V': 123, 'Var': -1112}]} + + m.groups['Crv'][0].set_dict(new_dict, dirty=True) + assert m.groups['Crv'][0].get_dict() == new_dict + assert m.groups['Crv'][0].VRef.value == 3 + assert m.groups['Crv'][0].VRef.dirty + assert m.groups['Crv'][0].Pri.dirty + + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + m.groups['Crv'][0].set_dict(new_dict, computed=True) + computed_dict = m.groups['Crv'][0].get_dict() + assert computed_dict['DeptRef'] == 4.0 + assert computed_dict['Pri'] == 5.0 + + def test_get_json(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \ + ''' "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 1,''' + \ + ''' "Pt": [{"V": 9200, "Var": 3000}, {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0},''' + \ + ''' {"V": 10700, "Var": -3000}]}''' + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + assert m.groups['Crv'][0].get_json(computed=True) == '''{"ActPt": 4, "DeptRef": 1000.0,''' + \ + ''' "Pri": 1000.0, "VRef": 0.01, "VRefAuto": 0.0, "VRefAutoEna": null,''' + \ + ''' "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 92.0, "Var": 30.0},''' + \ + ''' {"V": 96.7, "Var": 0.0}, {"V": 103.0, "Var": 0.0}, {"V": 107.0, "Var": -30.0}]}''' + + def test_set_json(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "RspTms_SF": 1, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + assert m.groups['Crv'][0].get_json() == '''{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1,''' + \ + ''' "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 1,''' + \ + ''' "Pt": [{"V": 9200, "Var": 3000}, {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0},''' + \ + ''' {"V": 10700, "Var": -3000}]}''' + + json_to_set = '''{"ActPt": 4, "DeptRef": 9999, "Pri": 9999, "VRef": 99, "VRefAuto": 88,''' + \ + ''' "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 88, "ReadOnly": 77,''' + \ + ''' "Pt": [{"V": 77, "Var": 66}, {"V": 55, "Var": 44}, {"V": 33, "Var": 22},''' + \ + ''' {"V": 111, "Var": -2222}]}''' + + m.groups['Crv'][0].set_json(json_to_set) + assert m.groups['Crv'][0].get_json() == json_to_set + assert m.groups['Crv'][0].DeptRef.value == 9999 + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + m.groups['Crv'][0].set_json(json_to_set, computed=True, dirty=True) + assert m.groups['Crv'][0].points['DeptRef'].value == 10 + assert m.groups['Crv'][0].points['DeptRef'].dirty + assert m.groups['Crv'][0].points['Pri'].value == 10 + assert m.groups['Crv'][0].points['Pri'].dirty + + def test_get_mb(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00' \ + b'\x00\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' + + # test computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + assert m.groups['Crv'][0].get_mb(computed=True) == b'\x00\x04\x03\xe8\x03\xe8\x00\x00\x00\x00\xff' \ + b'\xff\xff\xff\x00\x00\x00\x06\x00\x01\x00\\\x00' \ + b'\x1e\x00`\x00\x00\x00g\x00\x00\x00k\xff\xe2' + + def test_set_mb(self): + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": 2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + assert m.groups['Crv'][0].get_mb() == b'\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00' \ + b'\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H' + + bs = b'\x00\x04\x03\xe7\x03x\x03\t\x02\x9a\x02+\x01\xbc\x01M\x00\xde' \ + b'\x00o\x00\xde\x01M\x01\xbc\x02+\x02\x9a\xfc\xf7\xf4H\x0b\xb8' + + m.groups['Crv'][0].set_mb(bs, dirty=True) + assert m.groups['Crv'][0].get_mb() == bs + assert m.groups['Crv'][0].DeptRef.value == 999 + assert m.groups['Crv'][0].DeptRef.dirty + + # test computed + # set points DeptRef and Pri to 3000 w/ byte string + computed_bs = b'\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<' \ + b'\x00\x00)\xcc\xf4H' + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + m.groups['Crv'][0].set_mb(computed_bs, computed=True) + assert m.groups['Crv'][0].points['DeptRef'].value == 3 + assert m.groups['Crv'][0].points['Pri'].value == 3 + + def test_get_text(self, model_705_data): + m = file_client.FileClientModel(705, data=model_705_data) + g = m.groups['Crv'][0] + expected_output = ''' ActPt 4\n''' + \ + ''' DeptRef 1\n''' + \ + ''' Pri 1\n''' + \ + ''' VRef 1 VNomPct\n''' + \ + ''' VRefAuto 0 VNomPct\n''' + \ + ''' VRefAutoEna None\n''' + \ + ''' VRefAutoTms None Secs\n''' + \ + ''' RspTms 6 Secs\n''' + \ + ''' ReadOnly 1\n''' + \ + ''' 01:V 9200 VNomPct\n''' + \ + ''' 01:Var 3000 DeptRef\n''' + \ + ''' 02:V 9670 VNomPct\n''' + \ + ''' 02:Var 0 DeptRef\n''' + \ + ''' 03:V 10300 VNomPct\n''' + \ + ''' 03:Var 0 DeptRef\n''' + \ + ''' 04:V 10700 VNomPct\n''' + \ + ''' 04:Var 3000 DeptRef\n''' + assert g.get_text() == expected_output + + +class TestFileClientModel: + def test__init__(self): + m = file_client.FileClientModel(704) + assert m.model_id == 704 + assert m.model_addr == 0 + assert m.model_len == 0 + assert m.model_def['id'] == 704 + assert m.error_info == '' + assert m.gdef['name'] == 'DERCtlAC' + assert m.mid is None + assert m.device is None + + assert m.model == m + m2 = file_client.FileClientModel('abc') + assert m2.error_info == 'Invalid model id: abc\n' + + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + # test repeating group model + m2 = file_client.FileClientModel(705, data=gdata_705) + assert m2.model_id == 705 + assert m2.model_addr == 0 + assert m2.model_len == 0 + assert m2.model_def['id'] == 705 + assert m2.error_info == '' + assert m2.gdef['name'] == 'DERVoltVar' + assert m2.mid is None + assert m2.device is None + + def test__error(self): + m = file_client.FileClientModel(704) + m.add_error('test error') + assert m.error_info == 'test error\n' + + def test_get_text(self, model_705_data): + m = file_client.FileClientModel(705, data=model_705_data) + expected_output = ''' ID 705\n''' + \ + ''' L 67\n''' + \ + ''' Ena 1\n''' + \ + ''' AdptCrvReq 0\n''' + \ + ''' AdptCrvRslt 0\n''' + \ + ''' NPt 4\n''' + \ + ''' NCrv 3\n''' + \ + ''' RvrtTms 0 Secs\n''' + \ + ''' RvrtRem 0 Secs\n''' + \ + ''' RvrtCrv 0\n''' + \ + ''' V_SF -2\n''' + \ + ''' DeptRef_SF -2\n''' + \ + ''' RspTms_SF None\n''' + \ + ''' 01:ActPt 4\n''' + \ + ''' 01:DeptRef 1\n''' + \ + ''' 01:Pri 1\n''' + \ + ''' 01:VRef 1 VNomPct\n''' + \ + ''' 01:VRefAuto 0 VNomPct\n''' + \ + ''' 01:VRefAutoEna None\n''' + \ + ''' 01:VRefAutoTms None Secs\n''' + \ + ''' 01:RspTms 6 Secs\n''' + \ + ''' 01:ReadOnly 1\n''' + \ + '''01:01:V 9200 VNomPct\n''' + \ + '''01:01:Var 3000 DeptRef\n''' + \ + '''01:02:V 9670 VNomPct\n''' + \ + '''01:02:Var 0 DeptRef\n''' + \ + '''01:03:V 10300 VNomPct\n''' + \ + '''01:03:Var 0 DeptRef\n''' + \ + '''01:04:V 10700 VNomPct\n''' + \ + '''01:04:Var 3000 DeptRef\n''' + \ + ''' 02:ActPt 4\n''' + \ + ''' 02:DeptRef 1\n''' + \ + ''' 02:Pri 1\n''' + \ + ''' 02:VRef 1 VNomPct\n''' + \ + ''' 02:VRefAuto 0 VNomPct\n''' + \ + ''' 02:VRefAutoEna None\n''' + \ + ''' 02:VRefAutoTms None Secs\n''' + \ + ''' 02:RspTms 6 Secs\n''' + \ + ''' 02:ReadOnly 0\n''' + \ + '''02:01:V 9300 VNomPct\n''' + \ + '''02:01:Var 3000 DeptRef\n''' + \ + '''02:02:V 9570 VNomPct\n''' + \ + '''02:02:Var 0 DeptRef\n''' + \ + '''02:03:V 10200 VNomPct\n''' + \ + '''02:03:Var 0 DeptRef\n''' + \ + '''02:04:V 10600 VNomPct\n''' + \ + '''02:04:Var 4000 DeptRef\n''' + \ + ''' 03:ActPt 4\n''' + \ + ''' 03:DeptRef 1\n''' + \ + ''' 03:Pri 1\n''' + \ + ''' 03:VRef 1 VNomPct\n''' + \ + ''' 03:VRefAuto 0 VNomPct\n''' + \ + ''' 03:VRefAutoEna None\n''' + \ + ''' 03:VRefAutoTms None Secs\n''' + \ + ''' 03:RspTms 6 Secs\n''' + \ + ''' 03:ReadOnly 0\n''' + \ + '''03:01:V 9400 VNomPct\n''' + \ + '''03:01:Var 2000 DeptRef\n''' + \ + '''03:02:V 9570 VNomPct\n''' + \ + '''03:02:Var 0 DeptRef\n''' + \ + '''03:03:V 10500 VNomPct\n''' + \ + '''03:03:Var 0 DeptRef\n''' + \ + '''03:04:V 10800 VNomPct\n''' + \ + '''03:04:Var 2000 DeptRef\n''' + assert expected_output == m.get_text() + + +class TestFileClientDevice: + def test__init__(self): + d = file_client.FileClientDevice() + assert d.name is None + assert d.did + assert d.models == {} + assert d.model_list == [] + assert d.model_class == file_client.FileClientModel + + def test__get_attr__(self): + d = file_client.FileClientDevice() + m = file_client.FileClientModel() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + d.add_model(m) + assert d.mid_test + + with pytest.raises(AttributeError) as exc: + d.foo + assert "\'FileClientDevice\' object has no attribute \'foo\'" in str(exc.value) + + def test_add_model(self): + d = file_client.FileClientDevice() + m = file_client.FileClientModel() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + d.add_model(m) + assert d.models['mid_test'] + assert d.models['group_test'] + assert m.device == d + + def test_get_dict(self): + d = file_client.FileClientDevice() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + d.add_model(m) + assert d.get_dict()['models'] == [ + {'ID': 705, 'L': 67, 'Ena': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, 'RvrtTms': 0, + 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'RspTms_SF': None, 'Crv': [ + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefAutoTms': None, + 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 9200, 'Var': 3000}, {'V': 9670, 'Var': 0}, {'V': 10300, 'Var': 0}, + {'V': 10700, 'Var': -3000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefAutoTms': None, + 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9300, 'Var': 3000}, {'V': 9570, 'Var': 0}, {'V': 10200, 'Var': 0}, + {'V': 10600, 'Var': -4000}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 1, 'VRefAuto': 0, 'VRefAutoEna': None, 'VRefAutoTms': None, + 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 9400, 'Var': 2000}, {'V': 9570, 'Var': 0}, {'V': 10500, 'Var': 0}, + {'V': 10800, 'Var': -2000}]}]}] + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + assert d.get_dict(computed=True)['models'] == [ + {'ID': 705, 'L': 67, 'Ena': 1, 'AdptCrvReq': 0, 'AdptCrvRslt': 0, 'NPt': 4, 'NCrv': 3, 'RvrtTms': 0, + 'RvrtRem': 0, 'RvrtCrv': 0, 'V_SF': -2, 'DeptRef_SF': -2, 'RspTms_SF': None, 'Crv': [ + {'ActPt': 4, 'DeptRef': 1000.0, 'Pri': 1000.0, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None, + 'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 1, + 'Pt': [{'V': 92.0, 'Var': 30.0}, {'V': 96.7, 'Var': 0.0}, {'V': 103.0, 'Var': 0.0}, + {'V': 107.0, 'Var': -30.0}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None, + 'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 93.0, 'Var': 30.0}, {'V': 95.7, 'Var': 0.0}, {'V': 102.0, 'Var': 0.0}, + {'V': 106.0, 'Var': -40.0}]}, + {'ActPt': 4, 'DeptRef': 1, 'Pri': 1, 'VRef': 0.01, 'VRefAuto': 0.0, 'VRefAutoEna': None, + 'VRefAutoTms': None, 'RspTms': 6, 'ReadOnly': 0, + 'Pt': [{'V': 94.0, 'Var': 20.0}, {'V': 95.7, 'Var': 0.0}, {'V': 105.0, 'Var': 0.0}, + {'V': 108.0, 'Var': -20.0}]}]}] + + def test_get_json(self): + d = file_client.FileClientDevice() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + d.add_model(m) + assert d.get_json() == '''{"name": null, "did": "''' + str(d.did) + '''", "models": [{"ID": 705,''' + \ + ''' "L": 67, "Ena": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0, "NPt": 4, "NCrv": 3, "RvrtTms": 0,''' + \ + ''' "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2, "DeptRef_SF": -2, "RspTms_SF": null,''' + \ + ''' "Crv": [{"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null,''' + \ + ''' "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 1, "Pt": [{"V": 9200, "Var": 3000},''' + \ + ''' {"V": 9670, "Var": 0}, {"V": 10300, "Var": 0}, {"V": 10700, "Var": -3000}]}, {"ActPt": 4,''' + \ + ''' "DeptRef": 1, "Pri": 1, "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null,''' + \ + ''' "RspTms": 6, "ReadOnly": 0, "Pt": [{"V": 9300, "Var": 3000}, {"V": 9570, "Var": 0},''' + \ + ''' {"V": 10200, "Var": 0}, {"V": 10600, "Var": -4000}]}, {"ActPt": 4, "DeptRef": 1, "Pri": 1,''' + \ + ''' "VRef": 1, "VRefAuto": 0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6,''' + \ + ''' "ReadOnly": 0, "Pt": [{"V": 9400, "Var": 2000}, {"V": 9570, "Var": 0}, {"V": 10500,''' + \ + ''' "Var": 0}, {"V": 10800, "Var": -2000}]}]}]}''' + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + + get_json_output2 = '''{"name": null, "did": "''' + str(d.did) + '''", "models": [{"ID": 705, "L": 67,''' + \ + ''' "Ena": 1, "AdptCrvReq": 0, "AdptCrvRslt": 0, "NPt": 4, "NCrv": 3,''' + \ + ''' "RvrtTms": 0, "RvrtRem": 0, "RvrtCrv": 0, "V_SF": -2,''' + \ + ''' "DeptRef_SF": -2, "RspTms_SF": null, "Crv": [{"ActPt": 4,''' + \ + ''' "DeptRef": 1000.0, "Pri": 1000.0, "VRef": 0.01, "VRefAuto": 0.0,''' + \ + ''' "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 1,''' + \ + ''' "Pt": [{"V": 92.0, "Var": 30.0}, {"V": 96.7, "Var": 0.0}, {"V": 103.0,''' + \ + ''' "Var": 0.0}, {"V": 107.0, "Var": -30.0}]}, {"ActPt": 4, "DeptRef": 1,''' + \ + ''' "Pri": 1, "VRef": 0.01, "VRefAuto": 0.0, "VRefAutoEna": null,''' + \ + ''' "VRefAutoTms": null, "RspTms": 6, "ReadOnly": 0, "Pt": [{"V": 93.0,''' + \ + ''' "Var": 30.0}, {"V": 95.7, "Var": 0.0}, {"V": 102.0, "Var": 0.0}, {"V": 106.0,''' + \ + ''' "Var": -40.0}]}, {"ActPt": 4, "DeptRef": 1, "Pri": 1, "VRef": 0.01,''' + \ + ''' "VRefAuto": 0.0, "VRefAutoEna": null, "VRefAutoTms": null, "RspTms": 6,''' + \ + ''' "ReadOnly": 0, "Pt": [{"V": 94.0, "Var": 20.0}, {"V": 95.7, "Var": 0.0},''' + \ + ''' {"V": 105.0, "Var": 0.0}, {"V": 108.0, "Var": -20.0}]}]}]}''' + assert d.get_json(computed=True) == get_json_output2 + + def test_get_mb(self): + d = file_client.FileClientDevice() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + d.add_model(m) + assert d.get_mb() == b"\x02\xc1\x00C\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\xff\xfe\xff\xfe\x80\x00\x00\x04\x00\x01\x00\x01\x00\x01\x00" \ + b"\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00" \ + b"\x00)\xcc\xf4H\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00" \ + b"\x00\x00\x06\x00\x00$T\x0b\xb8%b\x00\x00'\xd8\x00\x00)h\xf0`\x00\x04\x00\x01" \ + b"\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x00$\xb8\x07\xd0%b" \ + b"\x00\x00)\x04\x00\x00*0\xf80" + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + assert d.get_mb(computed=True) == b'\x02\xc1\x00C\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\xff\xfe\xff\xfe\x80\x00\x00\x04\x03\xe8\x03' \ + b'\xe8\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x01\x00\\\x00' \ + b'\x1e\x00`\x00\x00\x00g\x00\x00\x00k\xff\xe2\x00\x04\x00\x01\x00\x01\x00' \ + b'\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x00\x00]\x00\x1e\x00_' \ + b'\x00\x00\x00f\x00\x00\x00j\xff\xd8\x00\x04\x00\x01\x00\x01\x00\x00\x00' \ + b'\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x00\x00^\x00\x14\x00_\x00\x00' \ + b'\x00i\x00\x00\x00l\xff\xec' + + def test_set_mb(self): + d = file_client.FileClientDevice() + gdata_705 = { + "ID": 705, + "Ena": 1, + "CrvSt": 1, + "AdptCrvReq": 0, + "AdptCrvRslt": 0, + "NPt": 4, + "NCrv": 3, + "RvrtTms": 0, + "RvrtRem": 0, + "RvrtCrv": 0, + "V_SF": -2, + "DeptRef_SF": -2, + "Crv": [ + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 1, + "Pt": [ + { + "V": 9200, + "Var": 3000 + }, + { + "V": 9670, + "Var": 0 + }, + { + "V": 10300, + "Var": 0 + }, + { + "V": 10700, + "Var": -3000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9300, + "Var": 3000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10200, + "Var": 0 + }, + { + "V": 10600, + "Var": -4000 + } + ] + }, + { + "ActPt": 4, + "DeptRef": 1, + "Pri": 1, + "VRef": 1, + "VRefAuto": 0, + "VRefTms": 5, + "RspTms": 6, + "ReadOnly": 0, + "Pt": [ + { + "V": 9400, + "Var": 2000 + }, + { + "V": 9570, + "Var": 0 + }, + { + "V": 10500, + "Var": 0 + }, + { + "V": 10800, + "Var": -2000 + } + ] + } + ] + } + m = file_client.FileClientModel(705, data=gdata_705) + d.add_model(m) + assert d.get_mb() == b"\x02\xc1\x00C\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\xff\xfe\xff\xfe\x80\x00\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff" \ + b"\xff\xff\xff\x00\x00\x00\x06\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H" \ + b"\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff\xff\xff\xff\x00\x00\x00\x06\x00\x00$T" \ + b"\x0b\xb8%b\x00\x00'\xd8\x00\x00)h\xf0`\x00\x04\x00\x01\x00\x01\x00\x01\x00\x00\xff" \ + b"\xff\xff\xff\x00\x00\x00\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" + + # DeptRef and Pri set to 3000 in byte string + bs = b"\x02\xc1\x00?\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x03\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\xff\xfe\xff\xfe\x00\x04\x0b\xb8\x0b\xb8\x00\x01\x00\x00\x00\x05\x00\x06" \ + b"\x00\x01#\xf0\x0b\xb8%\xc6\x00\x00(<\x00\x00)\xcc\xf4H\x00\x04\x00\x01\x00\x01\x00\x01" \ + b"\x00\x00\x00\x05\x00\x06\x00\x00$T\x0b\xb8%b\x00\x00'\xd8\x00\x00)h\xf0`\x00\x04\x00" \ + b"\x01\x00\x01\x00\x01\x00\x00\x00\x05\x00\x06\x00\x00$\xb8\x07\xd0%b\x00\x00)\x04\x00\x00*0\xf80" + + d.set_mb(bs, dirty=True) + assert m.groups['Crv'][0].DeptRef.value == 3000 + assert m.groups['Crv'][0].DeptRef.dirty + assert m.groups['Crv'][0].Pri.value == 3000 + assert m.groups['Crv'][0].Pri.dirty + + # computed + m.groups['Crv'][0].points['DeptRef'].sf_required = True + m.groups['Crv'][0].points['DeptRef'].sf_value = 3 + m.groups['Crv'][0].points['Pri'].sf_required = True + m.groups['Crv'][0].points['Pri'].sf_value = 3 + d.set_mb(bs, computed=True, dirty=False) + assert m.groups['Crv'][0].DeptRef.value == 3 + assert not m.groups['Crv'][0].DeptRef.dirty + assert m.groups['Crv'][0].Pri.value == 3 + assert not m.groups['Crv'][0].Pri.dirty + + def test_find_mid(self): + d = file_client.FileClientDevice() + m = file_client.FileClientModel() + setattr(m, 'model_id', 'mid_test') + setattr(m, 'gname', 'group_test') + setattr(m, 'mid', 'mid_test') + d.add_model(m) + assert d.find_mid('mid_test') == m + + def test_scan(self): + d = file_client.FileClientDevice('./sunspec2/tests/test_data/device_1547.json') + d.scan() + assert d.common + assert d.DERMeasureAC + + def test_repeating_point(self): + d = file_client.FileClientDevice('sunspec2/tests/test_data/inverter_123.json') + d.scan() + assert d.models[129][-1].curve[0].Tms1.value == 200 + assert d.models[129][-1].curve[1].Tms1.value == 0 + assert d.models[129][-1].curve[2].Tms11.value is None + + def test_get_text(self, model_705_data): + d = file_client.FileClientDevice() + m = file_client.FileClientModel(705, data=model_705_data) + d.add_model(m) + get_text_expected_output = '''Model: DERVoltVar (705)\n\n''' + \ + ''' ID 705\n''' + \ + ''' L 67\n''' + \ + ''' Ena 1\n''' + \ + ''' AdptCrvReq 0\n''' + \ + ''' AdptCrvRslt 0\n''' + \ + ''' NPt 4\n''' + \ + ''' NCrv 3\n''' + \ + ''' RvrtTms 0 Secs\n''' + \ + ''' RvrtRem 0 Secs\n''' + \ + ''' RvrtCrv 0\n''' + \ + ''' V_SF -2\n''' + \ + ''' DeptRef_SF -2\n''' + \ + ''' RspTms_SF None\n''' + \ + ''' 01:ActPt 4\n''' + \ + ''' 01:DeptRef 1\n''' + \ + ''' 01:Pri 1\n''' + \ + ''' 01:VRef 1 VNomPct\n''' + \ + ''' 01:VRefAuto 0 VNomPct\n''' + \ + ''' 01:VRefAutoEna None\n''' + \ + ''' 01:VRefAutoTms None Secs\n''' + \ + ''' 01:RspTms 6 Secs\n''' + \ + ''' 01:ReadOnly 1\n''' + \ + '''01:01:V 9200 VNomPct\n''' + \ + '''01:01:Var 3000 DeptRef\n''' + \ + '''01:02:V 9670 VNomPct\n''' + \ + '''01:02:Var 0 DeptRef\n''' + \ + '''01:03:V 10300 VNomPct\n''' + \ + '''01:03:Var 0 DeptRef\n''' + \ + '''01:04:V 10700 VNomPct\n''' + \ + '''01:04:Var 3000 DeptRef\n''' + \ + ''' 02:ActPt 4\n''' + \ + ''' 02:DeptRef 1\n''' + \ + ''' 02:Pri 1\n''' + \ + ''' 02:VRef 1 VNomPct\n''' + \ + ''' 02:VRefAuto 0 VNomPct\n''' + \ + ''' 02:VRefAutoEna None\n''' + \ + ''' 02:VRefAutoTms None Secs\n''' + \ + ''' 02:RspTms 6 Secs\n''' + \ + ''' 02:ReadOnly 0\n''' + \ + '''02:01:V 9300 VNomPct\n''' + \ + '''02:01:Var 3000 DeptRef\n''' + \ + '''02:02:V 9570 VNomPct\n''' + \ + '''02:02:Var 0 DeptRef\n''' + \ + '''02:03:V 10200 VNomPct\n''' + \ + '''02:03:Var 0 DeptRef\n''' + \ + '''02:04:V 10600 VNomPct\n''' + \ + '''02:04:Var 4000 DeptRef\n''' + \ + ''' 03:ActPt 4\n''' + \ + ''' 03:DeptRef 1\n''' + \ + ''' 03:Pri 1\n''' + \ + ''' 03:VRef 1 VNomPct\n''' + \ + ''' 03:VRefAuto 0 VNomPct\n''' + \ + ''' 03:VRefAutoEna None\n''' + \ + ''' 03:VRefAutoTms None Secs\n''' + \ + ''' 03:RspTms 6 Secs\n''' + \ + ''' 03:ReadOnly 0\n''' + \ + '''03:01:V 9400 VNomPct\n''' + \ + '''03:01:Var 2000 DeptRef\n''' + \ + '''03:02:V 9570 VNomPct\n''' + \ + '''03:02:Var 0 DeptRef\n''' + \ + '''03:03:V 10500 VNomPct\n''' + \ + '''03:03:Var 0 DeptRef\n''' + \ + '''03:04:V 10800 VNomPct\n''' + \ + '''03:04:Var 2000 DeptRef\n''' + get_text_output = d.get_text() + # dont compare timestamps + assert get_text_output[get_text_output.index('Model'):] == get_text_expected_output + + +class FileClient: + pass diff --git a/sunspec2/tests/test_mb.py b/sunspec2/tests/test_mb.py new file mode 100644 index 0000000..04fdc2e --- /dev/null +++ b/sunspec2/tests/test_mb.py @@ -0,0 +1,221 @@ +import sunspec2.mb as mb +import pytest + + +def test_create_unimpl_value(): + with pytest.raises(ValueError): + mb.create_unimpl_value(None) + + with pytest.raises(ValueError): + mb.create_unimpl_value('string') + + assert mb.create_unimpl_value('string', len=8) == b'\x00\x00\x00\x00\x00\x00\x00\x00' + assert mb.create_unimpl_value('ipv6addr') == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + assert mb.create_unimpl_value('int16') == b'\x80\x00' + assert mb.create_unimpl_value('uint16') == b'\xff\xff' + assert mb.create_unimpl_value('acc16') == b'\x00\x00' + assert mb.create_unimpl_value('enum16') == b'\xff\xff' + assert mb.create_unimpl_value('bitfield16') == b'\xff\xff' + assert mb.create_unimpl_value('int32') == b'\x80\x00\x00\x00' + assert mb.create_unimpl_value('uint32') == b'\xff\xff\xff\xff' + assert mb.create_unimpl_value('acc32') == b'\x00\x00\x00\x00' + assert mb.create_unimpl_value('enum32') == b'\xff\xff\xff\xff' + assert mb.create_unimpl_value('bitfield32') == b'\xff\xff\xff\xff' + assert mb.create_unimpl_value('ipaddr') == b'\x00\x00\x00\x00' + assert mb.create_unimpl_value('int64') == b'\x80\x00\x00\x00\x00\x00\x00\x00' + assert mb.create_unimpl_value('uint64') == b'\xff\xff\xff\xff\xff\xff\xff\xff' + assert mb.create_unimpl_value('acc64') == b'\x00\x00\x00\x00\x00\x00\x00\x00' + assert mb.create_unimpl_value('float32') == b'N\xff\x80\x00' + assert mb.create_unimpl_value('sunssf') == b'\x80\x00' + assert mb.create_unimpl_value('eui48') == b'\x00\x00\xff\xff\xff\xff\xff\xff' + assert mb.create_unimpl_value('pad') == b'\x00\x00' + + +def test_data_to_s16(): + assert mb.data_to_s16(b'\x13\x88') == 5000 + + +def test_data_to_u16(): + assert mb.data_to_u16(b'\x27\x10') == 10000 + + +def test_data_to_s32(): + assert mb.data_to_s32(b'\x12\x34\x56\x78') == 305419896 + assert mb.data_to_s32(b'\xED\xCB\xA9\x88') == -305419896 + + +def test_data_to_u32(): + assert mb.data_to_u32(b'\x12\x34\x56\x78') == 305419896 + + +def test_data_to_s64(): + assert mb.data_to_s64(b'\x12\x34\x56\x78\x12\x34\x56\x78') == 1311768465173141112 + assert mb.data_to_s64(b'\xED\xCB\xA9\x87\xED\xCB\xA9\x88') == -1311768465173141112 + + +def test_data_to_u64(): + assert mb.data_to_u64(b'\xff\xff\xff\xff\xff\xff\xff\xff') == 18446744073709551615 + + +def test_data_to_ipv6addr(): + assert mb.data_to_ipv6addr(b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34') == '20010DB8:85A30000:00008A2E:03707334' + + +def test_data_to_eui48(): + # need test to test for python 2 + assert mb.data_to_eui48(b'\x00\x00\x12\x34\x56\x78\x90\xAB') == '12:34:56:78:90:AB' + + +def test_data_to_f64(): + assert mb.data_to_f64(b'\x44\x9a\x43\xf3\x00\x00\x00\x00') == 3.1008742600725133e+22 + + +def test_data_to_str(): + assert mb.data_to_str(b'test') == 'test' + assert mb.data_to_str(b'444444') == '444444' + + +def test_s16_to_data(): + assert mb.s16_to_data(5000) == b'\x13\x88' + + +def test_u16_to_data(): + assert mb.u16_to_data(10000) == b'\x27\x10' + + +def test_s32_to_data(): + assert mb.s32_to_data(305419896) == b'\x12\x34\x56\x78' + assert mb.s32_to_data(-305419896) == b'\xED\xCB\xA9\x88' + + +def test_u32_to_data(): + assert mb.u32_to_data(305419896) == b'\x12\x34\x56\x78' + + +def test_s64_to_data(): + assert mb.s64_to_data(1311768465173141112) == b'\x12\x34\x56\x78\x12\x34\x56\x78' + assert mb.s64_to_data(-1311768465173141112) == b'\xED\xCB\xA9\x87\xED\xCB\xA9\x88' + + +def test_u64_to_data(): + assert mb.u64_to_data(18446744073709551615) == b'\xff\xff\xff\xff\xff\xff\xff\xff' + + +def test_ipv6addr_to_data(): + assert mb.ipv6addr_to_data('20010DB8:85A30000:00008A2E:03707334') == \ + b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34' + # need additional test to test for python 2 + + +def test_f32_to_data(): + assert mb.f32_to_data(32500.43359375) == b'F\xfd\xe8\xde' + + +def test_f64_to_data(): + assert mb.f64_to_data(3.1008742600725133e+22) == b'\x44\x9a\x43\xf3\x00\x00\x00\x00' + + +def test_str_to_data(): + assert mb.str_to_data('test') == b'test' + assert mb.str_to_data('444444') == b'444444' + assert mb.str_to_data('test', 5) == b'test\x00' + + +def test_eui48_to_data(): + assert mb.eui48_to_data('12:34:56:78:90:AB') == b'\x00\x00\x12\x34\x56\x78\x90\xAB' + + +def test_is_impl_int16(): + assert not mb.is_impl_int16(-32768) + assert mb.is_impl_int16(1111) + assert mb.is_impl_int16(None) + + +def test_is_impl_uint16(): + assert not mb.is_impl_uint16(0xffff) + assert mb.is_impl_uint16(0x1111) + + +def test_is_impl_acc16(): + assert not mb.is_impl_acc16(0) + assert mb.is_impl_acc16(1111) + + +def test_is_impl_enum16(): + assert not mb.is_impl_enum16(0xffff) + assert mb.is_impl_enum16(0x1111) + + +def test_is_impl_bitfield16(): + assert not mb.is_impl_bitfield16(0xffff) + assert mb.is_impl_bitfield16(0x1111) + + +def test_is_impl_int32(): + assert not mb.is_impl_int32(-2147483648) + assert mb.is_impl_int32(1111111) + + +def test_is_impl_uint32(): + assert not mb.is_impl_uint32(0xffffffff) + assert mb.is_impl_uint32(0x11111111) + + +def test_is_impl_acc32(): + assert not mb.is_impl_acc32(0) + assert mb.is_impl_acc32(1) + + +def test_is_impl_enum32(): + assert not mb.is_impl_enum32(0xffffffff) + assert mb.is_impl_enum32(0x11111111) + + +def test_is_impl_bitfield32(): + assert not mb.is_impl_bitfield32(0xffffffff) + assert mb.is_impl_bitfield32(0x11111111) + + +def test_is_impl_ipaddr(): + assert not mb.is_impl_ipaddr(0) + assert mb.is_impl_ipaddr('192.168.0.1') + + +def test_is_impl_int64(): + assert not mb.is_impl_int64(-9223372036854775808) + assert mb.is_impl_int64(111111111111111) + + +def test_is_impl_uint64(): + assert not mb.is_impl_uint64(0xffffffffffffffff) + assert mb.is_impl_uint64(0x1111111111111111) + + +def test_is_impl_acc64(): + assert not mb.is_impl_acc64(0) + assert mb.is_impl_acc64(1) + + +def test_is_impl_ipv6addr(): + assert not mb.is_impl_ipv6addr('\0') + assert mb.is_impl_ipv6addr(b'\x20\x01\x0d\xb8\x85\xa3\x00\x00\x00\x00\x8a\x2e\x03\x70\x73\x34') + + +def test_is_impl_float32(): + assert not mb.is_impl_float32(None) + assert mb.is_impl_float32(0x123456) + + +def test_is_impl_string(): + assert not mb.is_impl_string('\0') + assert mb.is_impl_string(b'\x74\x65\x73\x74') + + +def test_is_impl_sunssf(): + assert not mb.is_impl_sunssf(-32768) + assert mb.is_impl_sunssf(30000) + + +def test_is_impl_eui48(): + assert not mb.is_impl_eui48('FF:FF:FF:FF:FF:FF') + assert mb.is_impl_eui48('00:00:00:00:00:00') diff --git a/sunspec2/tests/test_mdef.py b/sunspec2/tests/test_mdef.py new file mode 100644 index 0000000..32247be --- /dev/null +++ b/sunspec2/tests/test_mdef.py @@ -0,0 +1,313 @@ +import sunspec2.mdef as mdef +import json +import copy +import pytest + + +def test_to_int(): + assert mdef.to_int('4') == 4 + assert isinstance(mdef.to_int('4'), int) + assert isinstance(mdef.to_int(4.0), int) + + +def test_to_str(): + assert mdef.to_str(4) == '4' + assert isinstance(mdef.to_str('4'), str) + + +def test_to_float(): + assert mdef.to_float('4') == 4.0 + assert isinstance(mdef.to_float('4'), float) + assert mdef.to_float('z') is None + + +def test_to_number_type(): + assert mdef.to_number_type('4') == 4 + assert mdef.to_number_type('4.0') == 4.0 + assert mdef.to_number_type('z') == 'z' + + +def test_validate_find_point(): + with open('./sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + assert mdef.validate_find_point(model_json['group'], 'ID') == model_json['group']['points'][0] + assert mdef.validate_find_point(model_json['group'], 'abc') is None + + +def test_validate_attrs(): + with open('./sunspec2/models/json/model_701.json') as f: + model_json = json.load(f) + + # model + assert mdef.validate_attrs(model_json, mdef.model_attr) == '' + + model_unexp_attr_err = copy.deepcopy(model_json) + model_unexp_attr_err['abc'] = 'def' + assert mdef.validate_attrs(model_unexp_attr_err, mdef.model_attr)[0:37] == 'Unexpected model definition attribute' + + model_unexp_type_err = copy.deepcopy(model_json) + model_unexp_type_err['id'] = '701' + assert mdef.validate_attrs(model_unexp_type_err, mdef.model_attr)[0:15] == 'Unexpected type' + + model_attr_missing = copy.deepcopy(model_json) + del model_attr_missing['id'] + assert mdef.validate_attrs(model_attr_missing, mdef.model_attr)[0:27] == 'Mandatory attribute missing' + + # group + assert mdef.validate_attrs(model_json['group'], mdef.group_attr) == '' + group_unexp_attr_err = copy.deepcopy(model_json)['group'] + group_unexp_attr_err['abc'] = 'def' + assert mdef.validate_attrs(group_unexp_attr_err, mdef.group_attr)[0:37] == 'Unexpected model definition attribute' + + group_unexp_type_err = copy.deepcopy(model_json)['group'] + group_unexp_type_err['name'] = 1 + assert mdef.validate_attrs(group_unexp_type_err, mdef.group_attr)[0:15] == 'Unexpected type' + + group_attr_missing = copy.deepcopy(model_json)['group'] + del group_attr_missing['name'] + assert mdef.validate_attrs(group_attr_missing, mdef.group_attr)[0:27] == 'Mandatory attribute missing' + + # point + assert mdef.validate_attrs(model_json['group']['points'][0], mdef.point_attr) == '' + + point_unexp_attr_err = copy.deepcopy(model_json)['group']['points'][0] + point_unexp_attr_err['abc'] = 'def' + assert mdef.validate_attrs(point_unexp_attr_err, mdef.point_attr)[0:37] == 'Unexpected model definition attribute' + + point_unexp_type_err = copy.deepcopy(model_json)['group']['points'][0] + point_unexp_type_err['name'] = 1 + assert mdef.validate_attrs(point_unexp_type_err, mdef.point_attr)[0:15] == 'Unexpected type' + + point_unexp_value_err = copy.deepcopy(model_json)['group']['points'][1] + point_unexp_value_err['access'] = 'z' + assert mdef.validate_attrs(point_unexp_value_err, mdef.point_attr)[0:16] == 'Unexpected value' + + point_attr_missing = copy.deepcopy(model_json)['group']['points'][0] + del point_attr_missing['name'] + assert mdef.validate_attrs(point_attr_missing, mdef.point_attr)[0:27] == 'Mandatory attribute missing' + + # symbol + assert mdef.validate_attrs(model_json['group']['points'][2]['symbols'][0], mdef.symbol_attr) == '' + + symbol_unexp_attr_err = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0] + symbol_unexp_attr_err['abc'] = 'def' + assert mdef.validate_attrs(symbol_unexp_attr_err, mdef.symbol_attr)[0:37] == 'Unexpected model definition attribute' + + symbol_unexp_type_err = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0] + symbol_unexp_type_err['name'] = 1 + assert mdef.validate_attrs(symbol_unexp_type_err, mdef.symbol_attr)[0:15] == 'Unexpected type' + + symbol_attr_missing = copy.deepcopy(model_json)['group']['points'][2]['symbols'][0] + del symbol_attr_missing['name'] + assert mdef.validate_attrs(symbol_attr_missing, mdef.symbol_attr)[0:27] == 'Mandatory attribute missing' + + +def test_validate_group_point_dup(): + with open('./sunspec2/models/json/model_704.json') as f: + model_json = json.load(f) + + assert mdef.validate_group_point_dup(model_json['group']) == '' + + dup_group_id_model = copy.deepcopy(model_json) + dup_group_id_group = dup_group_id_model['group'] + dup_group_id_group['groups'][0]['name'] = 'PFWInjRvrt' + assert mdef.validate_group_point_dup(dup_group_id_group)[0:18] == 'Duplicate group id' + + dup_group_point_id_model = copy.deepcopy(model_json) + dup_group_point_id_group = dup_group_point_id_model['group'] + dup_group_point_id_group['groups'][0]['name'] = 'PFWInjEna' + assert mdef.validate_group_point_dup(dup_group_point_id_group)[0:28] == 'Duplicate group and point id' + + mand_attr_miss_model = copy.deepcopy(model_json) + mand_attr_miss_group = mand_attr_miss_model['group'] + del mand_attr_miss_group['groups'][0]['name'] + assert mdef.validate_group_point_dup(mand_attr_miss_group)[0:32] == 'Mandatory name attribute missing' + + dup_point_id_model = copy.deepcopy(model_json) + dup_point_id_group = dup_point_id_model['group'] + dup_point_id_group['points'][1]['name'] = 'ID' + assert mdef.validate_group_point_dup(dup_point_id_group)[0:30] == 'Duplicate point id ID in group' + + mand_attr_miss_point_model = copy.deepcopy(model_json) + mand_attr_miss_point_group = mand_attr_miss_point_model['group'] + del mand_attr_miss_point_group['points'][1]['name'] + assert mdef.validate_group_point_dup(mand_attr_miss_point_group)[0:55] == 'Mandatory attribute missing in point ' \ + 'definition element' + + +def test_validate_symbols(): + symbols = [ + {'name': 'CAT_A', 'value': 1}, + {'name': 'CAT_B', 'value': 2} + ] + assert mdef.validate_symbols(symbols, mdef.symbol_attr) == '' + + +def test_validate_sf(): + with open('./sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + model_point = model_json['group']['points'][2] + model_group = model_json['group'] + model_group_arr = [model_group, model_group] + assert mdef.validate_sf(model_point, 'W_SF', model_group_arr) == '' + + not_sf_type_model = copy.deepcopy(model_json) + not_sf_type_point = not_sf_type_model['group']['points'][2] + not_sf_type_group = not_sf_type_model['group'] + not_sf_type_group_arr = [not_sf_type_group, not_sf_type_group] + for point in not_sf_type_model['group']['points']: + if point['name'] == 'W_SF': + point['type'] = 'abc' + assert mdef.validate_sf(not_sf_type_point, 'W_SF', not_sf_type_group_arr)[0:60] == 'Scale factor W_SF for point ' \ + 'WMaxRtg is not scale factor ' \ + 'type' + + sf_not_found_model = copy.deepcopy(model_json) + sf_not_found_point = sf_not_found_model['group']['points'][2] + sf_not_found_group = sf_not_found_model['group'] + sf_not_found_group_arr = [sf_not_found_group, sf_not_found_group] + assert mdef.validate_sf(sf_not_found_point, 'ABC', sf_not_found_group_arr)[0:44] == 'Scale factor ABC for point ' \ + 'WMaxRtg not found' + + sf_out_range_model = copy.deepcopy(model_json) + sf_out_range_point = sf_out_range_model['group']['points'][2] + sf_out_range_group = sf_out_range_model['group'] + sf_out_range_group_arr = [sf_out_range_group, sf_out_range_group] + assert mdef.validate_sf(sf_out_range_point, 11, sf_out_range_group_arr)[0:46] == 'Scale factor 11 for point ' \ + 'WMaxRtg out of range' + + sf_invalid_type_model = copy.deepcopy(model_json) + sf_invalid_type_point = sf_invalid_type_model['group']['points'][2] + sf_invalid_type_group = sf_invalid_type_model['group'] + sf_invalid_type_group_arr = [sf_invalid_type_group, sf_invalid_type_group] + assert mdef.validate_sf(sf_invalid_type_point, 4.0, sf_invalid_type_group_arr)[0:51] == 'Scale factor 4.0 for' \ + ' point WMaxRtg has ' \ + 'invalid type' + + +def test_validate_point_def(): + with open('./sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + model_group = model_json['group'] + group = model_json['group'] + point = model_json['group']['points'][0] + assert mdef.validate_point_def(point, model_group, group) == '' + + unk_point_type_model = copy.deepcopy(model_json) + unk_point_type_model_group = unk_point_type_model['group'] + unk_point_type_group = unk_point_type_model['group'] + unk_point_type_point = unk_point_type_model['group']['points'][0] + unk_point_type_point['type'] = 'abc' + assert mdef.validate_point_def(unk_point_type_point, unk_point_type_model_group, + unk_point_type_group)[0:35] == 'Unknown point type abc for point ID' + + dup_symbol_model = copy.deepcopy(model_json) + dup_symbol_model_group = dup_symbol_model['group'] + dup_symbol_group = dup_symbol_model['group'] + dup_symbol_point = dup_symbol_model['group']['points'][21] + dup_symbol_point['symbols'][0]['name'] = 'CAT_B' + assert mdef.validate_point_def(dup_symbol_point, dup_symbol_model_group, + dup_symbol_group)[0:19] == 'Duplicate symbol id' + + mand_attr_missing = copy.deepcopy(model_json) + mand_attr_missing_model_group = mand_attr_missing['group'] + mand_attr_missing_group = mand_attr_missing['group'] + mand_attr_missing_point = mand_attr_missing['group']['points'][0] + del mand_attr_missing_point['name'] + assert mdef.validate_point_def(mand_attr_missing_point, mand_attr_missing_model_group, + mand_attr_missing_group)[0:27] == 'Mandatory attribute missing' + + +def test_validate_group_def(): + with open('./sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + assert mdef.validate_group_def(model_json['group'], model_json['group']) == '' + + +def test_validate_model_group_def(): + with open('./sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + assert mdef.validate_model_group_def(model_json, model_json['group']) == '' + + missing_id_model = copy.deepcopy(model_json) + missing_id_group = missing_id_model['group'] + missing_id_group['points'][0]['name'] = 'abc' + assert mdef.validate_model_group_def(missing_id_model, missing_id_group)[0:41] == 'First point in top-level' \ + ' group must be ID' + + wrong_model_id_model = copy.deepcopy(model_json) + wrong_model_id_group = wrong_model_id_model['group'] + wrong_model_id_group['points'][0]['value'] = 0 + assert mdef.validate_model_group_def(wrong_model_id_model, wrong_model_id_group)[0:42] == 'Model ID does not ' \ + 'match top-level group ID' + + missing_len_model = copy.deepcopy(model_json) + missing_len_group = missing_len_model['group'] + missing_len_group['points'][1]['name'] = 'abc' + assert mdef.validate_model_group_def(missing_len_model, missing_len_group)[0:41] == 'Second point in top-level ' \ + 'group must be L' + + missing_two_p_model = copy.deepcopy(model_json) + missing_two_p_group = missing_two_p_model['group'] + missing_two_p_point = missing_two_p_group['points'][0] + del missing_two_p_group['points'] + missing_two_p_group['points'] = [missing_two_p_point] + assert mdef.validate_model_group_def(missing_two_p_model, missing_two_p_group)[0:48] == 'Top-level group must' \ + ' contain at least two ' \ + 'points' + + missing_p_def_model = copy.deepcopy(model_json) + missing_p_def_group = missing_p_def_model['group'] + del missing_p_def_group['points'] + assert mdef.validate_model_group_def(missing_p_def_model, missing_p_def_group)[0:41] == 'Top-level group' \ + ' missing point definitions' + + +def test_validate_model_def(): + with open('./sunspec2/models/json/model_702.json') as f: + model_json = json.load(f) + + assert mdef.validate_model_def(model_json) == '' + + +def test_from_json_str(): + with open('./sunspec2/models/json/model_63001.json') as f: + model_json = json.load(f) + model_json_str = json.dumps(model_json) + assert isinstance(mdef.from_json_str(model_json_str), dict) + + +def test_from_json_file(): + assert isinstance(mdef.from_json_file('./sunspec2/models/json/model_63001.json'), dict) + + +def test_to_json_str(): + with open('./sunspec2/models/json/model_63001.json') as f: + model_json = json.load(f) + assert isinstance(mdef.to_json_str(model_json), str) + + +def test_to_json_filename(): + assert mdef.to_json_filename('63001') == 'model_63001.json' + + +def test_to_json_file(tmp_path): + with open('./sunspec2/models/json/model_63001.json') as f: + model_json = json.load(f) + mdef.to_json_file(model_json, filedir=tmp_path) + + with open(tmp_path / 'model_63001.json') as f: + model_json = json.load(f) + assert isinstance(model_json, dict) + + +def test_model_filename_to_id(): + assert mdef.model_filename_to_id('model_00077.json') == 77 + with pytest.raises(Exception) as exc: + mdef.model_filename_to_id('model_abc.json') + assert 'Error extracting model id from filename' in str(exc.value) diff --git a/sunspec2/tests/test_modbus_client.py b/sunspec2/tests/test_modbus_client.py new file mode 100644 index 0000000..c0fdf4c --- /dev/null +++ b/sunspec2/tests/test_modbus_client.py @@ -0,0 +1,1503 @@ +import sunspec2.modbus.client as client +import pytest +import socket +import sunspec2.tests.mock_socket as MockSocket +import serial +import sunspec2.tests.mock_port as MockPort +import sunspec2.file.client as file_client +import sunspec2.modbus.modbus as suns_modbus +import struct + +class TestSunSpecModbusClientPoint: + def test_read(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + # tcp + d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + d_tcp.connect() + d_tcp.client.socket._set_buffer(tcp_buffer) + d_tcp.scan() + + assert d_tcp.common[0].SN.value == 'sn-123456789' + assert not d_tcp.common[0].SN.dirty + + d_tcp.common[0].SN.value = 'will be overwritten by read' + assert d_tcp.common[0].SN.value == 'will be overwritten by read' + assert d_tcp.common[0].SN.dirty + d_tcp.client.socket.clear_buffer() + tcp_p_buffer = [b'\x00\x00\x00\x00\x00#\x01\x03 ', + b'sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'] + d_tcp.client.socket._set_buffer(tcp_p_buffer) + d_tcp.common[0].SN.read() + assert d_tcp.common[0].SN.value == 'sn-123456789' + assert not d_tcp.common[0].SN.dirty + + # rtu + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + assert d_rtu.common[0].SN.value == 'sn-123456789' + assert not d_rtu.common[0].SN.dirty + + d_rtu.common[0].SN.value = 'will be overwritten by read' + assert d_rtu.common[0].SN.value == 'will be overwritten by read' + assert d_rtu.common[0].SN.dirty + + d_rtu.client.serial.clear_buffer() + tcp_p_buffer = [b'\x01\x03 sn', + b'-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd5\xb8'] + d_rtu.client.serial._set_buffer(tcp_p_buffer) + d_rtu.common[0].SN.read() + assert d_rtu.common[0].SN.value == 'sn-123456789' + assert not d_rtu.common[0].SN.dirty + + def test_write(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + # tcp + d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) + # simulate a sequence of exchanges with the device + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', # Readback first 6 registers + b'SunS\x00\x01', # SunSpec ID + common model header (ID = 1) + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', # Read back L register to get common model length + b'\x00B', # common model length = 0x42 = 66 = 'B' + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', # Readback 0x88 bytes in common model (0x44 = 68 regs) + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', # Readback/response to query next model + b'\x00~', # 126 = '~' = 0x7e + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', # readback next model len + b'\x00@', # 64 = '@' = 0x40 + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', # readback next model data (0x84 bytes = 66 regs) + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', # readback next model + b'\xff\xff'] # terminator + d_tcp.client.connect() + d_tcp.client.socket._set_buffer(tcp_buffer) + d_tcp.scan() + + assert d_tcp.common[0].SN.value == 'sn-123456789' + assert not d_tcp.common[0].SN.dirty + + d_tcp.common[0].SN.value = 'sn-000' + assert d_tcp.common[0].SN.value == 'sn-000' + assert d_tcp.common[0].SN.dirty + + tcp_write_buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', + b't\x00\x10'] + d_tcp.client.socket.clear_buffer() + d_tcp.client.socket._set_buffer(tcp_write_buffer) + d_tcp.common[0].write() + + d_tcp.common[0].SN.value = 'will be overwritten by read' + assert d_tcp.common[0].SN.value == 'will be overwritten by read' + assert d_tcp.common[0].SN.dirty + + tcp_read_buffer = [b'\x00\x00\x00\x00\x00#\x01\x03 ', # Read back data to verify write + b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'] + d_tcp.client.socket.clear_buffer() + d_tcp.client.socket._set_buffer(tcp_read_buffer) + d_tcp.common[0].SN.read() + assert d_tcp.common[0].SN.value == 'sn-000' + assert not d_tcp.common[0].SN.dirty + + # rtu + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + rtu_buffer = [ # simulate a sequence of responses from the device scan + b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', # Response: SunSpec ID + common model header (ID = 1) + CRC + b'\x01\x03\x02\x00B', + b'8u', # Response: common model length = 0x42 = 66 = 'B' + CRC + b'\x01\x03\x88\x00\x01', # Readback registers in common model (0x44 = 68 regs) + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00TestDevice-2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x01\x00\x00\xb9\xfb', # Common model data + b'\x01\x03\x02\xff\xff', # Terminator data + b'\xb9\xf4' # CRC + ] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + assert d_rtu.common[0].SN.value == 'sn-123456789' + assert not d_rtu.common[0].SN.dirty + + assert d_rtu.common[0].DA.value == 1 + d_rtu.common[0].DA.value = 2 + assert d_rtu.common[0].DA.value == 2 + assert d_rtu.common[0].DA.dirty + + # 0x01: This is the device address, 1. + # 0x06: This is the function code for "Write Single Register"/0x10 (16) = "Write Multiple Registers". + # 0x9c84: This is the register address, 0x9c84 is 40068. (DA) + # 0x0002: This is the data value to be written to the register, 2. + # Cyclic Redundancy Check (CRC) - struct.pack('>H', suns_modbus.computeCRC(b'\x01\x06\x9c\x84\x00\x02')) + rtu_read_buffer = [ + b'\x01\x06\x9c\x84\x00\x02', + suns_modbus.computeCRC(b'\x01\x06\x9c\x84\x00\x02').to_bytes(2, 'big') + ] + d_rtu.client.serial.clear_buffer() + d_rtu.client.serial._set_buffer(rtu_read_buffer) + d_rtu.common[0].write() + + # 0x01: Slave Address (1). + # 0x03: Function Code (Read Holding Registers). T + # 0x02: Byte Count (2). This tells you that the response contains 2 bytes of data. + # 0x0002: Data (2 in decimal). This is the actual data read from the holding registers. DA = 2 + rtu_read_buffer = [ + b'\x01\x03\x02\x00\x02', # Read back data to verify write + struct.pack('>H', suns_modbus.computeCRC(b'\x01\x03\x02\x00\x02')) + ] + d_rtu.client.serial.clear_buffer() + d_rtu.client.serial._set_buffer(rtu_read_buffer) + d_rtu.common[0].DA.read() + assert d_rtu.common[0].DA.value == 2 + assert not d_rtu.common[0].SN.dirty + + def test_get_text(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + # tcp + d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + d_tcp.connect() + d_tcp.client.socket._set_buffer(tcp_buffer) + d_tcp.scan() + expected_output = ' SN sn-123456789\n' + assert d_tcp.common[0].SN.get_text() == expected_output + + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + assert d_rtu.common[0].SN.get_text() == expected_output + + +class TestSunSpecModbusClientGroup: + def test_read(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + # tcp + d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + d_tcp.client.connect() + d_tcp.client.socket._set_buffer(tcp_buffer) + d_tcp.scan() + assert d_tcp.common[0].SN.value == "sn-123456789" + assert d_tcp.common[0].Vr.value == "1.2.3" + assert not d_tcp.common[0].SN.dirty + assert not d_tcp.common[0].Vr.dirty + + d_tcp.common[0].SN.value = 'this will overwrite from read' + d_tcp.common[0].Vr.value = 'this will overwrite from read' + assert d_tcp.common[0].SN.value == 'this will overwrite from read' + assert d_tcp.common[0].Vr.value == 'this will overwrite from read' + assert d_tcp.common[0].SN.dirty + assert d_tcp.common[0].Vr.dirty + + tcp_read_buffer = [b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'] + d_tcp.client.socket.clear_buffer() + d_tcp.client.socket._set_buffer(tcp_read_buffer) + d_tcp.common[0].read() + + assert d_tcp.common[0].SN.value == "sn-123456789" + assert d_tcp.common[0].Vr.value == "1.2.3" + assert not d_tcp.common[0].SN.dirty + assert not d_tcp.common[0].Vr.dirty + + # rtu + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + assert d_rtu.common[0].SN.value == "sn-123456789" + assert d_rtu.common[0].Vr.value == "1.2.3" + assert not d_rtu.common[0].SN.dirty + assert not d_rtu.common[0].Vr.dirty + + d_rtu.common[0].SN.value = 'this will overwrite from read' + d_rtu.common[0].Vr.value = 'this will overwrite from read' + assert d_rtu.common[0].SN.value == 'this will overwrite from read' + assert d_rtu.common[0].Vr.value == 'this will overwrite from read' + assert d_rtu.common[0].SN.dirty + assert d_rtu.common[0].Vr.dirty + + rtu_read_buffer = [b'\x01\x03\x84\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00H\xef'] + d_rtu.client.serial.clear_buffer() + d_rtu.client.serial._set_buffer(rtu_read_buffer) + d_rtu.common[0].read() + assert d_rtu.common[0].SN.value == "sn-123456789" + assert d_rtu.common[0].Vr.value == "1.2.3" + assert not d_rtu.common[0].SN.dirty + assert not d_rtu.common[0].Vr.dirty + + def test_write(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + # tcp + d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + d_tcp.client.connect() + d_tcp.client.socket._set_buffer(tcp_buffer) + d_tcp.scan() + assert d_tcp.common[0].SN.value == "sn-123456789" + assert d_tcp.common[0].Vr.value == "1.2.3" + assert not d_tcp.common[0].SN.dirty + assert not d_tcp.common[0].Vr.dirty + + d_tcp.common[0].SN.value = 'sn-000' + d_tcp.common[0].Vr.value = 'v0.0.0' + assert d_tcp.common[0].SN.value == "sn-000" + assert d_tcp.common[0].Vr.value == "v0.0.0" + assert d_tcp.common[0].SN.dirty + assert d_tcp.common[0].Vr.dirty + + tcp_write_buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', + b'l\x00\x18'] + d_tcp.client.socket.clear_buffer() + d_tcp.client.socket._set_buffer(tcp_write_buffer) + d_tcp.common[0].write() + + d_tcp.common[0].SN.value = 'this will overwrite from read' + d_tcp.common[0].Vr.value = 'this will overwrite from read' + assert d_tcp.common[0].SN.value == 'this will overwrite from read' + assert d_tcp.common[0].Vr.value == 'this will overwrite from read' + assert d_tcp.common[0].SN.dirty + assert d_tcp.common[0].Vr.dirty + + tcp_read_buffer = [b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c' + b'\x00\x00\x00\x00\x00\x00\x00v0.0.0\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00sn-000\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'] + d_tcp.client.socket.clear_buffer() + d_tcp.client.socket._set_buffer(tcp_read_buffer) + d_tcp.common[0].read() + + assert d_tcp.common[0].SN.value == "sn-000" + assert d_tcp.common[0].Vr.value == "v0.0.0" + assert not d_tcp.common[0].SN.dirty + assert not d_tcp.common[0].Vr.dirty + + # rtu + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + rtu_buffer = [ + b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00TestDevice-2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-123456789' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00I\xfb', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4' + ] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + assert d_rtu.common[0].SN.value == "sn-123456789" + assert d_rtu.common[0].Vr.value == "1.2.3" + assert not d_rtu.common[0].SN.dirty + assert not d_rtu.common[0].Vr.dirty + + d_rtu.common[0].DA.value = 2 + assert d_rtu.common[0].DA.value == 2 + assert d_rtu.common[0].DA.dirty + + rtu_read_buffer = [ + b'\x01\x06\x9c\x84\x00\x02', + struct.pack('>H', suns_modbus.computeCRC(b'\x01\x06\x9c\x84\x00\x02')) + ] + d_rtu.client.serial.clear_buffer() + d_rtu.client.serial._set_buffer(rtu_read_buffer) + d_rtu.common[0].write() + + d_rtu.common[0].SN.value = 'this will overwrite from read' + d_rtu.common[0].Vr.value = 'this will overwrite from read' + assert d_rtu.common[0].SN.value == 'this will overwrite from read' + assert d_rtu.common[0].Vr.value == 'this will overwrite from read' + assert d_rtu.common[0].SN.dirty + assert d_rtu.common[0].Vr.dirty + + rtu_read_buffer = [b'\x01\x03\x84\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00' + b'\x00\x00\x00\x00\x00\x00v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4h'] + d_rtu.client.serial.clear_buffer() + d_rtu.client.serial._set_buffer(rtu_read_buffer) + d_rtu.common[0].read() + assert d_rtu.common[0].SN.value == "sn-000" + assert d_rtu.common[0].Vr.value == "v0.0.0" + assert not d_rtu.common[0].SN.dirty + assert not d_rtu.common[0].Vr.dirty + + def test_write_points(self): + pass + + def test_get_text(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + # tcp + d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + d_tcp.client.connect() + d_tcp.client.socket._set_buffer(tcp_buffer) + d_tcp.scan() + expected_output = ''' ID 1\n L ''' + \ + ''' 66\n Mn ''' + \ + ''' SunSpecTest\n Md ''' + \ + ''' TestDevice-1\n Opt opt_a_b_''' + \ + '''c\n Vr 1.2.3\n SN ''' + \ + ''' sn-123456789\n DA ''' + \ + ''' 1\n Pad ''' + \ + ''' 0\n''' + assert d_tcp.common[0].get_text() == expected_output + + # rtu + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + assert d_rtu.common[0].get_text() == expected_output + + +class TestSunSpecModbusClientModel: + def test___init__(self, monkeypatch): + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + rtu_buffer = [ + b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TestDevice-2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\xb9\xfb', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4' + ] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + client_model = d_rtu.models['common'][0] + assert client_model.model_id == 1 + assert client_model.model_addr == 40002 + assert client_model.model_len == 66 + assert client_model.model_def['id'] == 1 + assert client_model.error_info == '' + assert client_model.gdef['name'] == 'common' + assert client_model.mid is not None + assert client_model.__class__.__name__ == 'SunSpecModbusClientModel' + + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + c_tcp = client.SunSpecModbusClientDeviceTCP() + tcp_req_check = [b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x00\x00\x03', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x03\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x02\x00B', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00F\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00G\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00F\x00@', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x88\x00\x01'] + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + c_tcp.client.connect() + c_tcp.client.socket._set_buffer(tcp_buffer) + c_tcp.scan() + c_tcp_model = c_tcp.models['common'][0] + assert c_tcp_model.model_id == 1 + assert c_tcp_model.model_addr == 40002 + assert c_tcp_model.model_len == 66 + assert c_tcp_model.model_def['id'] == 1 + assert c_tcp_model.error_info == '' + assert c_tcp_model.gdef['name'] == 'common' + assert c_tcp_model.mid is not None + assert c_tcp_model.__class__.__name__ == 'SunSpecModbusClientModel' + + def test_error(self, monkeypatch): + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + rtu_buffer = [ + b'\x01\x83\x02\xc0\xf1', + b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + client_model = d_rtu.models['common'][0] + client_model.add_error('test error') + assert client_model.error_info == 'test error\n' + + def test_get_text(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + # tcp + d_tcp = client.SunSpecModbusClientDeviceTCP(slave_id=1, ipaddr='127.0.0.1', ipport=8502) + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + d_tcp.client.connect() + d_tcp.client.socket._set_buffer(tcp_buffer) + d_tcp.scan() + expected_output = ''' ID 1\n L ''' + \ + ''' 66\n Mn ''' + \ + ''' SunSpecTest\n Md ''' + \ + ''' TestDevice-1\n Opt opt_a_b_''' + \ + '''c\n Vr 1.2.3\n SN ''' + \ + ''' sn-123456789\n DA ''' + \ + ''' 1\n Pad ''' + \ + ''' 0\n''' + assert d_tcp.common[0].get_text() == expected_output + + # rtu + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan() + assert d_rtu.common[0].get_text() == expected_output + + def test_read(self, monkeypatch): + d_rtu = client.SunSpecModbusClientDeviceRTU(slave_id=1, name="COM2") + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + + rtu_buffer = [b'\x01\x83\x02\xc0\xf1', + b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00TestDevice-2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x01\x00\x00\xb9\xfb'] + d_rtu.open() + d_rtu.client.serial._set_buffer(rtu_buffer) + d_rtu.scan(full_model_read=False) + d_rtu.models['common'][0].read() + assert d_rtu.models['common'][0].__class__.__name__ == "SunSpecModbusClientModel" + assert d_rtu.common[0].ID.value == 1 + assert d_rtu.common[0].L.value == 66 + assert d_rtu.common[0].Mn.value == "SunSpecTest" + assert d_rtu.common[0].Md.value == "TestDevice-2" + assert d_rtu.common[0].Opt.value == "opt_a_b_c" + assert d_rtu.common[0].Vr.value == "1.2.3" + assert d_rtu.common[0].SN.value == "sn-123456789" + assert d_rtu.common[0].DA.value == 1 + assert d_rtu.common[0].Pad.value == 0 + + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + c_tcp = client.SunSpecModbusClientDeviceTCP() + tcp_buffer = [b'\x00\x00\x00\x00\x00\x03\x01\x83\x02', + b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00Test-1547-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00'] + c_tcp.client.connect() + c_tcp.client.socket._set_buffer(tcp_buffer) + c_tcp.scan(full_model_read=False) + c_tcp.models['common'][0].read() + assert c_tcp.models['common'][0].__class__.__name__ == "SunSpecModbusClientModel" + assert c_tcp.common[0].ID.value == 1 + assert c_tcp.common[0].L.value == 66 + assert c_tcp.common[0].Mn.value == "SunSpecTest" + assert c_tcp.common[0].Md.value == "Test-1547-1" + assert c_tcp.common[0].Opt.value == "opt_a_b_c" + assert c_tcp.common[0].Vr.value == "1.2.3" + assert c_tcp.common[0].SN.value == "sn-123456789" + assert c_tcp.common[0].DA.value == 1 + assert c_tcp.common[0].Pad.value == 0 + + +class TestSunSpecModbusClientDevice: + def test___init__(self): + d = client.SunSpecModbusClientDevice() + assert d.did + assert d.retry_count == 2 + assert d.base_addr_list == [40000, 0, 50000] + assert d.base_addr is None + + def test_connect(self): + pass + + def test_disconnect(self): + pass + + def test_close(self): + pass + + def test_read(self): + pass + + def test_write(self): + pass + + def test_scan(self, monkeypatch): + # tcp scan + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + c_tcp = client.SunSpecModbusClientDeviceTCP() + tcp_req_check = [b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c@\x00\x03', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9cC\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9cB\x00D', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c\x86\x00\x01'] + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + c_tcp.client.connect() + c_tcp.client.socket._set_buffer(tcp_buffer) + c_tcp.scan() + assert c_tcp.common + assert c_tcp.volt_var + for req in range(len(tcp_req_check)): + assert tcp_req_check[req] == c_tcp.client.socket.request[req] + + # test full model read = false on scan + # also tests successive scans + c_tcp.client.socket.clear_buffer() + c_tcp.client.socket.request = [] + tcp_buffer2 = [ + b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff' + ] + tcp_req_check2 = [ + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c@\x00\x03', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9cC\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c\x86\x00\x01' + ] + c_tcp.client.socket._set_buffer(tcp_buffer2) + c_tcp.scan(full_model_read=False) + assert c_tcp.common + assert c_tcp.common[0].ID.value == 1 + assert c_tcp.common[0].L.value == 66 + assert c_tcp.common[0].Mn.value is None + assert c_tcp.common[0].Md.value is None + for req in range(len(tcp_req_check2)): + assert tcp_req_check2[req] == c_tcp.client.socket.request[req] + + # rtu scan + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c_rtu = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + + rtu_req_check = [ + b'\x01\x03\x9c@\x00\x03*O', + b'\x01\x03\x9cC\x00\x01[\x8e', + b'\x01\x03\x9cB\x00D\xcb\xbd', + b'\x01\x03\x9c\x86\x00\x01K\xb3', + ] + rtu_buffer = [ + b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'TestDevice-2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c' + b'\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\xb9\xfb', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4' + ] + c_rtu.open() + c_rtu.client.serial._set_buffer(rtu_buffer) + c_rtu.scan() + assert c_rtu.common + for req in range(len(rtu_req_check)): + assert rtu_req_check[req] == c_rtu.client.serial.request[req] + + # test full model read = false on scan + # also tests successive scans + c_rtu.client.serial.clear_buffer() + c_rtu.client.serial.request = [] + rtu_req_check2 = [ + b'\x01\x03\x9c@\x00\x03*O', + b'\x01\x03\x9cC\x00\x01[\x8e', + b'\x01\x03\x9c\x86\x00\x01K\xb3' + ] + rtu_buffer2 = [ + b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4' + ] + c_rtu.client.serial._set_buffer(rtu_buffer2) + c_rtu.scan(full_model_read=False) + assert c_rtu.common + assert c_rtu.common[0].ID.value == 1 + assert c_rtu.common[0].L.value == 66 + assert c_rtu.common[0].Mn.value is None + assert c_rtu.common[0].Md.value is None + for req in range(len(rtu_req_check2)): + assert rtu_req_check2[req] == c_rtu.client.serial.request[req] + + def test_get_text(self, monkeypatch): + # tcp scan + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + c_tcp = client.SunSpecModbusClientDeviceTCP() + tcp_req_check = [b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x00\x00\x03', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x03\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x02\x00B', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00F\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00G\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00F\x00@', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x88\x00\x01'] + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + c_tcp.client.connect() + c_tcp.client.socket._set_buffer(tcp_buffer) + c_tcp.scan() + expected_output = \ + '''Model: common (1)\n\n ID 1\n L ''' + \ + ''' 66\n Mn ''' + \ + ''' SunSpecTest\n Md TestDevice-1\n Opt ''' + \ + ''' opt_a_b_c\n Vr ''' + \ + '''1.2.3\n SN sn-123456789\n DA ''' + \ + ''' 1\n Pad 0\n''' + \ + '''\nModel: volt_var (126)\n\n ID 126\n L ''' + \ + ''' 64\n ActCrv ''' + \ + ''' 3\n ModEna None\n WinTms ''' + \ + ''' None Secs\n RvrtTms ''' + \ + ''' None Secs\n RmpTms None Secs\n N''' + \ + '''Crv None\n NPt ''' + \ + ''' None\n V_SF None\n DeptRef_''' + \ + '''SF None\n RmpIncDec_SF ''' + \ + ''' None\n 01:ActPt None\n 01:DeptRef ''' + \ + ''' None\n 01:V1 ''' + \ + ''' None % VRef\n 01:VAr1 None\n 01:V2 ''' + \ + ''' None % VRef\n 01:VAr2 ''' + \ + ''' None\n 01:V3 None % VRef\n 01:VAr3 ''' + \ + ''' None\n 01:V4 ''' + \ + ''' None % VRef\n 01:VAr4 None\n 01:V5 ''' + \ + ''' None % VRef\n 01:VAr5 ''' + \ + ''' None\n 01:V6 None % VRef\n 01:V''' + \ + '''Ar6 None\n 01:V7 ''' + \ + ''' None % VRef\n 01:VAr7 None\n 01:V''' + \ + '''8 None % VRef\n 01:VAr8 ''' + \ + ''' None\n 01:V9 None % VRef\n''' + \ + ''' 01:VAr9 None\n 01:V10 ''' + \ + ''' None % VRef\n 01:VAr10 None\n''' + \ + ''' 01:V11 None % VRef\n 01:VAr11 ''' + \ + ''' None\n 01:V12 None %''' + \ + ''' VRef\n 01:VAr12 None\n 01:V13 ''' + \ + ''' None % VRef\n 01:VAr13 Non''' + \ + '''e\n 01:V14 None % VRef\n 01:VAr14 ''' + \ + ''' None\n 01:V15 Non''' + \ + '''e % VRef\n 01:VAr15 None\n 01:V16 ''' + \ + ''' None % VRef\n 01:VAr16 ''' + \ + ''' None\n 01:V17 None % VRef\n 01:VAr17 ''' + \ + ''' None\n 01:V18 ''' + \ + ''' None % VRef\n 01:VAr18 None\n 01:V19 ''' + \ + ''' None % VRef\n 01:VAr19 ''' + \ + ''' None\n 01:V20 None % VRef\n 01:VAr20''' + \ + ''' None\n 01:CrvNam ''' + \ + ''' None\n 01:RmpTms None Secs\n 01:RmpDecT''' + \ + '''mm None % ref_value/min\n 01:RmpIncTmm ''' + \ + ''' None % ref_value/min\n 01:ReadOnly ''' + \ + ''' None\n''' + get_text_output = c_tcp.get_text() + assert get_text_output[get_text_output.index('Model'):] == expected_output + + # rtu scan + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c_rtu = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + + rtu_req_check = [b'\x01\x03\x00\x00\x00\x03\x05\xcb', b'\x01\x03\x00\x03\x00\x01t\n', + b'\x01\x03\x00\x02\x00Bd;', b'\x01\x03\x00F\x00\x01e\xdf', b'\x01\x03\x00G\x00\x014\x1f', + b'\x01\x03\x00F\x00@\xa5\xef', b'\x01\x03\x00\x88\x00\x01\x04 '] + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + c_rtu.open() + c_rtu.client.serial._set_buffer(rtu_buffer) + c_rtu.scan() + get_text_output = c_rtu.get_text() + assert get_text_output[get_text_output.index('Model'):] == expected_output + + +class TestSunSpecModbusClientDeviceTCP: + def test___init__(self): + d = client.SunSpecModbusClientDeviceTCP() + assert d.slave_id == 1 + assert d.ipaddr == '127.0.0.1' + assert d.ipport == 502 + assert d.timeout is None + assert d.ctx is None + assert d.trace_func is None + assert d.max_count == 125 + assert d.client.__class__.__name__ == 'ModbusClientTCP' + + def test_connect(self, monkeypatch): + d = client.SunSpecModbusClientDeviceTCP() + with pytest.raises(Exception) as exc: + d.connect() + + assert 'Connection error' in str(exc.value) + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + + d.connect() + assert d.client.socket is not None + assert d.client.socket.connected is True + assert d.client.socket.ipaddr == '127.0.0.1' + assert d.client.socket.ipport == 502 + assert d.client.socket.timeout == 2 + + def test_disconnect(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + + d = client.SunSpecModbusClientDeviceTCP() + d.client.connect() + assert d.client.socket + d.client.disconnect() + assert d.client.socket is None + + def test_read(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + d = client.SunSpecModbusClientDeviceTCP() + buffer = [b'\x00\x00\x00\x00\x00\x8f\x01\x03\x8c', b'SunS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c' + b'\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00'] + check_req = b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c@\x00F' + d.client.connect() + d.client.socket._set_buffer(buffer) + assert d.read(40000, 70) == buffer[1] + assert d.client.socket.request[0] == check_req + + def test_write(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + d = client.SunSpecModbusClientDeviceTCP() + d.client.connect() + + data_to_write = b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', b't\x00\x10'] + d.client.socket._set_buffer(buffer) + d.client.write(40052, data_to_write) + + check_req = b"\x00\x00\x00\x00\x00'\x01\x10\x9ct\x00\x10 sn-000\x00\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + assert d.client.socket.request[0] == check_req + + def test_get_text(self, monkeypatch): + # tcp scan + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'connect', MockSocket.mock_tcp_connect) + monkeypatch.setattr(client.SunSpecModbusClientDeviceTCP, 'disconnect', MockSocket.mock_tcp_connect) + + c_tcp = client.SunSpecModbusClientDeviceTCP() + tcp_req_check = [b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x00\x00\x03', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x03\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x02\x00B', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00F\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00G\x00\x01', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00F\x00@', + b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x88\x00\x01'] + tcp_buffer = [b'\x00\x00\x00\x00\x00\t\x01\x03\x06', + b'SunS\x00\x01', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00B', + b'\x00\x00\x00\x00\x00\x8b\x01\x03\x88', + b'\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00~', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\x00@', + b'\x00\x00\x00\x00\x00\x87\x01\x03\x84', + b'\x00~\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00\xff' + b'\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80' + b'\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff', + b'\x00\x00\x00\x00\x00\x05\x01\x03\x02', + b'\xff\xff'] + c_tcp.client.connect() + c_tcp.client.socket._set_buffer(tcp_buffer) + c_tcp.scan() + expected_output = \ + '''Model: common (1)\n\n ID 1\n L ''' + \ + ''' 66\n Mn ''' + \ + ''' SunSpecTest\n Md TestDevice-1\n Opt ''' + \ + ''' opt_a_b_c\n Vr ''' + \ + '''1.2.3\n SN sn-123456789\n DA ''' + \ + ''' 1\n Pad 0\n''' + \ + '''\nModel: volt_var (126)\n\n ID 126\n L ''' + \ + ''' 64\n ActCrv ''' + \ + ''' 3\n ModEna None\n WinTms ''' + \ + ''' None Secs\n RvrtTms ''' + \ + ''' None Secs\n RmpTms None Secs\n N''' + \ + '''Crv None\n NPt ''' + \ + ''' None\n V_SF None\n DeptRef_''' + \ + '''SF None\n RmpIncDec_SF ''' + \ + ''' None\n 01:ActPt None\n 01:DeptRef ''' + \ + ''' None\n 01:V1 ''' + \ + ''' None % VRef\n 01:VAr1 None\n 01:V2 ''' + \ + ''' None % VRef\n 01:VAr2 ''' + \ + ''' None\n 01:V3 None % VRef\n 01:VAr3 ''' + \ + ''' None\n 01:V4 ''' + \ + ''' None % VRef\n 01:VAr4 None\n 01:V5 ''' + \ + ''' None % VRef\n 01:VAr5 ''' + \ + ''' None\n 01:V6 None % VRef\n 01:V''' + \ + '''Ar6 None\n 01:V7 ''' + \ + ''' None % VRef\n 01:VAr7 None\n 01:V''' + \ + '''8 None % VRef\n 01:VAr8 ''' + \ + ''' None\n 01:V9 None % VRef\n''' + \ + ''' 01:VAr9 None\n 01:V10 ''' + \ + ''' None % VRef\n 01:VAr10 None\n''' + \ + ''' 01:V11 None % VRef\n 01:VAr11 ''' + \ + ''' None\n 01:V12 None %''' + \ + ''' VRef\n 01:VAr12 None\n 01:V13 ''' + \ + ''' None % VRef\n 01:VAr13 Non''' + \ + '''e\n 01:V14 None % VRef\n 01:VAr14 ''' + \ + ''' None\n 01:V15 Non''' + \ + '''e % VRef\n 01:VAr15 None\n 01:V16 ''' + \ + ''' None % VRef\n 01:VAr16 ''' + \ + ''' None\n 01:V17 None % VRef\n 01:VAr17 ''' + \ + ''' None\n 01:V18 ''' + \ + ''' None % VRef\n 01:VAr18 None\n 01:V19 ''' + \ + ''' None % VRef\n 01:VAr19 ''' + \ + ''' None\n 01:V20 None % VRef\n 01:VAr20''' + \ + ''' None\n 01:CrvNam ''' + \ + ''' None\n 01:RmpTms None Secs\n 01:RmpDecT''' + \ + '''mm None % ref_value/min\n 01:RmpIncTmm ''' + \ + ''' None % ref_value/min\n 01:ReadOnly ''' + \ + ''' None\n''' + get_text_output = c_tcp.get_text() + assert get_text_output[get_text_output.index('Model'):] == expected_output + + +class TestSunSpecModbusClientDeviceRTU: + def test___init__(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + assert d.slave_id == 1 + assert d.name == "COMM2" + assert d.client.__class__.__name__ == "ModbusClientRTU" + assert d.ctx is None + assert d.trace_func is None + assert d.max_count == 125 + + def test_open(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + d.open() + assert d.client.serial.connected + + def test_close(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + d.open() + d.close() + assert not d.client.serial.connected + + def test_read(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + d.open() + in_buff = [b'\x01\x03\x8cSu', b'nS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00' + b'\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x01\x00\x00\xb7d'] + check_req = b'\x01\x03\x9c@\x00F\xeb\xbc' + d.client.serial._set_buffer(in_buff) + check_read = in_buff[0] + in_buff[1] + assert d.read(40000, 70) == check_read[3:-2] + assert d.client.serial.request[0] == check_req + + def test_write(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + d = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + d.open() + data_to_write = b'v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-000\x00\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + buffer = [b'\x01\x10\x9cl\x00', b'\x18.N'] + d.client.serial._set_buffer(buffer) + d.write(40044, data_to_write) + + check_req = b'\x01\x10\x9cl\x00\x180v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-000\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\xad\xff' + assert d.client.serial.request[0] == check_req + + def test_get_text(self, monkeypatch): + # rtu scan + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c_rtu = client.SunSpecModbusClientDeviceRTU(1, "COMM2") + + rtu_req_check = [b'\x01\x03\x00\x00\x00\x03\x05\xcb', b'\x01\x03\x00\x03\x00\x01t\n', + b'\x01\x03\x00\x02\x00Bd;', b'\x01\x03\x00F\x00\x01e\xdf', b'\x01\x03\x00G\x00\x014\x1f', + b'\x01\x03\x00F\x00@\xa5\xef', b'\x01\x03\x00\x88\x00\x01\x04 '] + rtu_buffer = [b'\x01\x03\x06Su', + b'nS\x00\x01\x8d\xe4', + b'\x01\x03\x02\x00B', + b'8u', + b'\x01\x03\x88\x00\x01', + b'\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00opt_a_b_c\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00M\xf9', + b'\x01\x03\x02\x00~', + b'8d', + b'\x01\x03\x02\x00@', + b'\xb9\xb4', + b'\x01\x03\x84\x00~', + b'\x00@\x00\x03\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x00\x80\x00\x80\x00' + b'\xff\xff\xff\xff\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00' + b'\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff' + b'\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\xff\xff\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xffI', + b'\x01\x03\x02\xff\xff', + b'\xb9\xf4'] + expected_output = \ + '''Model: common (1)\n\n ID 1\n L ''' + \ + ''' 66\n Mn ''' + \ + ''' SunSpecTest\n Md TestDevice-1\n Opt ''' + \ + ''' opt_a_b_c\n Vr ''' + \ + '''1.2.3\n SN sn-123456789\n DA ''' + \ + ''' 1\n Pad 0\n''' + \ + '''\nModel: volt_var (126)\n\n ID 126\n L ''' + \ + ''' 64\n ActCrv ''' + \ + ''' 3\n ModEna None\n WinTms ''' + \ + ''' None Secs\n RvrtTms ''' + \ + ''' None Secs\n RmpTms None Secs\n N''' + \ + '''Crv None\n NPt ''' + \ + ''' None\n V_SF None\n DeptRef_''' + \ + '''SF None\n RmpIncDec_SF ''' + \ + ''' None\n 01:ActPt None\n 01:DeptRef ''' + \ + ''' None\n 01:V1 ''' + \ + ''' None % VRef\n 01:VAr1 None\n 01:V2 ''' + \ + ''' None % VRef\n 01:VAr2 ''' + \ + ''' None\n 01:V3 None % VRef\n 01:VAr3 ''' + \ + ''' None\n 01:V4 ''' + \ + ''' None % VRef\n 01:VAr4 None\n 01:V5 ''' + \ + ''' None % VRef\n 01:VAr5 ''' + \ + ''' None\n 01:V6 None % VRef\n 01:V''' + \ + '''Ar6 None\n 01:V7 ''' + \ + ''' None % VRef\n 01:VAr7 None\n 01:V''' + \ + '''8 None % VRef\n 01:VAr8 ''' + \ + ''' None\n 01:V9 None % VRef\n''' + \ + ''' 01:VAr9 None\n 01:V10 ''' + \ + ''' None % VRef\n 01:VAr10 None\n''' + \ + ''' 01:V11 None % VRef\n 01:VAr11 ''' + \ + ''' None\n 01:V12 None %''' + \ + ''' VRef\n 01:VAr12 None\n 01:V13 ''' + \ + ''' None % VRef\n 01:VAr13 Non''' + \ + '''e\n 01:V14 None % VRef\n 01:VAr14 ''' + \ + ''' None\n 01:V15 Non''' + \ + '''e % VRef\n 01:VAr15 None\n 01:V16 ''' + \ + ''' None % VRef\n 01:VAr16 ''' + \ + ''' None\n 01:V17 None % VRef\n 01:VAr17 ''' + \ + ''' None\n 01:V18 ''' + \ + ''' None % VRef\n 01:VAr18 None\n 01:V19 ''' + \ + ''' None % VRef\n 01:VAr19 ''' + \ + ''' None\n 01:V20 None % VRef\n 01:VAr20''' + \ + ''' None\n 01:CrvNam ''' + \ + ''' None\n 01:RmpTms None Secs\n 01:RmpDecT''' + \ + '''mm None % ref_value/min\n 01:RmpIncTmm ''' + \ + ''' None % ref_value/min\n 01:ReadOnly ''' + \ + ''' None\n''' + c_rtu.open() + c_rtu.client.serial._set_buffer(rtu_buffer) + c_rtu.scan() + get_text_output = c_rtu.get_text() + assert get_text_output[get_text_output.index('Model'):] == expected_output + + +class TestSunSpecFileClientDevice(object): + def test___init__(self): + d = file_client.FileClientDevice(filename=None, addr=40002) + assert d.filename is None + assert d.addr == 40002 + + def test_scan(self): + d = file_client.FileClientDevice(filename='./sunspec2/tests/test_data/device_1547.json', addr=40002) + d.scan() + assert d.models['common'][0] is not None + assert d.models['common'][0].Mn.cvalue == 'SunSpecTest' + assert d.models['common'][0].Md.cvalue == 'Test-1547-1' + assert d.models['common'][0].Opt.cvalue == 'opt_a_b_c' + assert d.models['common'][0].Vr.cvalue == '1.2.3' + assert d.models['common'][0].SN.cvalue == 'sn-123456789' + assert d.models['common'][0].DA.cvalue == 1 + assert d.models['common'][0].Pad.cvalue == 0 + + def test_close(self): + pass + + def test_read(self): + d = file_client.FileClientDevice(filename='./sunspec2/tests/test_data/device_1547.json', addr=40002) + d.scan() + assert d.models['common'][0].model_addr == 40002 + assert d.models['common'][0].points_len == 68 + assert d.models['common'][0].len == 68 + assert d.models['DERMeasureAC'][0].model_addr == 40070 + assert d.models['DERMeasureAC'][0].points_len == 155 + assert d.models['DERMeasureAC'][0].len == 155 + assert d.models['DERMeasureAC'][0].ID.cvalue == 701 + assert d.models['DERMeasureAC'][0].L.cvalue == 153 + assert d.models['DERCapacity'][0].L.cvalue == 50 + assert d.models['DERCapacity'][0].len == 52 + assert d.models['DERCtlAC'][0].len == 67 + assert d.models['DERCtlAC'][0].points_len == 59 + + def test_write(self): + d = file_client.FileClientDevice(filename='./sunspec2/tests/test_data/device_1547.json', addr=40002) + d.scan() + d.models['common'][0].SN.cvalue = 'sn-000' + d.write() + d.read() + assert d.models['common'][0].SN.cvalue == 'sn-000' + + + +if __name__ == "__main__": + pass + diff --git a/sunspec2/tests/test_modbus_modbus.py b/sunspec2/tests/test_modbus_modbus.py new file mode 100644 index 0000000..e4ac92d --- /dev/null +++ b/sunspec2/tests/test_modbus_modbus.py @@ -0,0 +1,223 @@ +import sunspec2.modbus.modbus as modbus_client +import pytest +import socket +import serial +import sunspec2.tests.mock_socket as MockSocket +import sunspec2.tests.mock_port as MockPort + + +def test_modbus_rtu_client(monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.modbus_rtu_client('COMM2') + assert c.baudrate == 9600 + assert c.parity == "N" + assert modbus_client.modbus_rtu_clients['COMM2'] + + with pytest.raises(modbus_client.ModbusClientError) as exc1: + c2 = modbus_client.modbus_rtu_client('COMM2', baudrate=99) + assert 'Modbus client baudrate mismatch' in str(exc1.value) + + with pytest.raises(modbus_client.ModbusClientError) as exc2: + c2 = modbus_client.modbus_rtu_client('COMM2', parity='E') + assert 'Modbus client parity mismatch' in str(exc2.value) + + +def test_modbus_rtu_client_remove(monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.modbus_rtu_client('COMM2') + assert modbus_client.modbus_rtu_clients['COMM2'] + modbus_client.modbus_rtu_client_remove('COMM2') + assert modbus_client.modbus_rtu_clients.get('COMM2') is None + + +def test___generate_crc16_table(): + pass + + +def test_computeCRC(): + pass + + +def test_checkCRC(): + pass + + +class TestModbusClientRTU: + def test___init__(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + assert c.name == "COM2" + assert c.baudrate == 9600 + assert c.parity is None + assert c.serial is not None + assert c.timeout == .5 + assert c.write_timeout == .5 + assert not c.devices + + def test_open(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + c.open() + assert c.serial.connected + + def test_close(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + c.open() + c.close() + assert not c.serial.connected + + def test_add_device(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + c.add_device(1, "1") + assert c.devices.get(1) is not None + assert c.devices[1] == "1" + + def test_remove_device(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + c.add_device(1, "1") + assert c.devices.get(1) is not None + assert c.devices[1] == "1" + c.remove_device(1) + assert c.devices.get(1) is None + + def test__read(self): + pass + + def test_read(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + in_buff = [b'\x01\x03\x8cSu', b'nS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00TestDevice-1\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c\x00' + b'\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'sn-123456789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x01\x00\x00\xb7d'] + check_req = b'\x01\x03\x9c@\x00F\xeb\xbc' + c.open() + c.serial._set_buffer(in_buff) + + check_read = in_buff[0] + in_buff[1] + assert c.read(1, 40000, 70) == check_read[3:-2] + assert c.serial.request[0] == check_req + + def test__write(self): + pass + + def test_write(self, monkeypatch): + monkeypatch.setattr(serial, 'Serial', MockPort.mock_port) + c = modbus_client.ModbusClientRTU(name="COM2") + c.open() + data_to_write = b'v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-000\x00\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + buffer = [b'\x01\x10\x9cl\x00', b'\x18.N'] + c.serial._set_buffer(buffer) + c.write(1, 40044, data_to_write) + + check_req = b'\x01\x10\x9cl\x00\x180v0.0.0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sn-000\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\xad\xff' + assert c.serial.request[0] == check_req + + +class TestModbusClientTCP: + def test___init__(self): + c = modbus_client.ModbusClientTCP() + assert c.slave_id == 1 + assert c.ipaddr == '127.0.0.1' + assert c.ipport == 502 + assert c.timeout == 2 + assert c.ctx is None + assert c.trace_func is None + assert c.max_count == 125 + + def test_close(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + + c = modbus_client.ModbusClientTCP() + c.connect() + assert c.socket + c.disconnect() + assert c.socket is None + + def test_connect(self, monkeypatch): + c = modbus_client.ModbusClientTCP() + + with pytest.raises(Exception) as exc: + c.connect() + assert 'Connection error' in str(exc.value) + + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + c.connect() + assert c.socket is not None + assert c.socket.connected is True + assert c.socket.ipaddr == '127.0.0.1' + assert c.socket.ipport == 502 + assert c.socket.timeout == 2 + + def test_disconnect(self, monkeypatch): + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + + c = modbus_client.ModbusClientTCP() + c.connect() + assert c.socket + c.disconnect() + assert c.socket is None + + def test__read(self, monkeypatch): + pass + + def test_read(self, monkeypatch): + c = modbus_client.ModbusClientTCP() + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + in_buff = [b'\x00\x00\x00\x00\x00\x8f\x01\x03\x8c', b'SunS\x00\x01\x00BSunSpecTest\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00TestDevice-1\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00opt_a_b_c' + b'\x00\x00\x00\x00\x00\x00\x001.2.3\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00sn-123456789\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x01\x00\x00'] + check_req = b'\x00\x00\x00\x00\x00\x06\x01\x03\x9c@\x00F' + c.connect() + c.socket._set_buffer(in_buff) + assert c.read(40000, 70) == in_buff[1] + assert c.socket.request[0] == check_req + + def test__write(self, monkeypatch): + pass + + def test_write(self, monkeypatch): + c = modbus_client.ModbusClientTCP() + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + c.connect() + data_to_write = b'sn-000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c', b't\x00\x10'] + c.socket._set_buffer(buffer) + c.write(40052, data_to_write) + + check_req = b"\x00\x00\x00\x00\x00'\x01\x10\x9ct\x00\x10 sn-000\x00\x00\x00\x00\x00\x00\x00" \ + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + assert c.socket.request[0] == check_req + + def test_write_over_max_size(self, monkeypatch): + c = modbus_client.ModbusClientTCP() + monkeypatch.setattr(socket, 'socket', MockSocket.mock_socket) + c.connect() + data_to_write = bytearray((c.max_write_count+1)*2) + data_to_write[:6] = b'sn-000' + + buffer = [b'\x00\x00\x00\x00\x00\x06\x01\x10\x9ct\x00\x7b', + b'\x00\x00\x00\x00\x00\x06\x01\x10\x9c\xef\x00\x01'] + c.socket._set_buffer(buffer) + c.write(40052, data_to_write) + + check_req0 = b"\x00\x00\x00\x00\x00\xfd\x01" + b"\x10\x9ct\x00{\xf6" + data_to_write[:(c.max_write_count*2)] + check_req1 = b"\x00\x00\x00\x00\x00\x09\x01" + b"\x10\x9c\xef\x00\x01\x02\x00\x00" + assert c.socket.request[0] == check_req0 + assert c.socket.request[1] == check_req1 diff --git a/sunspec2/tests/test_smdx.py b/sunspec2/tests/test_smdx.py new file mode 100644 index 0000000..6445e27 --- /dev/null +++ b/sunspec2/tests/test_smdx.py @@ -0,0 +1,217 @@ +import sunspec2.smdx as smdx +import sunspec2.mdef as mdef +import xml.etree.ElementTree as ET +import pytest +import copy + + +def test_to_smdx_filename(): + assert smdx.to_smdx_filename(77) == 'smdx_00077.xml' + + +def test_model_filename_to_id(): + assert smdx.model_filename_to_id('smdx_00077.xml') == 77 + with pytest.raises(Exception) as exc: + smdx.model_filename_to_id('smdx_abc.xml') + assert 'Error extracting model id from filename' in str(exc.value) + + +def test_from_smdx_file(): + smdx_304 = {'id': 304, 'group': {'name': 'inclinometer', 'type': 'group', 'points': [ + {'name': 'ID', 'value': 304, 'desc': 'Model identifier', 'label': 'Model ID', 'size': 1, 'mandatory': 'M', + 'static': 'S', 'type': 'uint16'}, + {'name': 'L', 'desc': 'Model length', 'label': 'Model Length', 'size': 1, 'mandatory': 'M', 'static': 'S', + 'type': 'uint16'}], 'groups': [{'name': 'incl', 'type': 'group', 'count': 0, 'points': [ + {'name': 'Inclx', 'type': 'int32', 'size': 2, 'mandatory': 'M', 'units': 'Degrees', 'sf': -2, 'label': 'X', + 'desc': 'X-Axis inclination'}, + {'name': 'Incly', 'type': 'int32', 'size': 2, 'units': 'Degrees', 'sf': -2, 'label': 'Y', + 'desc': 'Y-Axis inclination'}, + {'name': 'Inclz', 'type': 'int32', 'size': 2, 'units': 'Degrees', 'sf': -2, 'label': 'Z', + 'desc': 'Z-Axis inclination'}]}], 'label': 'Inclinometer Model', + 'desc': 'Include to support orientation measurements'}} + assert smdx.from_smdx_file('./sunspec2/models/smdx/smdx_00304.xml') == smdx_304 + + +def test_from_smdx_file_symbols(): + mdef = smdx.from_smdx_file('sunspec2/models/smdx/smdx_00803.xml') + for point_def in mdef["group"]["groups"][0]["points"]: + if point_def["name"] != "StrSt": + continue + symbol = point_def["symbols"][1] + assert symbol["name"] == "CONTACTOR_STATUS" + assert symbol["label"] == "Contactor Status" + assert symbol["desc"].startswith("String") + assert symbol["detail"] + break + else: + pytest.fail("Point not found") + + +def test_from_smdx(): + tree = ET.parse('./sunspec2/models/smdx/smdx_00304.xml') + root = tree.getroot() + + mdef_not_found = copy.deepcopy(root) + mdef_not_found.remove(mdef_not_found.find('model')) + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(mdef_not_found) + + duplicate_fixed_btype_str = ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' + duplicate_fixed_btype_xml = ET.fromstring(duplicate_fixed_btype_str) + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(duplicate_fixed_btype_xml) + + dup_repeating_btype_str = ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' + dup_repeating_btype_xml = ET.fromstring(dup_repeating_btype_str) + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(dup_repeating_btype_xml) + + invalid_btype_root = copy.deepcopy(root) + invalid_btype_root.find('model').find('block').set('type', 'abc') + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(invalid_btype_root) + + dup_fixed_p_def_str = ''' + + + + + + + + + + + + + + + + ''' + dup_fixed_p_def_xml = ET.fromstring(dup_fixed_p_def_str) + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(dup_fixed_p_def_xml) + + dup_repeating_p_def_str = ''' + + + + + + + + + + + + + + + + + + + + + ''' + dup_repeating_p_def_xml = ET.fromstring(dup_repeating_p_def_str) + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx(dup_repeating_p_def_xml) + + +def test_from_smdx_point(): + smdx_point_str = """""" + smdx_point_xml = ET.fromstring(smdx_point_str) + assert smdx.from_smdx_point(smdx_point_xml) == {'name': 'Mn', 'type': 'string', 'size': 16, 'mandatory': 'M'} + + missing_pid_xml = copy.deepcopy(smdx_point_xml) + del missing_pid_xml.attrib['id'] + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(missing_pid_xml) + + missing_ptype = copy.deepcopy(smdx_point_xml) + del missing_ptype.attrib['type'] + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(missing_ptype) + + unk_ptype = copy.deepcopy(smdx_point_xml) + unk_ptype.attrib['type'] = 'abc' + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(unk_ptype) + + missing_len = copy.deepcopy(smdx_point_xml) + del missing_len.attrib['len'] + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(missing_len) + + unk_mand_type = copy.deepcopy(smdx_point_xml) + unk_mand_type.attrib['mandatory'] = 'abc' + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(unk_mand_type) + + unk_access_type = copy.deepcopy(smdx_point_xml) + unk_access_type.attrib['access'] = 'abc' + with pytest.raises(mdef.ModelDefinitionError): + smdx.from_smdx_point(unk_access_type) + + +def test_indent(): + pass diff --git a/sunspec2/tests/test_spreadsheet.py b/sunspec2/tests/test_spreadsheet.py new file mode 100644 index 0000000..d5a62a5 --- /dev/null +++ b/sunspec2/tests/test_spreadsheet.py @@ -0,0 +1,526 @@ +import sunspec2.spreadsheet as spreadsheet +import pytest +import csv +import copy +import json + + +def test_idx(): + row = ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', + 'Units', 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'] + + assert spreadsheet.idx(row, 'Address Offset') == 0 + with pytest.raises(ValueError): + del row[0] + assert spreadsheet.idx(row, 'Address Offset', mandatory=True) + + +def test_row_is_empty(): + row = [''] * 10 + assert spreadsheet.row_is_empty(row, 0) + row[0] = 'abc' + assert not spreadsheet.row_is_empty(row, 0) + + +def test_find_name(): + points = [ + { + "name": "Inclx", + "type": "int32", + "mandatory": "M", + "units": "Degrees", + "sf": -2, + "label": "X", + "desc": "X-Axis inclination" + }, + { + "name": "Incly", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Y", + "desc": "Y-Axis inclination" + }, + { + "name": "Inclz", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Z", + "desc": "Z-Axis inclination" + } + ] + assert spreadsheet.find_name(points, 'abc') is None + assert spreadsheet.find_name(points, 'Incly') == points[1] + + +def test_from_spreadsheet(): + model_spreadsheet = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description', 'Standards'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements', '', ''], + [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', '', ''], + [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', '', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', '', ''], + ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', '', ''], + ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', '', ''], + ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '', ''] + ] + model_def = { + 'group': + { + 'name': 'inclinometer', 'type': 'group', 'label': 'Inclinometer Model', + 'desc': 'Include to support orientation measurements', 'points': + [ + {'name': 'ID', 'type': 'uint16', 'size': 1, 'mandatory': 'M', 'static': 'S', + 'label': 'Model ID', + 'desc': 'Model identifier', 'value': 304, 'standards': []}, + {'name': 'L', 'type': 'uint16', 'size': 1, 'mandatory': 'M', 'static': 'S', + 'label': 'Model Length', + 'desc': 'Model length', 'standards': []} + ], + 'groups': [ + {'name': 'incl', 'type': 'group', 'count': 0, 'points': [ + {'name': 'Inclx', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees', + 'mandatory': 'M', 'label': 'X', + 'desc': 'X-Axis inclination', 'standards': []}, + {'name': 'Incly', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees', + 'label': 'Y', + 'desc': 'Y-Axis inclination', 'standards': []}, + {'name': 'Inclz', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees', + 'label': 'Z', + 'desc': 'Z-Axis inclination', 'standards': []}]} + ] + }, + 'id': 304 + } + + spreadsheet_data = spreadsheet.from_spreadsheet(model_spreadsheet) + + assert spreadsheet_data == model_def + + +def test_to_spreadsheet(): + model_spreadsheet = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description', 'Standards'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements', '', ''], + [0, '', 'ID', 304, '', 'uint16', 1, '', '', '', 'M', 'S', 'Model ID', 'Model identifier', '', ''], + [1, '', 'L', '', '', 'uint16', 1, '', '', '', 'M', 'S', 'Model Length', 'Model length', '', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', '', ''], + ['', 0, 'Inclx', '', '', 'int32', 2, -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', '', ''], + ['', 2, 'Incly', '', '', 'int32', 2, -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', '', ''], + ['', 4, 'Inclz', '', '', 'int32', 2, -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '', ''] + ] + + model_def = { + "id": 304, + "group": { + "name": "inclinometer", + "type": "group", + "points": [ + { + "name": "ID", + "value": 304, + "desc": "Model identifier", + "label": "Model ID", + "mandatory": "M", + "static": "S", + "type": "uint16" + }, + { + "name": "L", + "desc": "Model length", + "label": "Model Length", + "mandatory": "M", + "static": "S", + "type": "uint16" + } + ], + "groups": [ + { + "name": "incl", + "type": "group", + "count": 0, + "points": [ + { + "name": "Inclx", + "type": "int32", + "mandatory": "M", + "units": "Degrees", + "sf": -2, + "label": "X", + "desc": "X-Axis inclination" + }, + { + "name": "Incly", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Y", + "desc": "Y-Axis inclination" + }, + { + "name": "Inclz", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Z", + "desc": "Z-Axis inclination" + } + ] + } + ], + "label": "Inclinometer Model", + "desc": "Include to support orientation measurements" + } + } + assert spreadsheet.to_spreadsheet(model_def) == model_spreadsheet + + +def test_to_spreadsheet_group(): + model_def = { + "group": { + "desc": "DER capacity model.", + "label": "DER Capacity", + "name": "DERCapacity", + "points": [ + { + "access": "R", + "desc": "DER capacity model id.", + "label": "DER Capacity Model ID", + "mandatory": "M", + "name": "ID", + "static": "S", + "type": "uint16", + "value": 702 + }, + { + "access": "R", + "desc": "DER capacity name model length.", + "label": "DER Capacity Model Length", + "mandatory": "M", + "name": "L", + "static": "S", + "type": "uint16" + }, + { + "access": "R", + "comments": [ + "Nameplate Ratings - Specifies capacity ratings" + ], + "desc": "Maximum active power rating at unity power factor in watts.", + "label": "Active Power Max Rating", + "mandatory": "O", + "name": "WMaxRtg", + "sf": "W_SF", + "type": "uint16", + "units": "W", + "symbols": [ + { + "name": "CAT_A", + "value": 1 + }, + { + "name": "CAT_B", + "value": 2 + } + ] + } + ], + "type": "group" + }, + "id": 702 + } + ss = [] + spreadsheet.to_spreadsheet_group(ss, model_def['group'], has_notes=False) + assert ss == [ + ['', '', 'DERCapacity', '', '', 'group', '', '', '', '', '', '', 'DER Capacity', 'DER capacity model.', '', ''], + ['', 0, 'ID', 702, '', 'uint16', 1, '', '', '', 'M', 'S', 'DER Capacity Model ID', + 'DER capacity model id.', '', ''], + ['', 1, 'L', '', '', 'uint16', 1, '', '', '', 'M', 'S', 'DER Capacity Model Length', + 'DER capacity name model length.', '', ''], + ['Nameplate Ratings - Specifies capacity ratings', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', 2, 'WMaxRtg', '', '', 'uint16', 1, 'W_SF', 'W', '', '', '', 'Active Power Max Rating', + 'Maximum active power rating at unity power factor in watts.', '', ''], + ['', '', 'CAT_A', 1, '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', 'CAT_B', 2, '', '', '', '', '', '', '', '', '', '', '', '']] + + +def test_to_spreadsheet_point(): + point = { + "access": "R", + "desc": "Abnormal operating performance category as specified in IEEE 1547-2018.", + "label": "Abnormal Operating Category", + "mandatory": "O", + "name": "AbnOpCatRtg", + "symbols": [ + { + "name": "CAT_1", + "value": 1 + }, + { + "name": "CAT_2", + "value": 2 + }, + { + "name": "CAT_3", + "value": 3 + } + ], + "type": "enum16" + } + ss = [] + assert spreadsheet.to_spreadsheet_point(ss, point, has_notes=False) == 1 + + missing_name_p = copy.deepcopy(point) + del missing_name_p['name'] + with pytest.raises(Exception) as exc1: + spreadsheet.to_spreadsheet_point(ss, missing_name_p, has_notes=False) + assert 'Point missing name attribute' in str(exc1.value) + + missing_type_p = copy.deepcopy(point) + del missing_type_p['type'] + with pytest.raises(Exception) as exc2: + spreadsheet.to_spreadsheet_point(ss, missing_type_p, has_notes=False) + assert 'Point AbnOpCatRtg missing type' in str(exc2.value) + + unk_p_type = copy.deepcopy(point) + unk_p_type['type'] = 'abc' + with pytest.raises(Exception) as exc3: + spreadsheet.to_spreadsheet_point(ss, unk_p_type, has_notes=False) + assert 'Unknown point type' in str(exc3.value) + + p_size_not_int = copy.deepcopy(point) + p_size_not_int['type'] = 'string' + p_size_not_int['size'] = 'abc' + with pytest.raises(Exception) as exc4: + spreadsheet.to_spreadsheet_point(ss, p_size_not_int, has_notes=False) + assert 'Point size is for point AbnOpCatRtg not an integer value' in str(exc4.value) + + +def test_to_spreadsheet_symbol(): + symbol = {"name": "MAX_W", "value": 0} + ss = [] + spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False) + assert ss[0][2] == 'MAX_W' and ss[0][3] == 0 + + ss = [] + del symbol['value'] + with pytest.raises(Exception) as exc1: + spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False) + assert 'Symbol MAX_W missing value' in str(exc1.value) + + ss = [] + del symbol['name'] + with pytest.raises(Exception) as exc2: + spreadsheet.to_spreadsheet_symbol(ss, symbol, has_notes=False) + assert 'Symbol missing name attribute' in str(exc2.value) + + +def test_to_spreadsheet_comment(): + ss = [] + spreadsheet.to_spreadsheet_comment(ss, 'Scaling Factors', has_notes=False) + assert ss[0][0] == 'Scaling Factors' + + +def test_spreadsheet_equal(): + spreadsheet_smdx_304 = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements', ''], + ['', '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], + ['', '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], + ['', '', 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], + ['', '', 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], + ['', '', 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] + ] + ss_copy = copy.deepcopy(spreadsheet_smdx_304) + + assert spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy) + + with pytest.raises(Exception) as exc1: + ss_copy[0][0] = 'abc' + spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy) + assert 'Line 1 different' in str(exc1.value) + + with pytest.raises(Exception) as exc2: + del ss_copy[0] + spreadsheet.spreadsheet_equal(spreadsheet_smdx_304, ss_copy) + assert 'Different length' in str(exc2.value) + + +def test_from_csv(): + model_def = { + 'group': { + 'name': 'inclinometer', 'type': 'group', 'label': 'Inclinometer Model', + 'desc': 'Include to support orientation measurements', + 'points': [ + {'name': 'ID', 'type': 'uint16', 'size': 1, 'mandatory': 'M', 'static': 'S', 'label': 'Model ID', + 'desc': 'Model identifier', 'value': 304, 'standards': []}, + {'name': 'L', 'type': 'uint16', 'size': 1, 'mandatory': 'M', 'static': 'S', 'label': 'Model Length', + 'desc': 'Model length', 'standards': []} + ], + 'groups': [ + {'name': 'incl', 'type': 'group', 'count': 0, + 'points': [ + {'name': 'Inclx', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees', 'mandatory': 'M', + 'label': 'X', + 'desc': 'X-Axis inclination', 'standards': []}, + {'name': 'Incly', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees', 'label': 'Y', + 'desc': 'Y-Axis inclination', 'standards': []}, + {'name': 'Inclz', 'type': 'int32', 'size': 2, 'sf': -2, 'units': 'Degrees', 'label': 'Z', + 'desc': 'Z-Axis inclination', 'standards': []} + ] + } + ] + }, + 'id': 304 + } + + csv_data = spreadsheet.from_csv('./sunspec2/tests/test_data/smdx_304.csv') + + assert model_def == csv_data + + +def test_to_csv(tmp_path): + model_def = { + "id": 304, + "group": { + "name": "inclinometer", + "type": "group", + "points": [ + { + "name": "ID", + "value": 304, + "desc": "Model identifier", + "label": "Model ID", + "mandatory": "M", + "static": "S", + "type": "uint16" + }, + { + "name": "L", + "desc": "Model length", + "label": "Model Length", + "mandatory": "M", + "static": "S", + "type": "uint16" + } + ], + "groups": [ + { + "name": "incl", + "type": "group", + "count": 0, + "points": [ + { + "name": "Inclx", + "type": "int32", + "mandatory": "M", + "units": "Degrees", + "sf": -2, + "label": "X", + "desc": "X-Axis inclination" + }, + { + "name": "Incly", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Y", + "desc": "Y-Axis inclination" + }, + { + "name": "Inclz", + "type": "int32", + "units": "Degrees", + "sf": -2, + "label": "Z", + "desc": "Z-Axis inclination" + } + ] + } + ], + "label": "Inclinometer Model", + "desc": "Include to support orientation measurements" + } + } + ss = spreadsheet.to_spreadsheet(model_def) + spreadsheet.to_csv(model_def, filename=tmp_path / 'smdx_304.csv') + + same_data = True + row_num = 0 + idx = 0 + with open(tmp_path / 'smdx_304.csv') as csvfile: + csvreader = csv.reader(csvfile) + for row in csvreader: + idx = 0 + for i in row: + if str(ss[row_num][idx]) != str(i): + same_data = False + idx += 1 + row_num += 1 + assert same_data + + +def test_spreadsheet_from_csv(): + spreadsheet_smdx_304 = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description', 'Standards'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements', '', ''], + ['', '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', '', ''], + ['', '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', '', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', '', ''], + ['', '', 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', '', ''], + ['', '', 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', '', ''], + ['', '', 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '', ''] + ] + + counter = 0 + for row in spreadsheet.spreadsheet_from_csv('./sunspec2/tests/test_data/smdx_304.csv'): + same = True + counter2 = 0 + for i in row: + if i != spreadsheet_smdx_304[counter][counter2]: + same = False + counter2 += 1 + counter += 1 + assert same + + +def test_spreadsheet_to_csv(tmp_path): + spreadsheet_smdx_304 = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements', ''], + [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', ''], + [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', ''], + ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', ''], + ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', ''], + ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', ''] + ] + spreadsheet.spreadsheet_to_csv(spreadsheet_smdx_304, filename=tmp_path / 'smdx_304.csv') + + same_data = True + rowNum = 0 + idx = 0 + with open(tmp_path / 'smdx_304.csv') as csvfile: + csvreader = csv.reader(csvfile) + for row in csvreader: + idx = 0 + for i in row: + if str(spreadsheet_smdx_304[rowNum][idx]) != str(i): + same_data = False + idx += 1 + rowNum += 1 + assert same_data + + diff --git a/sunspec2/tests/test_xlsx.py b/sunspec2/tests/test_xlsx.py new file mode 100644 index 0000000..6d7871a --- /dev/null +++ b/sunspec2/tests/test_xlsx.py @@ -0,0 +1,210 @@ +import sunspec2.xlsx as xlsx +import pytest +import openpyxl +import openpyxl.styles as styles +import json + + +def test___init__(): + wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx') + assert wb.filename == './sunspec2/tests/test_data/wb_701-705.xlsx' + assert wb.params == {} + + wb2 = xlsx.ModelWorkbook() + assert wb2.filename is None + assert wb2.params == {} + + +def test_get_models(): + wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx') + assert wb.get_models() == [701, 702, 703, 704, 705] + wb2 = xlsx.ModelWorkbook() + assert wb2.get_models() == [] + + +def test_save(tmp_path): + wb = xlsx.ModelWorkbook() + wb.save(tmp_path / 'test.xlsx') + wb2 = xlsx.ModelWorkbook(filename=tmp_path / 'test.xlsx') + iter_rows = wb2.xlsx_iter_rows(wb.wb['Index']) + assert next(iter_rows) == ['Model', 'Label', 'Description'] + + +def test_xlsx_iter_rows(): + wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx') + iter_rows = wb.xlsx_iter_rows(wb.wb['704']) + assert next(iter_rows) == ['Address Offset', 'Group Offset', 'Name', + 'Value', 'Count', 'Type', 'Size', 'Scale Factor', + 'Units', 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', + 'Label', 'Description', 'Detailed Description', 'Standards'] + assert next(iter_rows) == [None, None, 'DERCtlAC', None, None, 'group', + None, None, None, None, None, None, 'DER AC Controls', + 'DER AC controls model.', None, None] + + +def test_spreadsheet_from_xlsx(): + wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx') + assert wb.spreadsheet_from_xlsx(704)[0:2] == [['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', + 'Type', 'Size', 'Scale Factor', 'Units', 'RW Access (RW)', + 'Mandatory (M)', 'Static (S)', 'Label', 'Description', + 'Detailed Description', 'Standards'], + ['', '', 'DERCtlAC', None, None, 'group', None, None, None, + None, None, None, 'DER AC Controls', 'DER AC controls model.', None, + None]] + + +def sort_nested_dicts(d): + for key, value in d.items(): + if isinstance(value, dict): + d[key] = sort_nested_dicts(value) # Sort nested dictionaries + elif key == 'points' and isinstance(value, list): + d[key] = sorted(value, key=lambda x: x['name']) + elif isinstance(value, list) and len(value) > 1: + d[key] = sorted(value, key=lambda x: sorted(x.items()) if isinstance(x, dict) else x) + return dict(sorted(d.items())) + + +# need deep diff to compare from_xlsx to json file, right now just compares with its own output +def test_from_xlsx(): + wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx') + with open('./sunspec2/models/json/model_704.json') as f: + from_xlsx_output = json.load(f) + + a = sort_nested_dicts(wb.from_xlsx(704)) + b = sort_nested_dicts(from_xlsx_output) + assert a == b + + +def test_set_cell(): + wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx') + with pytest.raises(ValueError) as exc: + wb.set_cell(wb.wb['704'], 1, 2, 3) + assert 'Workbooks opened with existing file are read only' in str(exc.value) + + wb2 = xlsx.ModelWorkbook() + assert wb2.set_cell(wb2.wb['Index'], 2, 1, 3, style='suns_comment').value == 3 + + +def test_set_info(): + wb = xlsx.ModelWorkbook() + values = [''] * 14 + values[13] = 'description' + values[12] = 'label' + wb.set_info(wb.wb['Index'], 2, values) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + next(iter_rows) + assert next(iter_rows) == [None, None, None, None, None, None, + None, None, None, None, None, None, 'label', 'description'] + + +def test_set_group(): + wb = xlsx.ModelWorkbook() + values = [''] * 16 + values[2] = 'name' + values[5] = 'type' + values[4] = 'count' + values[13] = 'description' + values[12] = 'label' + wb.set_group(wb.wb['Index'], 2, values, 2) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + next(iter_rows) + assert next(iter_rows) == ['', '', 'name', '', 'count', 'type', '', '', '', '', '', '', + 'label', 'description', '', ''] + + +def test_set_point(): + wb = xlsx.ModelWorkbook() + values = [''] * 16 + values[0] = 'addr_offset' + values[1] = 'group_offset' + values[2] = 'name' + values[3] = 'value' + values[4] = 'count' + values[5] = 'type' + values[6] = 'size' + values[7] = 'sf' + values[8] = 'units' + values[9] = 'access' + values[10] = 'mandatory' + values[11] = 'static' + wb.set_point(wb.wb['Index'], 2, values, 1) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + next(iter_rows) + assert next(iter_rows) == ['addr_offset', 'group_offset', 'name', 'value', 'count', 'type', '', + 'sf', 'units', 'access', 'mandatory', 'static', '', '', '', ''] + + +def test_set_symbol(): + wb = xlsx.ModelWorkbook() + values = [''] * 16 + values[2] = 'name' + values[3] = 'value' + values[12] = 'label' + values[13] = 'description' + wb.set_symbol(wb.wb['Index'], 2, values) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + next(iter_rows) # skip header (['Model', 'Label', 'Description', None, ...]) + assert next(iter_rows) == ['', '', 'name', 'value', '', '', '', + '', '', '', '', '', 'label', 'description', '', ''] + + +def test_set_comment(): + wb = xlsx.ModelWorkbook() + wb.set_comment(wb.wb['Index'], 2, ['This is a comment']) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + next(iter_rows) + assert next(iter_rows)[0] == 'This is a comment' + + +def test_set_hdr(): + wb = xlsx.ModelWorkbook() + wb.set_hdr(wb.wb['Index'], ['This', 'is', 'a', 'test', 'header']) + iter_rows = wb.xlsx_iter_rows(wb.wb['Index']) + assert next(iter_rows) == ['This', 'is', 'a', 'test', 'header'] + + +def test_spreadsheet_to_xlsx(): + wb = xlsx.ModelWorkbook(filename='./sunspec2/tests/test_data/wb_701-705.xlsx') + with pytest.raises(ValueError) as exc: + wb.spreadsheet_to_xlsx(702, []) + assert 'Workbooks opened with existing file are read only' in str(exc.value) + + spreadsheet_smdx_304 = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description', 'Standards'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements', '', ''], + [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', '', ''], + [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', '', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', '', ''], + ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', '', ''], + ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', '', ''], + ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '', ''] + ] + wb2 = xlsx.ModelWorkbook() + wb2.spreadsheet_to_xlsx(304, spreadsheet_smdx_304) + iter_rows = wb2.xlsx_iter_rows(wb2.wb['304']) + for row in spreadsheet_smdx_304: + assert next(iter_rows) == row + + +def test_to_xlsx(tmp_path): + spreadsheet_smdx_304 = [ + ['Address Offset', 'Group Offset', 'Name', 'Value', 'Count', 'Type', 'Size', 'Scale Factor', 'Units', + 'RW Access (RW)', 'Mandatory (M)', 'Static (S)', 'Label', 'Description', 'Detailed Description', 'Standards'], + ['', '', 'inclinometer', '', '', 'group', '', '', '', '', '', '', 'Inclinometer Model', + 'Include to support orientation measurements', '', ''], + [0, '', 'ID', 304, '', 'uint16', '', '', '', '', 'M', 'S', 'Model ID', 'Model identifier', '', ''], + [1, '', 'L', '', '', 'uint16', '', '', '', '', 'M', 'S', 'Model Length', 'Model length', '', ''], + ['', '', 'inclinometer.incl', '', 0, 'group', '', '', '', '', '', '', '', '', '', ''], + ['', 0, 'Inclx', '', '', 'int32', '', -2, 'Degrees', '', 'M', '', 'X', 'X-Axis inclination', '', ''], + ['', 2, 'Incly', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Y', 'Y-Axis inclination', '', ''], + ['', 4, 'Inclz', '', '', 'int32', '', -2, 'Degrees', '', '', '', 'Z', 'Z-Axis inclination', '', ''] + ] + with open('./sunspec2/models/json/model_304.json') as f: + m_703 = json.load(f) + wb = xlsx.ModelWorkbook() + wb.to_xlsx(m_703) + iter_rows = wb.xlsx_iter_rows(wb.wb['304']) + for row in spreadsheet_smdx_304: + assert next(iter_rows) == row diff --git a/sunspec2/xlsx.py b/sunspec2/xlsx.py new file mode 100644 index 0000000..3c5ec2f --- /dev/null +++ b/sunspec2/xlsx.py @@ -0,0 +1,413 @@ +""" + Copyright (C) 2020 SunSpec Alliance + + Permission is hereby granted, free of charge, to any person obtaining a + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. +""" + +from sunspec2 import mdef +import sunspec2.spreadsheet as ss + +models_hdr = [('Model', 0), + ('Label', 30), + ('Description', 60)] + +column_width = { + ss.ADDRESS_OFFSET_IDX: 0, + ss.GROUP_OFFSET_IDX: 0, + ss.NAME_IDX: 25, + ss.VALUE_IDX: 12, + ss.COUNT_IDX: 12, + ss.TYPE_IDX: 12, + ss.SIZE_IDX: 12, + ss.SCALE_FACTOR_IDX: 12, + ss.UNITS_IDX: 12, + ss.ACCESS_IDX: 12, + ss.MANDATORY_IDX: 12, + ss.STATIC_IDX: 12, + ss.LABEL_IDX: 30, + ss.DESCRIPTION_IDX: 60, + ss.DETAIL_IDX: 30, + ss.STANDARDS_IDX: 30, + ss.NOTES_IDX: 60 +} + +group_styles = { + 'suns_group_1': { + 'group_color': 'b8cce4', # 184, 204, 228 + 'point_color': 'dce6f1', # 220, 230, 241 + }, + 'suns_group_2': { + 'group_color': 'd8e4bc', # 216, 228, 188 + 'point_color': 'ebf1de', # 235, 241, 222 + }, + 'suns_group_3': { + 'group_color': 'ccc0da', # 204, 192, 218 + 'point_color': 'e4dfec', # 228, 223, 236 + }, + 'suns_group_4': { + 'group_color': 'fcd5b4', # 252, 213, 180 + 'point_color': 'fde9d9', # 253, 233, 217 + }, + 'suns_group_5': { + 'group_color': 'e6b8b7', # 230, 184, 183 + 'point_color': 'f2dcdb', # 242, 220, 219 + } +} + +try: + import openpyxl + import openpyxl.styles as styles + + + class ModelWorkbook(object): + def __init__(self, filename=None, model_dir=None, license_summary=False, params=None): + self.wb = None + self.filename = filename + self.params = params + if self.params is None: + self.params = {} + + if filename is not None: + self.wb = openpyxl.load_workbook(filename=filename) + else: + self.wb = openpyxl.Workbook() + + self.ws_models = self.wb.active + self.ws_models.title = 'Index' + + thin = styles.Side(border_style=self.params.get('side_border', 'thin'), + color=self.params.get('side_color', '999999')) + + for i in range(1, len(group_styles) + 1): + key = 'suns_group_%s' % i + name = 'suns_group_entry_%s' % i + style = styles.NamedStyle(name=name) + color = group_styles[key]['group_color'] + # self.params.get('group_color', color) + style.fill = styles.PatternFill('solid', fgColor=color) + style.font = styles.Font() + style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(style) + + name = 'suns_group_text_%s' % i + style = styles.NamedStyle(name=name) + style.fill = styles.PatternFill('solid', fgColor=color) + style.font = styles.Font() + style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(style) + + name = 'suns_point_entry_%s' % i + style = styles.NamedStyle(name=name) + color = group_styles[key]['point_color'] + # self.params.get('group_color', color) + style.fill = styles.PatternFill('solid', fgColor=color) + style.font = styles.Font() + style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(style) + + name = 'suns_point_text_%s' % i + style = styles.NamedStyle(name=name) + style.fill = styles.PatternFill('solid', fgColor=color) + style.font = styles.Font() + style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(style) + + if 'suns_hdr' not in self.wb.named_styles: + hdr_style = styles.NamedStyle(name='suns_hdr') + hdr_style.fill = styles.PatternFill('solid', fgColor=self.params.get('hdr_color', 'dddddd')) + hdr_style.font = styles.Font(bold=True) + hdr_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + hdr_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(hdr_style) + if 'suns_group_entry' not in self.wb.named_styles: + model_entry_style = styles.NamedStyle(name='suns_group_entry') + model_entry_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('group_color', 'fff9e5')) + model_entry_style.font = styles.Font() + model_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + model_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(model_entry_style) + if 'suns_group_text' not in self.wb.named_styles: + model_text_style = styles.NamedStyle(name='suns_group_text') + model_text_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('group_color', 'fff9e5')) + model_text_style.font = styles.Font() + model_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + model_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(model_text_style) + if 'suns_point_entry' not in self.wb.named_styles: + fixed_entry_style = styles.NamedStyle(name='suns_point_entry') + fixed_entry_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('point_color', 'e6f2ff')) + fixed_entry_style.font = styles.Font() + fixed_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + fixed_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(fixed_entry_style) + if 'suns_point_text' not in self.wb.named_styles: + fixed_text_style = styles.NamedStyle(name='suns_point_text') + fixed_text_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('point_color', 'e6f2ff')) + fixed_text_style.font = styles.Font() + fixed_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + fixed_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(fixed_text_style) + if 'suns_point_variable_entry' not in self.wb.named_styles: + fixed_entry_style = styles.NamedStyle(name='suns_point_variable_entry') + fixed_entry_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('point_variable_color', + 'ecf9ec')) + fixed_entry_style.font = styles.Font() + fixed_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + fixed_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(fixed_entry_style) + if 'suns_point_variable_text' not in self.wb.named_styles: + fixed_text_style = styles.NamedStyle(name='suns_point_variable_text') + fixed_text_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('point_variable_color', + 'ecf9ec')) + fixed_text_style.font = styles.Font() + fixed_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + fixed_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(fixed_text_style) + if 'suns_symbol_entry' not in self.wb.named_styles: + repeating_entry_style = styles.NamedStyle(name='suns_symbol_entry') + repeating_entry_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('symbol_color', 'fafafa')) + repeating_entry_style.font = styles.Font() + repeating_entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + repeating_entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(repeating_entry_style) + if 'suns_symbol_text' not in self.wb.named_styles: + repeating_text_style = styles.NamedStyle(name='suns_symbol_text') + repeating_text_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('symbol_color', 'fafafa')) + repeating_text_style.font = styles.Font() + repeating_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + repeating_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(repeating_text_style) + if 'suns_comment' not in self.wb.named_styles: + symbol_text_style = styles.NamedStyle(name='suns_comment') + symbol_text_style.fill = styles.PatternFill('solid', + fgColor=self.params.get('comment_color', 'dddddd')) + # fgColor=self.params.get('symbol_color', 'fffcd9')) + symbol_text_style.font = styles.Font() + symbol_text_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + symbol_text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(symbol_text_style) + if 'suns_entry' not in self.wb.named_styles: + entry_style = styles.NamedStyle(name='suns_entry') + entry_style.fill = styles.PatternFill('solid', fgColor='ffffff') + entry_style.border = styles.Border(top=thin, left=thin, right=thin, bottom=thin) + entry_style.alignment = styles.Alignment(horizontal='center', wrapText=True) + self.wb.add_named_style(entry_style) + if 'suns_text' not in self.wb.named_styles: + text_style = styles.NamedStyle(name='suns_text') + text_style.font = styles.Font() + text_style.alignment = styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(text_style) + if 'suns_hyper' not in self.wb.named_styles: + hyper_style = openpyxl.styles.NamedStyle(name='suns_hyper') + hyper_style.font = openpyxl.styles.Font(color='0000ee', underline='single') + hyper_style.alignment = openpyxl.styles.Alignment(horizontal='left', wrapText=True) + self.wb.add_named_style(hyper_style) + + for i in range(len(models_hdr)): + self.set_cell(self.ws_models, 1, i + 1, models_hdr[i][0], 'suns_hdr') + if models_hdr[i][1]: + self.ws_models.column_dimensions[chr(65 + i)].width = models_hdr[i][1] + + def get_models(self): + models = [] + if self.wb is not None: + for m in self.wb.sheetnames: + try: + mid = int(m) + models.append(mid) + except: + pass + return models + + def save(self, filename): + self.wb.save(filename) + + def xlsx_iter_rows(self, ws): + for row in ws.iter_rows(): + yield [cell.value for cell in row] + + def spreadsheet_from_xlsx(self, mid=None): + spreadsheet = [] + ws = self.wb[str(mid)] + for row in self.xlsx_iter_rows(ws): + # filter out informative offset information from the normative model definition + if row[ss.TYPE_IDX] and row[ss.TYPE_IDX] != ss.TYPE: + row[ss.ADDRESS_OFFSET_IDX] = '' + row[ss.GROUP_OFFSET_IDX] = '' + spreadsheet.append(row) + return spreadsheet + + def from_xlsx(self, mid=None): + return ss.from_spreadsheet(self.spreadsheet_from_xlsx(mid)) + + def set_cell(self, ws, row, col, value, style=None): + if self.filename: + raise ValueError('Workbooks opened with existing file are read only') + cell = ws.cell(row=row, column=col) + cell.value = value + if style: + cell.style = style + return cell + + def set_info(self, ws, row, values, style=None): + self.set_cell(ws, row, ss.LABEL_IDX + 1, values[ss.LABEL_IDX], style=style) + self.set_cell(ws, row, ss.DESCRIPTION_IDX + 1, values[ss.DESCRIPTION_IDX], style=style) + if len(values) > ss.NOTES_IDX: + self.set_cell(ws, row, ss.NOTES_IDX + 1, values[ss.NOTES_IDX], style=style) + + def set_group(self, ws, row, values, level): + for i in range(len(values)): + self.set_cell(ws, row, i + 1, '', 'suns_group_entry_%s' % level) + self.set_cell(ws, row, ss.NAME_IDX + 1, values[ss.NAME_IDX]) + self.set_cell(ws, row, ss.TYPE_IDX + 1, values[ss.TYPE_IDX]) + self.set_cell(ws, row, ss.COUNT_IDX + 1, values[ss.COUNT_IDX]) + self.set_info(ws, row, values, 'suns_group_text_%s' % level) + + def set_point(self, ws, row, values, level): + entry_style = 'suns_point_entry_%s' % level + text_style = 'suns_point_text_%s' % level + self.set_cell(ws, row, ss.ADDRESS_OFFSET_IDX + 1, values[ss.ADDRESS_OFFSET_IDX], entry_style) + self.set_cell(ws, row, ss.GROUP_OFFSET_IDX + 1, values[ss.GROUP_OFFSET_IDX], entry_style) + self.set_cell(ws, row, ss.NAME_IDX + 1, values[ss.NAME_IDX], entry_style) + self.set_cell(ws, row, ss.VALUE_IDX + 1, values[ss.VALUE_IDX], entry_style) + self.set_cell(ws, row, ss.COUNT_IDX + 1, values[ss.COUNT_IDX], entry_style) + self.set_cell(ws, row, ss.TYPE_IDX + 1, values[ss.TYPE_IDX], entry_style) + + # don't put type size in xlsx unless point type is string + if values[ss.TYPE_IDX] == 'string': + self.set_cell(ws, row, ss.SIZE_IDX + 1, values[ss.SIZE_IDX], entry_style) + else: + self.set_cell(ws, row, ss.SIZE_IDX + 1, '', entry_style) + + self.set_cell(ws, row, ss.SCALE_FACTOR_IDX + 1, values[ss.SCALE_FACTOR_IDX], entry_style) + self.set_cell(ws, row, ss.UNITS_IDX + 1, values[ss.UNITS_IDX], entry_style) + self.set_cell(ws, row, ss.ACCESS_IDX + 1, values[ss.ACCESS_IDX], entry_style) + self.set_cell(ws, row, ss.MANDATORY_IDX + 1, values[ss.MANDATORY_IDX], entry_style) + self.set_cell(ws, row, ss.STATIC_IDX + 1, values[ss.STATIC_IDX], entry_style) + + self.set_info(ws, row, values, text_style) + + self.set_cell(ws, row, ss.DETAIL_IDX + 1, values[ss.DETAIL_IDX], text_style) + self.set_cell(ws, row, ss.STANDARDS_IDX + 1, values[ss.STANDARDS_IDX], text_style) + + def set_symbol(self, ws, row, values): + for i in range(len(values)): + self.set_cell(ws, row, i + 1, '', 'suns_symbol_entry') + self.set_cell(ws, row, ss.NAME_IDX + 1, values[ss.NAME_IDX]) + self.set_cell(ws, row, ss.VALUE_IDX + 1, values[ss.VALUE_IDX]) + self.set_info(ws, row, values, 'suns_symbol_text') + + def set_comment(self, ws, row, values): + ws.merge_cells('A%s:%s%s' % (row, chr(65 + len(values) - 1), row)) + self.set_cell(ws, row, 1, values[0], 'suns_comment') + + def set_hdr(self, ws, values): + """ + Create header + + :param ws: worksheet + :param values: values + :return: None + """ + for i in range(len(values)): + self.set_cell(ws, 1, i + 1, values[i], 'suns_hdr') + width = column_width[i] + if width: + ws.column_dimensions[chr(65 + i)].width = column_width[i] + + def spreadsheet_to_xlsx(self, mid, spreadsheet): + if self.filename: + raise ValueError('Workbooks opened with existing file are read only') + + info = False + label = None + description = None + level = 1 + + ws = self.wb.create_sheet(title=str(mid)) + self.set_hdr(ws, spreadsheet[0]) + row = 2 + for values in spreadsheet[1:]: + # point - has type + etype = values[ss.TYPE_IDX] + if etype: + # group + if etype in mdef.group_types: + level = len(values[ss.NAME_IDX].split('.')) + self.set_group(ws, row, values, level) + if not info: + label = values[ss.LABEL_IDX] + description = values[ss.DESCRIPTION_IDX] + info = True + # point + elif etype in mdef.point_type_info: + self.set_point(ws, row, values, level) + else: + raise Exception('Unknown element type: %s' % etype) + elif values[ss.NAME_IDX]: + # symbol - has name and value with no type + if values[ss.VALUE_IDX] is not None and values[ss.VALUE_IDX] != '': + self.set_symbol(ws, row, values) + # comment - no name, value, or type + elif values[0]: + self.set_comment(ws, row, values) + row += 1 + + if self.ws_models is not None: + row = self.ws_models.max_row + 1 + self.set_cell(self.ws_models, row, 1, str(mid), 'suns_entry') + cell = self.set_cell(self.ws_models, row, 2, label, 'suns_hyper') + cell.hyperlink = '#%s!%s' % (str(mid), 'A1') + self.set_cell(self.ws_models, row, 3, description, 'suns_text') + + def to_xlsx(self, model_def): + mid = model_def[mdef.ID] + spreadsheet = ss.to_spreadsheet(model_def) + self.spreadsheet_to_xlsx(mid, spreadsheet) + + def create_error_sheet(self, mid, err_msg): + ws = self.wb.create_sheet(title=str(mid)) + ws.column_dimensions['A'].width = 40 + ws['A1'] = 'Model Definition Errors' + ws['A1'].font = styles.Font(bold=True) + ws['A1'].alignment = styles.Alignment(horizontal='center') + ws['A2'].alignment = styles.Alignment(horizontal='center', wrap_text=True) + ws['A2'] = err_msg + +except: + # provide indication the openpyxl library not available + class ModelWorkbook(object): + def __init__(self, filename=None, model_dir=None, license_summary=False): + raise ImportError('openpyxl library not installed, it is required for working with .xlsx files') + +if __name__ == "__main__": + pass diff --git a/suntime/__init__.py b/suntime/__init__.py new file mode 100644 index 0000000..8899189 --- /dev/null +++ b/suntime/__init__.py @@ -0,0 +1,5 @@ +from .suntime import Sun, SunTimeException + +__version__ = '1.3.2' +__author__ = 'Krzysztof Stopa' +__license__ = 'LGPLv3' diff --git a/suntime/suntime.py b/suntime/suntime.py new file mode 100644 index 0000000..e4eb53a --- /dev/null +++ b/suntime/suntime.py @@ -0,0 +1,153 @@ +import math +import warnings +from datetime import datetime, timedelta, time, timezone + + +# CONSTANT +TO_RAD = math.pi/180.0 + + +class SunTimeException(Exception): + + def __init__(self, message): + super(SunTimeException, self).__init__(message) + + +class Sun: + """ + Approximated calculation of sunrise and sunset datetimes. Adapted from: + https://stackoverflow.com/questions/19615350/calculate-sunrise-and-sunset-times-for-a-given-gps-coordinate-within-postgresql + """ + def __init__(self, lat, lon): + self._lat = lat + self._lon = lon + + self.lngHour = self._lon / 15 + + def get_sunrise_time(self, at_date=datetime.now(), time_zone=timezone.utc): + """ + :param at_date: Reference date. datetime.now() if not provided. + :param time_zone: pytz object with .tzinfo() or None + :return: sunrise datetime. + :raises: SunTimeException when there is no sunrise and sunset on given location and date. + """ + time_delta = self.get_sun_timedelta(at_date, time_zone=time_zone, is_rise_time=True) + if time_delta is None: + raise SunTimeException('The sun never rises on this location (on the specified date)') + else: + return datetime.combine(at_date, time(tzinfo=time_zone)) + time_delta + + def get_sunset_time(self, at_date=datetime.now(), time_zone=timezone.utc): + """ + Calculate the sunset time for given date. + :param at_date: Reference date. datetime.now() if not provided. + :param time_zone: pytz object with .tzinfo() or None + :return: sunset datetime. + :raises: SunTimeException when there is no sunrise and sunset on given location and date. + """ + time_delta = self.get_sun_timedelta(at_date, time_zone=time_zone, is_rise_time=False) + if time_delta is None: + raise SunTimeException('The sun never rises on this location (on the specified date)') + else: + return datetime.combine(at_date, time(tzinfo=time_zone)) + time_delta + + def get_local_sunrise_time(self, at_date=datetime.now(), time_zone=None): + """ DEPRECATED: Use get_sunrise_time() instead. """ + warnings.warn("get_local_sunrise_time is deprecated and will be removed in future versions." + "Use get_sunrise_time with proper time zone", DeprecationWarning) + + return self.get_sunrise_time(at_date, time_zone) + + def get_local_sunset_time(self, at_date=datetime.now(), time_zone=None): + """ DEPRECATED: Use get_sunset_time() instead. """ + warnings.warn("get_local_sunset_time is deprecated and will be removed in future versions." + "Use get_sunset_time with proper time zone.", DeprecationWarning) + return self.get_sunset_time(at_date, time_zone) + + def get_sun_timedelta(self, at_date, time_zone, is_rise_time=True, zenith=90.8): + """ + Calculate sunrise or sunset date. + :param at_date: Reference date + :param time_zone: pytz object with .tzinfo() or None + :param is_rise_time: True if you want to calculate sunrise time. + :param zenith: Sun reference zenith + :return: timedelta showing hour, minute, and second of sunrise or sunset + """ + + # If not set get local timezone from datetime + if time_zone is None: + time_zone = datetime.now().tzinfo + + # 1. first get the day of the year + N = at_date.timetuple().tm_yday + + # 2. convert the longitude to hour value and calculate an approximate time + if is_rise_time: + t = N + ((6 - self.lngHour) / 24) + else: # sunset + t = N + ((18 - self.lngHour) / 24) + + # 3a. calculate the Sun's mean anomaly + M = (0.9856 * t) - 3.289 + + # 3b. calculate the Sun's true longitude + L = M + (1.916 * math.sin(TO_RAD*M)) + (0.020 * math.sin(TO_RAD * 2 * M)) + 282.634 + L = self._force_range(L, 360) # NOTE: L adjusted into the range [0,360) + + # 4a. calculate the Sun's declination + sinDec = 0.39782 * math.sin(TO_RAD*L) + cosDec = math.cos(math.asin(sinDec)) + + # 4b. calculate the Sun's local hour angle + cosH = (math.cos(TO_RAD*zenith) - (sinDec * math.sin(TO_RAD*self._lat))) / (cosDec * math.cos(TO_RAD*self._lat)) + + if cosH > 1: + return None # The sun never rises on this location (on the specified date) + if cosH < -1: + return None # The sun never sets on this location (on the specified date) + + # 4c. finish calculating H and convert into hours + if is_rise_time: + H = 360 - (1/TO_RAD) * math.acos(cosH) + else: # setting + H = (1/TO_RAD) * math.acos(cosH) + H = H / 15 + + # 5a. calculate the Sun's right ascension + RA = (1/TO_RAD) * math.atan(0.91764 * math.tan(TO_RAD*L)) + RA = self._force_range(RA, 360) # NOTE: RA adjusted into the range [0,360) + + # 5b. right ascension value needs to be in the same quadrant as L + Lquadrant = (math.floor(L/90)) * 90 + RAquadrant = (math.floor(RA/90)) * 90 + RA = RA + (Lquadrant - RAquadrant) + + # 5c. right ascension value needs to be converted into hours + RA = RA / 15 + + # 6. calculate local mean time of rising/setting + T = H + RA - (0.06571 * t) - 6.622 + + # 7a. adjust back to UTC + UT = T - self.lngHour + + if time_zone: + # 7b. adjust back to local time + UT += time_zone.utcoffset(at_date).total_seconds() / 3600 + + # 7c. rounding and impose range bounds + UT = round(UT, 2) + if is_rise_time: + UT = self._force_range(UT, 24) + + # 8. return timedelta + return timedelta(hours=UT) + + @staticmethod + def _force_range(v, max): + # force v to be >= 0 and < max + if v < 0: + return v + max + elif v >= max: + return v - max + return v diff --git a/webSocketServer.py b/webSocketServer.py new file mode 100644 index 0000000..1ed75d7 --- /dev/null +++ b/webSocketServer.py @@ -0,0 +1,33 @@ +import asyncio +import sys +import logging +sys.path.append("./") +import websockets +from websockets import serve, broadcast + +CONNECTIONS = set() +stop = asyncio.Future() +global wsServer + +logging.basicConfig( + format="%(asctime)s %(message)s", + level=logging.WARN, +) + +async def connectHandler(websocket): + CONNECTIONS.add(websocket) + #print("clientConnected") + try: + await websocket.wait_closed() + finally: + CONNECTIONS.remove(websocket) + #print("clientgone") + +def message_all(message): + broadcast(CONNECTIONS, message) + #print(message) +async def serveWebSocket(): + wsServer = await serve(connectHandler, "", 1337) + #await asyncio.Future() # run forever +async def closeWebSocket(): + wsServer.close() \ No newline at end of file diff --git a/websocket/__init__.py b/websocket/__init__.py new file mode 100644 index 0000000..6e018cb --- /dev/null +++ b/websocket/__init__.py @@ -0,0 +1,26 @@ +""" +__init__.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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 ._abnf import * +from ._app import WebSocketApp +from ._core import * +from ._exceptions import * +from ._logging import * +from ._socket import * + +__version__ = "1.3.3" diff --git a/websocket/_abnf.py b/websocket/_abnf.py new file mode 100644 index 0000000..2e5ad97 --- /dev/null +++ b/websocket/_abnf.py @@ -0,0 +1,424 @@ +import array +import os +import struct +import sys + +from ._exceptions import * +from ._utils import validate_utf8 +from threading import Lock + +""" +_abnf.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + +try: + # If wsaccel is available, use compiled routines to mask data. + # wsaccel only provides around a 10% speed boost compared + # to the websocket-client _mask() implementation. + # Note that wsaccel is unmaintained. + from wsaccel.xormask import XorMaskerSimple + + def _mask(_m, _d): + return XorMaskerSimple(_m).process(_d) + +except ImportError: + # wsaccel is not available, use websocket-client _mask() + native_byteorder = sys.byteorder + + def _mask(mask_value, data_value): + datalen = len(data_value) + data_value = int.from_bytes(data_value, native_byteorder) + mask_value = int.from_bytes(mask_value * (datalen // 4) + mask_value[: datalen % 4], native_byteorder) + return (data_value ^ mask_value).to_bytes(datalen, native_byteorder) + + +__all__ = [ + 'ABNF', 'continuous_frame', 'frame_buffer', + 'STATUS_NORMAL', + 'STATUS_GOING_AWAY', + 'STATUS_PROTOCOL_ERROR', + 'STATUS_UNSUPPORTED_DATA_TYPE', + 'STATUS_STATUS_NOT_AVAILABLE', + 'STATUS_ABNORMAL_CLOSED', + 'STATUS_INVALID_PAYLOAD', + 'STATUS_POLICY_VIOLATION', + 'STATUS_MESSAGE_TOO_BIG', + 'STATUS_INVALID_EXTENSION', + 'STATUS_UNEXPECTED_CONDITION', + 'STATUS_BAD_GATEWAY', + 'STATUS_TLS_HANDSHAKE_ERROR', +] + +# closing frame status codes. +STATUS_NORMAL = 1000 +STATUS_GOING_AWAY = 1001 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_UNSUPPORTED_DATA_TYPE = 1003 +STATUS_STATUS_NOT_AVAILABLE = 1005 +STATUS_ABNORMAL_CLOSED = 1006 +STATUS_INVALID_PAYLOAD = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_MESSAGE_TOO_BIG = 1009 +STATUS_INVALID_EXTENSION = 1010 +STATUS_UNEXPECTED_CONDITION = 1011 +STATUS_SERVICE_RESTART = 1012 +STATUS_TRY_AGAIN_LATER = 1013 +STATUS_BAD_GATEWAY = 1014 +STATUS_TLS_HANDSHAKE_ERROR = 1015 + +VALID_CLOSE_STATUS = ( + STATUS_NORMAL, + STATUS_GOING_AWAY, + STATUS_PROTOCOL_ERROR, + STATUS_UNSUPPORTED_DATA_TYPE, + STATUS_INVALID_PAYLOAD, + STATUS_POLICY_VIOLATION, + STATUS_MESSAGE_TOO_BIG, + STATUS_INVALID_EXTENSION, + STATUS_UNEXPECTED_CONDITION, + STATUS_SERVICE_RESTART, + STATUS_TRY_AGAIN_LATER, + STATUS_BAD_GATEWAY, +) + + +class ABNF: + """ + ABNF frame class. + See http://tools.ietf.org/html/rfc5234 + and http://tools.ietf.org/html/rfc6455#section-5.2 + """ + + # operation code values. + OPCODE_CONT = 0x0 + OPCODE_TEXT = 0x1 + OPCODE_BINARY = 0x2 + OPCODE_CLOSE = 0x8 + OPCODE_PING = 0x9 + OPCODE_PONG = 0xa + + # available operation code value tuple + OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, + OPCODE_PING, OPCODE_PONG) + + # opcode human readable string + OPCODE_MAP = { + OPCODE_CONT: "cont", + OPCODE_TEXT: "text", + OPCODE_BINARY: "binary", + OPCODE_CLOSE: "close", + OPCODE_PING: "ping", + OPCODE_PONG: "pong" + } + + # data length threshold. + LENGTH_7 = 0x7e + LENGTH_16 = 1 << 16 + LENGTH_63 = 1 << 63 + + def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, + opcode=OPCODE_TEXT, mask=1, data=""): + """ + Constructor for ABNF. Please check RFC for arguments. + """ + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.opcode = opcode + self.mask = mask + if data is None: + data = "" + self.data = data + self.get_mask_key = os.urandom + + def validate(self, skip_utf8_validation=False) -> None: + """ + Validate the ABNF frame. + + Parameters + ---------- + skip_utf8_validation: skip utf8 validation. + """ + if self.rsv1 or self.rsv2 or self.rsv3: + raise WebSocketProtocolException("rsv is not implemented, yet") + + if self.opcode not in ABNF.OPCODES: + raise WebSocketProtocolException("Invalid opcode %r", self.opcode) + + if self.opcode == ABNF.OPCODE_PING and not self.fin: + raise WebSocketProtocolException("Invalid ping frame.") + + if self.opcode == ABNF.OPCODE_CLOSE: + l = len(self.data) + if not l: + return + if l == 1 or l >= 126: + raise WebSocketProtocolException("Invalid close frame.") + if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]): + raise WebSocketProtocolException("Invalid close frame.") + + code = 256 * self.data[0] + self.data[1] + if not self._is_valid_close_status(code): + raise WebSocketProtocolException("Invalid close opcode %r", code) + + @staticmethod + def _is_valid_close_status(code: int) -> bool: + return code in VALID_CLOSE_STATUS or (3000 <= code < 5000) + + def __str__(self) -> str: + return "fin=" + str(self.fin) \ + + " opcode=" + str(self.opcode) \ + + " data=" + str(self.data) + + @staticmethod + def create_frame(data, opcode, fin=1): + """ + Create frame to send text, binary and other data. + + Parameters + ---------- + data: + data to send. This is string value(byte array). + If opcode is OPCODE_TEXT and this value is unicode, + data value is converted into unicode string, automatically. + opcode: + operation code. please see OPCODE_XXX. + fin: + fin flag. if set to 0, create continue fragmentation. + """ + if opcode == ABNF.OPCODE_TEXT and isinstance(data, str): + data = data.encode("utf-8") + # mask must be set if send data from client + return ABNF(fin, 0, 0, 0, opcode, 1, data) + + def format(self) -> bytes: + """ + Format this object to string(byte array) to send data to server. + """ + if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]): + raise ValueError("not 0 or 1") + if self.opcode not in ABNF.OPCODES: + raise ValueError("Invalid OPCODE") + length = len(self.data) + if length >= ABNF.LENGTH_63: + raise ValueError("data is too long") + + frame_header = chr(self.fin << 7 | + self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 | + self.opcode).encode('latin-1') + if length < ABNF.LENGTH_7: + frame_header += chr(self.mask << 7 | length).encode('latin-1') + elif length < ABNF.LENGTH_16: + frame_header += chr(self.mask << 7 | 0x7e).encode('latin-1') + frame_header += struct.pack("!H", length) + else: + frame_header += chr(self.mask << 7 | 0x7f).encode('latin-1') + frame_header += struct.pack("!Q", length) + + if not self.mask: + return frame_header + self.data + else: + mask_key = self.get_mask_key(4) + return frame_header + self._get_masked(mask_key) + + def _get_masked(self, mask_key): + s = ABNF.mask(mask_key, self.data) + + if isinstance(mask_key, str): + mask_key = mask_key.encode('utf-8') + + return mask_key + s + + @staticmethod + def mask(mask_key, data): + """ + Mask or unmask data. Just do xor for each byte + + Parameters + ---------- + mask_key: bytes or str + 4 byte mask. + data: bytes or str + data to mask/unmask. + """ + if data is None: + data = "" + + if isinstance(mask_key, str): + mask_key = mask_key.encode('latin-1') + + if isinstance(data, str): + data = data.encode('latin-1') + + return _mask(array.array("B", mask_key), array.array("B", data)) + + +class frame_buffer: + _HEADER_MASK_INDEX = 5 + _HEADER_LENGTH_INDEX = 6 + + def __init__(self, recv_fn, skip_utf8_validation): + self.recv = recv_fn + self.skip_utf8_validation = skip_utf8_validation + # Buffers over the packets from the layer beneath until desired amount + # bytes of bytes are received. + self.recv_buffer = [] + self.clear() + self.lock = Lock() + + def clear(self): + self.header = None + self.length = None + self.mask = None + + def has_received_header(self) -> bool: + return self.header is None + + def recv_header(self): + header = self.recv_strict(2) + b1 = header[0] + fin = b1 >> 7 & 1 + rsv1 = b1 >> 6 & 1 + rsv2 = b1 >> 5 & 1 + rsv3 = b1 >> 4 & 1 + opcode = b1 & 0xf + b2 = header[1] + has_mask = b2 >> 7 & 1 + length_bits = b2 & 0x7f + + self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits) + + def has_mask(self): + if not self.header: + return False + return self.header[frame_buffer._HEADER_MASK_INDEX] + + def has_received_length(self) -> bool: + return self.length is None + + def recv_length(self): + bits = self.header[frame_buffer._HEADER_LENGTH_INDEX] + length_bits = bits & 0x7f + if length_bits == 0x7e: + v = self.recv_strict(2) + self.length = struct.unpack("!H", v)[0] + elif length_bits == 0x7f: + v = self.recv_strict(8) + self.length = struct.unpack("!Q", v)[0] + else: + self.length = length_bits + + def has_received_mask(self) -> bool: + return self.mask is None + + def recv_mask(self): + self.mask = self.recv_strict(4) if self.has_mask() else "" + + def recv_frame(self): + + with self.lock: + # Header + if self.has_received_header(): + self.recv_header() + (fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header + + # Frame length + if self.has_received_length(): + self.recv_length() + length = self.length + + # Mask + if self.has_received_mask(): + self.recv_mask() + mask = self.mask + + # Payload + payload = self.recv_strict(length) + if has_mask: + payload = ABNF.mask(mask, payload) + + # Reset for next frame + self.clear() + + frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload) + frame.validate(self.skip_utf8_validation) + + return frame + + def recv_strict(self, bufsize: int) -> bytes: + shortage = bufsize - sum(map(len, self.recv_buffer)) + while shortage > 0: + # Limit buffer size that we pass to socket.recv() to avoid + # fragmenting the heap -- the number of bytes recv() actually + # reads is limited by socket buffer and is relatively small, + # yet passing large numbers repeatedly causes lots of large + # buffers allocated and then shrunk, which results in + # fragmentation. + bytes_ = self.recv(min(16384, shortage)) + self.recv_buffer.append(bytes_) + shortage -= len(bytes_) + + unified = b"".join(self.recv_buffer) + + if shortage == 0: + self.recv_buffer = [] + return unified + else: + self.recv_buffer = [unified[bufsize:]] + return unified[:bufsize] + + +class continuous_frame: + + def __init__(self, fire_cont_frame, skip_utf8_validation): + self.fire_cont_frame = fire_cont_frame + self.skip_utf8_validation = skip_utf8_validation + self.cont_data = None + self.recving_frames = None + + def validate(self, frame): + if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT: + raise WebSocketProtocolException("Illegal frame") + if self.recving_frames and \ + frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): + raise WebSocketProtocolException("Illegal frame") + + def add(self, frame): + if self.cont_data: + self.cont_data[1] += frame.data + else: + if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): + self.recving_frames = frame.opcode + self.cont_data = [frame.opcode, frame.data] + + if frame.fin: + self.recving_frames = None + + def is_fire(self, frame): + return frame.fin or self.fire_cont_frame + + def extract(self, frame): + data = self.cont_data + self.cont_data = None + frame.data = data[1] + if not self.fire_cont_frame and data[0] == ABNF.OPCODE_TEXT and not self.skip_utf8_validation and not validate_utf8(frame.data): + raise WebSocketPayloadException( + "cannot decode: " + repr(frame.data)) + + return [data[0], frame] diff --git a/websocket/_app.py b/websocket/_app.py new file mode 100644 index 0000000..da49ec7 --- /dev/null +++ b/websocket/_app.py @@ -0,0 +1,429 @@ +import selectors +import sys +import threading +import time +import traceback +from ._abnf import ABNF +from ._core import WebSocket, getdefaulttimeout +from ._exceptions import * +from . import _logging + +""" +_app.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + +__all__ = ["WebSocketApp"] + + +class Dispatcher: + """ + Dispatcher + """ + def __init__(self, app, ping_timeout): + self.app = app + self.ping_timeout = ping_timeout + + def read(self, sock, read_callback, check_callback): + while self.app.keep_running: + sel = selectors.DefaultSelector() + sel.register(self.app.sock.sock, selectors.EVENT_READ) + + r = sel.select(self.ping_timeout) + if r: + if not read_callback(): + break + check_callback() + sel.close() + + +class SSLDispatcher: + """ + SSLDispatcher + """ + def __init__(self, app, ping_timeout): + self.app = app + self.ping_timeout = ping_timeout + + def read(self, sock, read_callback, check_callback): + while self.app.keep_running: + r = self.select() + if r: + if not read_callback(): + break + check_callback() + + def select(self): + sock = self.app.sock.sock + if sock.pending(): + return [sock,] + + sel = selectors.DefaultSelector() + sel.register(sock, selectors.EVENT_READ) + + r = sel.select(self.ping_timeout) + sel.close() + + if len(r) > 0: + return r[0][0] + + +class WrappedDispatcher: + """ + WrappedDispatcher + """ + def __init__(self, app, ping_timeout, dispatcher): + self.app = app + self.ping_timeout = ping_timeout + self.dispatcher = dispatcher + + def read(self, sock, read_callback, check_callback): + self.dispatcher.read(sock, read_callback) + self.ping_timeout and self.dispatcher.timeout(self.ping_timeout, check_callback) + + +class WebSocketApp: + """ + Higher level of APIs are provided. The interface is like JavaScript WebSocket object. + """ + + def __init__(self, url, header=None, + on_open=None, on_message=None, on_error=None, + on_close=None, on_ping=None, on_pong=None, + on_cont_message=None, + keep_running=True, get_mask_key=None, cookie=None, + subprotocols=None, + on_data=None, + socket=None): + """ + WebSocketApp initialization + + Parameters + ---------- + url: str + Websocket url. + header: list or dict + Custom header for websocket handshake. + on_open: function + Callback object which is called at opening websocket. + on_open has one argument. + The 1st argument is this class object. + on_message: function + Callback object which is called when received data. + on_message has 2 arguments. + The 1st argument is this class object. + The 2nd argument is utf-8 data received from the server. + on_error: function + Callback object which is called when we get error. + on_error has 2 arguments. + The 1st argument is this class object. + The 2nd argument is exception object. + on_close: function + Callback object which is called when connection is closed. + on_close has 3 arguments. + The 1st argument is this class object. + The 2nd argument is close_status_code. + The 3rd argument is close_msg. + on_cont_message: function + Callback object which is called when a continuation + frame is received. + on_cont_message has 3 arguments. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + The 3rd argument is continue flag. if 0, the data continue + to next frame data + on_data: function + Callback object which is called when a message received. + This is called before on_message or on_cont_message, + and then on_message or on_cont_message is called. + on_data has 4 argument. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came. + The 4th argument is continue flag. If 0, the data continue + keep_running: bool + This parameter is obsolete and ignored. + get_mask_key: function + A callable function to get new mask keys, see the + WebSocket.set_mask_key's docstring for more information. + cookie: str + Cookie value. + subprotocols: list + List of available sub protocols. Default is None. + socket: socket + Pre-initialized stream socket. + """ + self.url = url + self.header = header if header is not None else [] + self.cookie = cookie + + self.on_open = on_open + self.on_message = on_message + self.on_data = on_data + self.on_error = on_error + self.on_close = on_close + self.on_ping = on_ping + self.on_pong = on_pong + self.on_cont_message = on_cont_message + self.keep_running = False + self.get_mask_key = get_mask_key + self.sock = None + self.last_ping_tm = 0 + self.last_pong_tm = 0 + self.subprotocols = subprotocols + self.prepared_socket = socket + + def send(self, data, opcode=ABNF.OPCODE_TEXT): + """ + send message + + Parameters + ---------- + data: str + Message to send. If you set opcode to OPCODE_TEXT, + data must be utf-8 string or unicode. + opcode: int + Operation code of data. Default is OPCODE_TEXT. + """ + + if not self.sock or self.sock.send(data, opcode) == 0: + raise WebSocketConnectionClosedException( + "Connection is already closed.") + + def close(self, **kwargs): + """ + Close websocket connection. + """ + self.keep_running = False + if self.sock: + self.sock.close(**kwargs) + self.sock = None + + def _send_ping(self, interval, event, payload): + while not event.wait(interval): + self.last_ping_tm = time.time() + if self.sock: + try: + self.sock.ping(payload) + except Exception as ex: + _logging.warning("send_ping routine terminated: {}".format(ex)) + break + + def run_forever(self, sockopt=None, sslopt=None, + ping_interval=0, ping_timeout=None, + ping_payload="", + http_proxy_host=None, http_proxy_port=None, + http_no_proxy=None, http_proxy_auth=None, + skip_utf8_validation=False, + host=None, origin=None, dispatcher=None, + suppress_origin=False, proxy_type=None): + """ + Run event loop for WebSocket framework. + + This loop is an infinite loop and is alive while websocket is available. + + Parameters + ---------- + sockopt: tuple + Values for socket.setsockopt. + sockopt must be tuple + and each element is argument of sock.setsockopt. + sslopt: dict + Optional dict object for ssl socket option. + ping_interval: int or float + Automatically send "ping" command + every specified period (in seconds). + If set to 0, no ping is sent periodically. + ping_timeout: int or float + Timeout (in seconds) if the pong message is not received. + ping_payload: str + Payload message to send with each ping. + http_proxy_host: str + HTTP proxy host name. + http_proxy_port: int or str + HTTP proxy port. If not set, set to 80. + http_no_proxy: list + Whitelisted host names that don't use the proxy. + skip_utf8_validation: bool + skip utf8 validation. + host: str + update host header. + origin: str + update origin header. + dispatcher: Dispatcher object + customize reading data from socket. + suppress_origin: bool + suppress outputting origin header. + + Returns + ------- + teardown: bool + False if the `WebSocketApp` is closed or caught KeyboardInterrupt, + True if any other exception was raised during a loop. + """ + + if ping_timeout is not None and ping_timeout <= 0: + raise WebSocketException("Ensure ping_timeout > 0") + if ping_interval is not None and ping_interval < 0: + raise WebSocketException("Ensure ping_interval >= 0") + if ping_timeout and ping_interval and ping_interval <= ping_timeout: + raise WebSocketException("Ensure ping_interval > ping_timeout") + if not sockopt: + sockopt = [] + if not sslopt: + sslopt = {} + if self.sock: + raise WebSocketException("socket is already opened") + thread = None + self.keep_running = True + self.last_ping_tm = 0 + self.last_pong_tm = 0 + + def teardown(close_frame=None): + """ + Tears down the connection. + + Parameters + ---------- + close_frame: ABNF frame + If close_frame is set, the on_close handler is invoked + with the statusCode and reason from the provided frame. + """ + + if thread and thread.is_alive(): + event.set() + thread.join() + self.keep_running = False + if self.sock: + self.sock.close() + close_status_code, close_reason = self._get_close_args( + close_frame if close_frame else None) + self.sock = None + + # Finally call the callback AFTER all teardown is complete + self._callback(self.on_close, close_status_code, close_reason) + + try: + self.sock = WebSocket( + self.get_mask_key, sockopt=sockopt, sslopt=sslopt, + fire_cont_frame=self.on_cont_message is not None, + skip_utf8_validation=skip_utf8_validation, + enable_multithread=True) + self.sock.settimeout(getdefaulttimeout()) + self.sock.connect( + self.url, header=self.header, cookie=self.cookie, + http_proxy_host=http_proxy_host, + http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy, + http_proxy_auth=http_proxy_auth, subprotocols=self.subprotocols, + host=host, origin=origin, suppress_origin=suppress_origin, + proxy_type=proxy_type, socket=self.prepared_socket) + dispatcher = self.create_dispatcher(ping_timeout, dispatcher) + + self._callback(self.on_open) + + if ping_interval: + event = threading.Event() + thread = threading.Thread( + target=self._send_ping, args=(ping_interval, event, ping_payload)) + thread.daemon = True + thread.start() + + def read(): + if not self.keep_running: + return teardown() + + op_code, frame = self.sock.recv_data_frame(True) + if op_code == ABNF.OPCODE_CLOSE: + return teardown(frame) + elif op_code == ABNF.OPCODE_PING: + self._callback(self.on_ping, frame.data) + elif op_code == ABNF.OPCODE_PONG: + self.last_pong_tm = time.time() + self._callback(self.on_pong, frame.data) + elif op_code == ABNF.OPCODE_CONT and self.on_cont_message: + self._callback(self.on_data, frame.data, + frame.opcode, frame.fin) + self._callback(self.on_cont_message, + frame.data, frame.fin) + else: + data = frame.data + if op_code == ABNF.OPCODE_TEXT: + data = data.decode("utf-8") + self._callback(self.on_data, data, frame.opcode, True) + self._callback(self.on_message, data) + + return True + + def check(): + if (ping_timeout): + has_timeout_expired = time.time() - self.last_ping_tm > ping_timeout + has_pong_not_arrived_after_last_ping = self.last_pong_tm - self.last_ping_tm < 0 + has_pong_arrived_too_late = self.last_pong_tm - self.last_ping_tm > ping_timeout + + if (self.last_ping_tm and + has_timeout_expired and + (has_pong_not_arrived_after_last_ping or has_pong_arrived_too_late)): + raise WebSocketTimeoutException("ping/pong timed out") + return True + + dispatcher.read(self.sock.sock, read, check) + return False + except (Exception, KeyboardInterrupt, SystemExit) as e: + self._callback(self.on_error, e) + if isinstance(e, SystemExit): + # propagate SystemExit further + raise + teardown() + return not isinstance(e, KeyboardInterrupt) + + def create_dispatcher(self, ping_timeout, dispatcher=None): + if dispatcher: # If custom dispatcher is set, use WrappedDispatcher + return WrappedDispatcher(self, ping_timeout, dispatcher) + timeout = ping_timeout or 10 + if self.sock.is_ssl(): + return SSLDispatcher(self, timeout) + + return Dispatcher(self, timeout) + + def _get_close_args(self, close_frame): + """ + _get_close_args extracts the close code and reason from the close body + if it exists (RFC6455 says WebSocket Connection Close Code is optional) + """ + # Need to catch the case where close_frame is None + # Otherwise the following if statement causes an error + if not self.on_close or not close_frame: + return [None, None] + + # Extract close frame status code + if close_frame.data and len(close_frame.data) >= 2: + close_status_code = 256 * close_frame.data[0] + close_frame.data[1] + reason = close_frame.data[2:].decode('utf-8') + return [close_status_code, reason] + else: + # Most likely reached this because len(close_frame_data.data) < 2 + return [None, None] + + def _callback(self, callback, *args): + if callback: + try: + callback(self, *args) + + except Exception as e: + _logging.error("error from callback {}: {}".format(callback, e)) + if self.on_error: + self.on_error(self, e) diff --git a/websocket/_cookiejar.py b/websocket/_cookiejar.py new file mode 100644 index 0000000..5476d1d --- /dev/null +++ b/websocket/_cookiejar.py @@ -0,0 +1,64 @@ +import http.cookies + +""" +_cookiejar.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + + +class SimpleCookieJar: + def __init__(self): + self.jar = dict() + + def add(self, set_cookie): + if set_cookie: + simpleCookie = http.cookies.SimpleCookie(set_cookie) + + for k, v in simpleCookie.items(): + domain = v.get("domain") + if domain: + if not domain.startswith("."): + domain = "." + domain + cookie = self.jar.get(domain) if self.jar.get(domain) else http.cookies.SimpleCookie() + cookie.update(simpleCookie) + self.jar[domain.lower()] = cookie + + def set(self, set_cookie): + if set_cookie: + simpleCookie = http.cookies.SimpleCookie(set_cookie) + + for k, v in simpleCookie.items(): + domain = v.get("domain") + if domain: + if not domain.startswith("."): + domain = "." + domain + self.jar[domain.lower()] = simpleCookie + + def get(self, host): + if not host: + return "" + + cookies = [] + for domain, simpleCookie in self.jar.items(): + host = host.lower() + if host.endswith(domain) or host == domain[1:]: + cookies.append(self.jar.get(domain)) + + return "; ".join(filter( + None, sorted( + ["%s=%s" % (k, v.value) for cookie in filter(None, cookies) for k, v in cookie.items()] + ))) diff --git a/websocket/_core.py b/websocket/_core.py new file mode 100644 index 0000000..c36b780 --- /dev/null +++ b/websocket/_core.py @@ -0,0 +1,602 @@ +import socket +import struct +import threading +import time + +# websocket modules +from ._abnf import * +from ._exceptions import * +from ._handshake import * +from ._http import * +from ._logging import * +from ._socket import * +from ._ssl_compat import * +from ._utils import * + +""" +_core.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + +__all__ = ['WebSocket', 'create_connection'] + + +class WebSocket: + """ + Low level WebSocket interface. + + This class is based on the WebSocket protocol `draft-hixie-thewebsocketprotocol-76 `_ + + We can connect to the websocket server and send/receive data. + The following example is an echo client. + + >>> import websocket + >>> ws = websocket.WebSocket() + >>> ws.connect("ws://echo.websocket.org") + >>> ws.send("Hello, Server") + >>> ws.recv() + 'Hello, Server' + >>> ws.close() + + Parameters + ---------- + get_mask_key: func + A callable function to get new mask keys, see the + WebSocket.set_mask_key's docstring for more information. + sockopt: tuple + Values for socket.setsockopt. + sockopt must be tuple and each element is argument of sock.setsockopt. + sslopt: dict + Optional dict object for ssl socket options. See FAQ for details. + fire_cont_frame: bool + Fire recv event for each cont frame. Default is False. + enable_multithread: bool + If set to True, lock send method. + skip_utf8_validation: bool + Skip utf8 validation. + """ + + def __init__(self, get_mask_key=None, sockopt=None, sslopt=None, + fire_cont_frame=False, enable_multithread=True, + skip_utf8_validation=False, **_): + """ + Initialize WebSocket object. + + Parameters + ---------- + sslopt: dict + Optional dict object for ssl socket options. See FAQ for details. + """ + self.sock_opt = sock_opt(sockopt, sslopt) + self.handshake_response = None + self.sock = None + + self.connected = False + self.get_mask_key = get_mask_key + # These buffer over the build-up of a single frame. + self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation) + self.cont_frame = continuous_frame( + fire_cont_frame, skip_utf8_validation) + + if enable_multithread: + self.lock = threading.Lock() + self.readlock = threading.Lock() + else: + self.lock = NoLock() + self.readlock = NoLock() + + def __iter__(self): + """ + Allow iteration over websocket, implying sequential `recv` executions. + """ + while True: + yield self.recv() + + def __next__(self): + return self.recv() + + def next(self): + return self.__next__() + + def fileno(self): + return self.sock.fileno() + + def set_mask_key(self, func): + """ + Set function to create mask key. You can customize mask key generator. + Mainly, this is for testing purpose. + + Parameters + ---------- + func: func + callable object. the func takes 1 argument as integer. + The argument means length of mask key. + This func must return string(byte array), + which length is argument specified. + """ + self.get_mask_key = func + + def gettimeout(self): + """ + Get the websocket timeout (in seconds) as an int or float + + Returns + ---------- + timeout: int or float + returns timeout value (in seconds). This value could be either float/integer. + """ + return self.sock_opt.timeout + + def settimeout(self, timeout): + """ + Set the timeout to the websocket. + + Parameters + ---------- + timeout: int or float + timeout time (in seconds). This value could be either float/integer. + """ + self.sock_opt.timeout = timeout + if self.sock: + self.sock.settimeout(timeout) + + timeout = property(gettimeout, settimeout) + + def getsubprotocol(self): + """ + Get subprotocol + """ + if self.handshake_response: + return self.handshake_response.subprotocol + else: + return None + + subprotocol = property(getsubprotocol) + + def getstatus(self): + """ + Get handshake status + """ + if self.handshake_response: + return self.handshake_response.status + else: + return None + + status = property(getstatus) + + def getheaders(self): + """ + Get handshake response header + """ + if self.handshake_response: + return self.handshake_response.headers + else: + return None + + def is_ssl(self): + try: + return isinstance(self.sock, ssl.SSLSocket) + except: + return False + + headers = property(getheaders) + + def connect(self, url, **options): + """ + Connect to url. url is websocket url scheme. + ie. ws://host:port/resource + You can customize using 'options'. + If you set "header" list object, you can set your own custom header. + + >>> ws = WebSocket() + >>> ws.connect("ws://echo.websocket.org/", + ... header=["User-Agent: MyProgram", + ... "x-custom: header"]) + + Parameters + ---------- + header: list or dict + Custom http header list or dict. + cookie: str + Cookie value. + origin: str + Custom origin url. + connection: str + Custom connection header value. + Default value "Upgrade" set in _handshake.py + suppress_origin: bool + Suppress outputting origin header. + host: str + Custom host header string. + timeout: int or float + Socket timeout time. This value is an integer or float. + If you set None for this value, it means "use default_timeout value" + http_proxy_host: str + HTTP proxy host name. + http_proxy_port: str or int + HTTP proxy port. Default is 80. + http_no_proxy: list + Whitelisted host names that don't use the proxy. + http_proxy_auth: tuple + HTTP proxy auth information. Tuple of username and password. Default is None. + redirect_limit: int + Number of redirects to follow. + subprotocols: list + List of available subprotocols. Default is None. + socket: socket + Pre-initialized stream socket. + """ + self.sock_opt.timeout = options.get('timeout', self.sock_opt.timeout) + self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options), + options.pop('socket', None)) + + try: + self.handshake_response = handshake(self.sock, url, *addrs, **options) + for attempt in range(options.pop('redirect_limit', 3)): + if self.handshake_response.status in SUPPORTED_REDIRECT_STATUSES: + url = self.handshake_response.headers['location'] + self.sock.close() + self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options), + options.pop('socket', None)) + self.handshake_response = handshake(self.sock, url, *addrs, **options) + self.connected = True + except: + if self.sock: + self.sock.close() + self.sock = None + raise + + def send(self, payload, opcode=ABNF.OPCODE_TEXT): + """ + Send the data as string. + + Parameters + ---------- + payload: str + Payload must be utf-8 string or unicode, + If the opcode is OPCODE_TEXT. + Otherwise, it must be string(byte array). + opcode: int + Operation code (opcode) to send. + """ + + frame = ABNF.create_frame(payload, opcode) + return self.send_frame(frame) + + def send_frame(self, frame): + """ + Send the data frame. + + >>> ws = create_connection("ws://echo.websocket.org/") + >>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT) + >>> ws.send_frame(frame) + >>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0) + >>> ws.send_frame(frame) + >>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1) + >>> ws.send_frame(frame) + + Parameters + ---------- + frame: ABNF frame + frame data created by ABNF.create_frame + """ + if self.get_mask_key: + frame.get_mask_key = self.get_mask_key + data = frame.format() + length = len(data) + if (isEnabledForTrace()): + trace("++Sent raw: " + repr(data)) + trace("++Sent decoded: " + frame.__str__()) + with self.lock: + while data: + l = self._send(data) + data = data[l:] + + return length + + def send_binary(self, payload): + """ + Send a binary message (OPCODE_BINARY). + + Parameters + ---------- + payload: bytes + payload of message to send. + """ + return self.send(payload, ABNF.OPCODE_BINARY) + + def ping(self, payload=""): + """ + Send ping data. + + Parameters + ---------- + payload: str + data payload to send server. + """ + if isinstance(payload, str): + payload = payload.encode("utf-8") + self.send(payload, ABNF.OPCODE_PING) + + def pong(self, payload=""): + """ + Send pong data. + + Parameters + ---------- + payload: str + data payload to send server. + """ + if isinstance(payload, str): + payload = payload.encode("utf-8") + self.send(payload, ABNF.OPCODE_PONG) + + def recv(self): + """ + Receive string data(byte array) from the server. + + Returns + ---------- + data: string (byte array) value. + """ + with self.readlock: + opcode, data = self.recv_data() + if opcode == ABNF.OPCODE_TEXT: + return data.decode("utf-8") + elif opcode == ABNF.OPCODE_TEXT or opcode == ABNF.OPCODE_BINARY: + return data + else: + return '' + + def recv_data(self, control_frame=False): + """ + Receive data with operation code. + + Parameters + ---------- + control_frame: bool + a boolean flag indicating whether to return control frame + data, defaults to False + + Returns + ------- + opcode, frame.data: tuple + tuple of operation code and string(byte array) value. + """ + opcode, frame = self.recv_data_frame(control_frame) + return opcode, frame.data + + def recv_data_frame(self, control_frame=False): + """ + Receive data with operation code. + + If a valid ping message is received, a pong response is sent. + + Parameters + ---------- + control_frame: bool + a boolean flag indicating whether to return control frame + data, defaults to False + + Returns + ------- + frame.opcode, frame: tuple + tuple of operation code and string(byte array) value. + """ + while True: + frame = self.recv_frame() + if (isEnabledForTrace()): + trace("++Rcv raw: " + repr(frame.format())) + trace("++Rcv decoded: " + frame.__str__()) + if not frame: + # handle error: + # 'NoneType' object has no attribute 'opcode' + raise WebSocketProtocolException( + "Not a valid frame %s" % frame) + elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT): + self.cont_frame.validate(frame) + self.cont_frame.add(frame) + + if self.cont_frame.is_fire(frame): + return self.cont_frame.extract(frame) + + elif frame.opcode == ABNF.OPCODE_CLOSE: + self.send_close() + return frame.opcode, frame + elif frame.opcode == ABNF.OPCODE_PING: + if len(frame.data) < 126: + self.pong(frame.data) + else: + raise WebSocketProtocolException( + "Ping message is too long") + if control_frame: + return frame.opcode, frame + elif frame.opcode == ABNF.OPCODE_PONG: + if control_frame: + return frame.opcode, frame + + def recv_frame(self): + """ + Receive data as frame from server. + + Returns + ------- + self.frame_buffer.recv_frame(): ABNF frame object + """ + return self.frame_buffer.recv_frame() + + def send_close(self, status=STATUS_NORMAL, reason=b""): + """ + Send close data to the server. + + Parameters + ---------- + status: int + Status code to send. See STATUS_XXX. + reason: str or bytes + The reason to close. This must be string or UTF-8 bytes. + """ + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + self.connected = False + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) + + def close(self, status=STATUS_NORMAL, reason=b"", timeout=3): + """ + Close Websocket object + + Parameters + ---------- + status: int + Status code to send. See STATUS_XXX. + reason: bytes + The reason to close in UTF-8. + timeout: int or float + Timeout until receive a close frame. + If None, it will wait forever until receive a close frame. + """ + if self.connected: + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + + try: + self.connected = False + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) + sock_timeout = self.sock.gettimeout() + self.sock.settimeout(timeout) + start_time = time.time() + while timeout is None or time.time() - start_time < timeout: + try: + frame = self.recv_frame() + if frame.opcode != ABNF.OPCODE_CLOSE: + continue + if isEnabledForError(): + recv_status = struct.unpack("!H", frame.data[0:2])[0] + if recv_status >= 3000 and recv_status <= 4999: + debug("close status: " + repr(recv_status)) + elif recv_status != STATUS_NORMAL: + error("close status: " + repr(recv_status)) + break + except: + break + self.sock.settimeout(sock_timeout) + self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + + self.shutdown() + + def abort(self): + """ + Low-level asynchronous abort, wakes up other threads that are waiting in recv_* + """ + if self.connected: + self.sock.shutdown(socket.SHUT_RDWR) + + def shutdown(self): + """ + close socket, immediately. + """ + if self.sock: + self.sock.close() + self.sock = None + self.connected = False + + def _send(self, data): + return send(self.sock, data) + + def _recv(self, bufsize): + try: + return recv(self.sock, bufsize) + except WebSocketConnectionClosedException: + if self.sock: + self.sock.close() + self.sock = None + self.connected = False + raise + + +def create_connection(url, timeout=None, class_=WebSocket, **options): + """ + Connect to url and return websocket object. + + Connect to url and return the WebSocket object. + Passing optional timeout parameter will set the timeout on the socket. + If no timeout is supplied, + the global default timeout setting returned by getdefaulttimeout() is used. + You can customize using 'options'. + If you set "header" list object, you can set your own custom header. + + >>> conn = create_connection("ws://echo.websocket.org/", + ... header=["User-Agent: MyProgram", + ... "x-custom: header"]) + + Parameters + ---------- + class_: class + class to instantiate when creating the connection. It has to implement + settimeout and connect. It's __init__ should be compatible with + WebSocket.__init__, i.e. accept all of it's kwargs. + header: list or dict + custom http header list or dict. + cookie: str + Cookie value. + origin: str + custom origin url. + suppress_origin: bool + suppress outputting origin header. + host: str + custom host header string. + timeout: int or float + socket timeout time. This value could be either float/integer. + If set to None, it uses the default_timeout value. + http_proxy_host: str + HTTP proxy host name. + http_proxy_port: str or int + HTTP proxy port. If not set, set to 80. + http_no_proxy: list + Whitelisted host names that don't use the proxy. + http_proxy_auth: tuple + HTTP proxy auth information. tuple of username and password. Default is None. + enable_multithread: bool + Enable lock for multithread. + redirect_limit: int + Number of redirects to follow. + sockopt: tuple + Values for socket.setsockopt. + sockopt must be a tuple and each element is an argument of sock.setsockopt. + sslopt: dict + Optional dict object for ssl socket options. See FAQ for details. + subprotocols: list + List of available subprotocols. Default is None. + skip_utf8_validation: bool + Skip utf8 validation. + socket: socket + Pre-initialized stream socket. + """ + sockopt = options.pop("sockopt", []) + sslopt = options.pop("sslopt", {}) + fire_cont_frame = options.pop("fire_cont_frame", False) + enable_multithread = options.pop("enable_multithread", True) + skip_utf8_validation = options.pop("skip_utf8_validation", False) + websock = class_(sockopt=sockopt, sslopt=sslopt, + fire_cont_frame=fire_cont_frame, + enable_multithread=enable_multithread, + skip_utf8_validation=skip_utf8_validation, **options) + websock.settimeout(timeout if timeout is not None else getdefaulttimeout()) + websock.connect(url, **options) + return websock diff --git a/websocket/_exceptions.py b/websocket/_exceptions.py new file mode 100644 index 0000000..811d594 --- /dev/null +++ b/websocket/_exceptions.py @@ -0,0 +1,80 @@ +""" +_exceptions.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + + +class WebSocketException(Exception): + """ + WebSocket exception class. + """ + pass + + +class WebSocketProtocolException(WebSocketException): + """ + If the WebSocket protocol is invalid, this exception will be raised. + """ + pass + + +class WebSocketPayloadException(WebSocketException): + """ + If the WebSocket payload is invalid, this exception will be raised. + """ + pass + + +class WebSocketConnectionClosedException(WebSocketException): + """ + If remote host closed the connection or some network error happened, + this exception will be raised. + """ + pass + + +class WebSocketTimeoutException(WebSocketException): + """ + WebSocketTimeoutException will be raised at socket timeout during read/write data. + """ + pass + + +class WebSocketProxyException(WebSocketException): + """ + WebSocketProxyException will be raised when proxy error occurred. + """ + pass + + +class WebSocketBadStatusException(WebSocketException): + """ + WebSocketBadStatusException will be raised when we get bad handshake status code. + """ + + def __init__(self, message, status_code, status_message=None, resp_headers=None): + msg = message % (status_code, status_message) + super().__init__(msg) + self.status_code = status_code + self.resp_headers = resp_headers + + +class WebSocketAddressException(WebSocketException): + """ + If the websocket address info cannot be found, this exception will be raised. + """ + pass diff --git a/websocket/_handshake.py b/websocket/_handshake.py new file mode 100644 index 0000000..6a57c95 --- /dev/null +++ b/websocket/_handshake.py @@ -0,0 +1,195 @@ +""" +_handshake.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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 hashlib +import hmac +import os +from base64 import encodebytes as base64encode +from http import client as HTTPStatus +from ._cookiejar import SimpleCookieJar +from ._exceptions import * +from ._http import * +from ._logging import * +from ._socket import * + +__all__ = ["handshake_response", "handshake", "SUPPORTED_REDIRECT_STATUSES"] + +# websocket supported version. +VERSION = 13 + +SUPPORTED_REDIRECT_STATUSES = (HTTPStatus.MOVED_PERMANENTLY, HTTPStatus.FOUND, HTTPStatus.SEE_OTHER,) +SUCCESS_STATUSES = SUPPORTED_REDIRECT_STATUSES + (HTTPStatus.SWITCHING_PROTOCOLS,) + +CookieJar = SimpleCookieJar() + + +class handshake_response: + + def __init__(self, status, headers, subprotocol): + self.status = status + self.headers = headers + self.subprotocol = subprotocol + CookieJar.add(headers.get("set-cookie")) + + +def handshake(sock, url, hostname, port, resource, **options): + headers, key = _get_handshake_headers(resource, url, hostname, port, options) + + header_str = "\r\n".join(headers) + send(sock, header_str) + dump("request header", header_str) + + status, resp = _get_resp_headers(sock) + if status in SUPPORTED_REDIRECT_STATUSES: + return handshake_response(status, resp, None) + success, subproto = _validate(resp, key, options.get("subprotocols")) + if not success: + raise WebSocketException("Invalid WebSocket Header") + + return handshake_response(status, resp, subproto) + + +def _pack_hostname(hostname): + # IPv6 address + if ':' in hostname: + return '[' + hostname + ']' + + return hostname + + +def _get_handshake_headers(resource, url, host, port, options): + headers = [ + "GET %s HTTP/1.1" % resource, + "Upgrade: websocket" + ] + if port == 80 or port == 443: + hostport = _pack_hostname(host) + else: + hostport = "%s:%d" % (_pack_hostname(host), port) + if options.get("host"): + headers.append("Host: %s" % options["host"]) + else: + headers.append("Host: %s" % hostport) + + # scheme indicates whether http or https is used in Origin + # The same approach is used in parse_url of _url.py to set default port + scheme, url = url.split(":", 1) + if not options.get("suppress_origin"): + if "origin" in options and options["origin"] is not None: + headers.append("Origin: %s" % options["origin"]) + elif scheme == "wss": + headers.append("Origin: https://%s" % hostport) + else: + headers.append("Origin: http://%s" % hostport) + + key = _create_sec_websocket_key() + + # Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified + if not options.get('header') or 'Sec-WebSocket-Key' not in options['header']: + key = _create_sec_websocket_key() + headers.append("Sec-WebSocket-Key: %s" % key) + else: + key = options['header']['Sec-WebSocket-Key'] + + if not options.get('header') or 'Sec-WebSocket-Version' not in options['header']: + headers.append("Sec-WebSocket-Version: %s" % VERSION) + + if not options.get('connection'): + headers.append('Connection: Upgrade') + else: + headers.append(options['connection']) + + subprotocols = options.get("subprotocols") + if subprotocols: + headers.append("Sec-WebSocket-Protocol: %s" % ",".join(subprotocols)) + + header = options.get("header") + if header: + if isinstance(header, dict): + header = [ + ": ".join([k, v]) + for k, v in header.items() + if v is not None + ] + headers.extend(header) + + server_cookie = CookieJar.get(host) + client_cookie = options.get("cookie", None) + + cookie = "; ".join(filter(None, [server_cookie, client_cookie])) + + if cookie: + headers.append("Cookie: %s" % cookie) + + headers.append("") + headers.append("") + + return headers, key + + +def _get_resp_headers(sock, success_statuses=SUCCESS_STATUSES): + status, resp_headers, status_message = read_headers(sock) + if status not in success_statuses: + raise WebSocketBadStatusException("Handshake status %d %s", status, status_message, resp_headers) + return status, resp_headers + + +_HEADERS_TO_CHECK = { + "upgrade": "websocket", + "connection": "upgrade", +} + + +def _validate(headers, key, subprotocols): + subproto = None + for k, v in _HEADERS_TO_CHECK.items(): + r = headers.get(k, None) + if not r: + return False, None + r = [x.strip().lower() for x in r.split(',')] + if v not in r: + return False, None + + if subprotocols: + subproto = headers.get("sec-websocket-protocol", None) + if not subproto or subproto.lower() not in [s.lower() for s in subprotocols]: + error("Invalid subprotocol: " + str(subprotocols)) + return False, None + subproto = subproto.lower() + + result = headers.get("sec-websocket-accept", None) + if not result: + return False, None + result = result.lower() + + if isinstance(result, str): + result = result.encode('utf-8') + + value = (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8') + hashed = base64encode(hashlib.sha1(value).digest()).strip().lower() + success = hmac.compare_digest(hashed, result) + + if success: + return True, subproto + else: + return False, None + + +def _create_sec_websocket_key(): + randomness = os.urandom(16) + return base64encode(randomness).decode('utf-8').strip() diff --git a/websocket/_http.py b/websocket/_http.py new file mode 100644 index 0000000..cdf2f02 --- /dev/null +++ b/websocket/_http.py @@ -0,0 +1,336 @@ +""" +_http.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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 errno +import os +import socket +import sys + +from ._exceptions import * +from ._logging import * +from ._socket import* +from ._ssl_compat import * +from ._url import * + +from base64 import encodebytes as base64encode + +__all__ = ["proxy_info", "connect", "read_headers"] + +try: + from python_socks.sync import Proxy + from python_socks._errors import * + from python_socks._types import ProxyType + HAVE_PYTHON_SOCKS = True +except: + HAVE_PYTHON_SOCKS = False + + class ProxyError(Exception): + pass + + class ProxyTimeoutError(Exception): + pass + + class ProxyConnectionError(Exception): + pass + + +class proxy_info: + + def __init__(self, **options): + self.proxy_host = options.get("http_proxy_host", None) + if self.proxy_host: + self.proxy_port = options.get("http_proxy_port", 0) + self.auth = options.get("http_proxy_auth", None) + self.no_proxy = options.get("http_no_proxy", None) + self.proxy_protocol = options.get("proxy_type", "http") + # Note: If timeout not specified, default python-socks timeout is 60 seconds + self.proxy_timeout = options.get("timeout", None) + if self.proxy_protocol not in ['http', 'socks4', 'socks4a', 'socks5', 'socks5h']: + raise ProxyError("Only http, socks4, socks5 proxy protocols are supported") + else: + self.proxy_port = 0 + self.auth = None + self.no_proxy = None + self.proxy_protocol = "http" + + +def _start_proxied_socket(url, options, proxy): + if not HAVE_PYTHON_SOCKS: + raise WebSocketException("Python Socks is needed for SOCKS proxying but is not available") + + hostname, port, resource, is_secure = parse_url(url) + + if proxy.proxy_protocol == "socks5": + rdns = False + proxy_type = ProxyType.SOCKS5 + if proxy.proxy_protocol == "socks4": + rdns = False + proxy_type = ProxyType.SOCKS4 + # socks5h and socks4a send DNS through proxy + if proxy.proxy_protocol == "socks5h": + rdns = True + proxy_type = ProxyType.SOCKS5 + if proxy.proxy_protocol == "socks4a": + rdns = True + proxy_type = ProxyType.SOCKS4 + + ws_proxy = Proxy.create( + proxy_type=proxy_type, + host=proxy.proxy_host, + port=int(proxy.proxy_port), + username=proxy.auth[0] if proxy.auth else None, + password=proxy.auth[1] if proxy.auth else None, + rdns=rdns) + + sock = ws_proxy.connect(hostname, port, timeout=proxy.proxy_timeout) + + if is_secure and HAVE_SSL: + sock = _ssl_socket(sock, options.sslopt, hostname) + elif is_secure: + raise WebSocketException("SSL not available.") + + return sock, (hostname, port, resource) + + +def connect(url, options, proxy, socket): + # Use _start_proxied_socket() only for socks4 or socks5 proxy + # Use _tunnel() for http proxy + # TODO: Use python-socks for http protocol also, to standardize flow + if proxy.proxy_host and not socket and not (proxy.proxy_protocol == "http"): + return _start_proxied_socket(url, options, proxy) + + hostname, port, resource, is_secure = parse_url(url) + + if socket: + return socket, (hostname, port, resource) + + addrinfo_list, need_tunnel, auth = _get_addrinfo_list( + hostname, port, is_secure, proxy) + if not addrinfo_list: + raise WebSocketException( + "Host not found.: " + hostname + ":" + str(port)) + + sock = None + try: + sock = _open_socket(addrinfo_list, options.sockopt, options.timeout) + if need_tunnel: + sock = _tunnel(sock, hostname, port, auth) + + if is_secure: + if HAVE_SSL: + sock = _ssl_socket(sock, options.sslopt, hostname) + else: + raise WebSocketException("SSL not available.") + + return sock, (hostname, port, resource) + except: + if sock: + sock.close() + raise + + +def _get_addrinfo_list(hostname, port, is_secure, proxy): + phost, pport, pauth = get_proxy_info( + hostname, is_secure, proxy.proxy_host, proxy.proxy_port, proxy.auth, proxy.no_proxy) + try: + # when running on windows 10, getaddrinfo without socktype returns a socktype 0. + # This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0` + # or `OSError: [Errno 22] Invalid argument` when creating socket. Force the socket type to SOCK_STREAM. + if not phost: + addrinfo_list = socket.getaddrinfo( + hostname, port, 0, socket.SOCK_STREAM, socket.SOL_TCP) + return addrinfo_list, False, None + else: + pport = pport and pport or 80 + # when running on windows 10, the getaddrinfo used above + # returns a socktype 0. This generates an error exception: + # _on_error: exception Socket type must be stream or datagram, not 0 + # Force the socket type to SOCK_STREAM + addrinfo_list = socket.getaddrinfo(phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP) + return addrinfo_list, True, pauth + except socket.gaierror as e: + raise WebSocketAddressException(e) + + +def _open_socket(addrinfo_list, sockopt, timeout): + err = None + for addrinfo in addrinfo_list: + family, socktype, proto = addrinfo[:3] + sock = socket.socket(family, socktype, proto) + sock.settimeout(timeout) + for opts in DEFAULT_SOCKET_OPTION: + sock.setsockopt(*opts) + for opts in sockopt: + sock.setsockopt(*opts) + + address = addrinfo[4] + err = None + while not err: + try: + sock.connect(address) + except socket.error as error: + sock.close() + error.remote_ip = str(address[0]) + try: + eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED, errno.ENETUNREACH) + except AttributeError: + eConnRefused = (errno.ECONNREFUSED, errno.ENETUNREACH) + if error.errno in eConnRefused: + err = error + continue + else: + raise error + else: + break + else: + continue + break + else: + if err: + raise err + + return sock + + +def _wrap_sni_socket(sock, sslopt, hostname, check_hostname): + context = sslopt.get('context', None) + if not context: + context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_TLS_CLIENT)) + + if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE: + cafile = sslopt.get('ca_certs', None) + capath = sslopt.get('ca_cert_path', None) + if cafile or capath: + context.load_verify_locations(cafile=cafile, capath=capath) + elif hasattr(context, 'load_default_certs'): + context.load_default_certs(ssl.Purpose.SERVER_AUTH) + if sslopt.get('certfile', None): + context.load_cert_chain( + sslopt['certfile'], + sslopt.get('keyfile', None), + sslopt.get('password', None), + ) + + # Python 3.10 switch to PROTOCOL_TLS_CLIENT defaults to "cert_reqs = ssl.CERT_REQUIRED" and "check_hostname = True" + # If both disabled, set check_hostname before verify_mode + # see https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153 + if sslopt.get('cert_reqs', ssl.CERT_NONE) == ssl.CERT_NONE and not sslopt.get('check_hostname', False): + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + else: + context.check_hostname = sslopt.get('check_hostname', True) + context.verify_mode = sslopt.get('cert_reqs', ssl.CERT_REQUIRED) + + if 'ciphers' in sslopt: + context.set_ciphers(sslopt['ciphers']) + if 'cert_chain' in sslopt: + certfile, keyfile, password = sslopt['cert_chain'] + context.load_cert_chain(certfile, keyfile, password) + if 'ecdh_curve' in sslopt: + context.set_ecdh_curve(sslopt['ecdh_curve']) + + return context.wrap_socket( + sock, + do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True), + suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True), + server_hostname=hostname, + ) + + +def _ssl_socket(sock, user_sslopt, hostname): + sslopt = dict(cert_reqs=ssl.CERT_REQUIRED) + sslopt.update(user_sslopt) + + certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE') + if certPath and os.path.isfile(certPath) \ + and user_sslopt.get('ca_certs', None) is None: + sslopt['ca_certs'] = certPath + elif certPath and os.path.isdir(certPath) \ + and user_sslopt.get('ca_cert_path', None) is None: + sslopt['ca_cert_path'] = certPath + + if sslopt.get('server_hostname', None): + hostname = sslopt['server_hostname'] + + check_hostname = sslopt.get('check_hostname', True) + sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname) + + return sock + + +def _tunnel(sock, host, port, auth): + debug("Connecting proxy...") + connect_header = "CONNECT %s:%d HTTP/1.1\r\n" % (host, port) + connect_header += "Host: %s:%d\r\n" % (host, port) + + # TODO: support digest auth. + if auth and auth[0]: + auth_str = auth[0] + if auth[1]: + auth_str += ":" + auth[1] + encoded_str = base64encode(auth_str.encode()).strip().decode().replace('\n', '') + connect_header += "Proxy-Authorization: Basic %s\r\n" % encoded_str + connect_header += "\r\n" + dump("request header", connect_header) + + send(sock, connect_header) + + try: + status, resp_headers, status_message = read_headers(sock) + except Exception as e: + raise WebSocketProxyException(str(e)) + + if status != 200: + raise WebSocketProxyException( + "failed CONNECT via proxy status: %r" % status) + + return sock + + +def read_headers(sock): + status = None + status_message = None + headers = {} + trace("--- response header ---") + + while True: + line = recv_line(sock) + line = line.decode('utf-8').strip() + if not line: + break + trace(line) + if not status: + + status_info = line.split(" ", 2) + status = int(status_info[1]) + if len(status_info) > 2: + status_message = status_info[2] + else: + kv = line.split(":", 1) + if len(kv) == 2: + key, value = kv + if key.lower() == "set-cookie" and headers.get("set-cookie"): + headers["set-cookie"] = headers.get("set-cookie") + "; " + value.strip() + else: + headers[key.lower()] = value.strip() + else: + raise WebSocketException("Invalid header") + + trace("-----------------------") + + return status, headers, status_message diff --git a/websocket/_logging.py b/websocket/_logging.py new file mode 100644 index 0000000..df690dc --- /dev/null +++ b/websocket/_logging.py @@ -0,0 +1,87 @@ +import logging + +""" +_logging.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + +_logger = logging.getLogger('websocket') +try: + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +_logger.addHandler(NullHandler()) + +_traceEnabled = False + +__all__ = ["enableTrace", "dump", "error", "warning", "debug", "trace", + "isEnabledForError", "isEnabledForDebug", "isEnabledForTrace"] + + +def enableTrace(traceable, handler=logging.StreamHandler()): + """ + Turn on/off the traceability. + + Parameters + ---------- + traceable: bool + If set to True, traceability is enabled. + """ + global _traceEnabled + _traceEnabled = traceable + if traceable: + _logger.addHandler(handler) + _logger.setLevel(logging.DEBUG) + + +def dump(title, message): + if _traceEnabled: + _logger.debug("--- " + title + " ---") + _logger.debug(message) + _logger.debug("-----------------------") + + +def error(msg): + _logger.error(msg) + + +def warning(msg): + _logger.warning(msg) + + +def debug(msg): + _logger.debug(msg) + + +def trace(msg): + if _traceEnabled: + _logger.debug(msg) + + +def isEnabledForError(): + return _logger.isEnabledFor(logging.ERROR) + + +def isEnabledForDebug(): + return _logger.isEnabledFor(logging.DEBUG) + + +def isEnabledForTrace(): + return _traceEnabled diff --git a/websocket/_socket.py b/websocket/_socket.py new file mode 100644 index 0000000..54e6399 --- /dev/null +++ b/websocket/_socket.py @@ -0,0 +1,179 @@ +import errno +import selectors +import socket + +from ._exceptions import * +from ._ssl_compat import * +from ._utils import * + +""" +_socket.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + +DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)] +if hasattr(socket, "SO_KEEPALIVE"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)) +if hasattr(socket, "TCP_KEEPIDLE"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30)) +if hasattr(socket, "TCP_KEEPINTVL"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10)) +if hasattr(socket, "TCP_KEEPCNT"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3)) + +_default_timeout = None + +__all__ = ["DEFAULT_SOCKET_OPTION", "sock_opt", "setdefaulttimeout", "getdefaulttimeout", + "recv", "recv_line", "send"] + + +class sock_opt: + + def __init__(self, sockopt, sslopt): + if sockopt is None: + sockopt = [] + if sslopt is None: + sslopt = {} + self.sockopt = sockopt + self.sslopt = sslopt + self.timeout = None + + +def setdefaulttimeout(timeout): + """ + Set the global timeout setting to connect. + + Parameters + ---------- + timeout: int or float + default socket timeout time (in seconds) + """ + global _default_timeout + _default_timeout = timeout + + +def getdefaulttimeout(): + """ + Get default timeout + + Returns + ---------- + _default_timeout: int or float + Return the global timeout setting (in seconds) to connect. + """ + return _default_timeout + + +def recv(sock, bufsize): + if not sock: + raise WebSocketConnectionClosedException("socket is already closed.") + + def _recv(): + try: + return sock.recv(bufsize) + except SSLWantReadError: + pass + except socket.error as exc: + error_code = extract_error_code(exc) + if error_code != errno.EAGAIN and error_code != errno.EWOULDBLOCK: + raise + + sel = selectors.DefaultSelector() + sel.register(sock, selectors.EVENT_READ) + + r = sel.select(sock.gettimeout()) + sel.close() + + if r: + return sock.recv(bufsize) + + try: + if sock.gettimeout() == 0: + bytes_ = sock.recv(bufsize) + else: + bytes_ = _recv() + except TimeoutError: + raise WebSocketTimeoutException("Connection timed out") + except socket.timeout as e: + message = extract_err_message(e) + raise WebSocketTimeoutException(message) + except SSLError as e: + message = extract_err_message(e) + if isinstance(message, str) and 'timed out' in message: + raise WebSocketTimeoutException(message) + else: + raise + + if not bytes_: + raise WebSocketConnectionClosedException( + "Connection to remote host was lost.") + + return bytes_ + + +def recv_line(sock): + line = [] + while True: + c = recv(sock, 1) + line.append(c) + if c == b'\n': + break + return b''.join(line) + + +def send(sock, data): + if isinstance(data, str): + data = data.encode('utf-8') + + if not sock: + raise WebSocketConnectionClosedException("socket is already closed.") + + def _send(): + try: + return sock.send(data) + except SSLWantWriteError: + pass + except socket.error as exc: + error_code = extract_error_code(exc) + if error_code is None: + raise + if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK: + raise + + sel = selectors.DefaultSelector() + sel.register(sock, selectors.EVENT_WRITE) + + w = sel.select(sock.gettimeout()) + sel.close() + + if w: + return sock.send(data) + + try: + if sock.gettimeout() == 0: + return sock.send(data) + else: + return _send() + except socket.timeout as e: + message = extract_err_message(e) + raise WebSocketTimeoutException(message) + except Exception as e: + message = extract_err_message(e) + if isinstance(message, str) and "timed out" in message: + raise WebSocketTimeoutException(message) + else: + raise diff --git a/websocket/_ssl_compat.py b/websocket/_ssl_compat.py new file mode 100644 index 0000000..e227840 --- /dev/null +++ b/websocket/_ssl_compat.py @@ -0,0 +1,39 @@ +""" +_ssl_compat.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" +__all__ = ["HAVE_SSL", "ssl", "SSLError", "SSLWantReadError", "SSLWantWriteError"] + +try: + import ssl + from ssl import SSLError + from ssl import SSLWantReadError + from ssl import SSLWantWriteError + HAVE_SSL = True +except ImportError: + # dummy class of SSLError for environment without ssl support + class SSLError(Exception): + pass + + class SSLWantReadError(Exception): + pass + + class SSLWantWriteError(Exception): + pass + + ssl = None + HAVE_SSL = False diff --git a/websocket/_url.py b/websocket/_url.py new file mode 100644 index 0000000..2d3d265 --- /dev/null +++ b/websocket/_url.py @@ -0,0 +1,172 @@ +import os +import socket +import struct + +from urllib.parse import unquote, urlparse + +""" +_url.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + +__all__ = ["parse_url", "get_proxy_info"] + + +def parse_url(url): + """ + parse url and the result is tuple of + (hostname, port, resource path and the flag of secure mode) + + Parameters + ---------- + url: str + url string. + """ + if ":" not in url: + raise ValueError("url is invalid") + + scheme, url = url.split(":", 1) + + parsed = urlparse(url, scheme="http") + if parsed.hostname: + hostname = parsed.hostname + else: + raise ValueError("hostname is invalid") + port = 0 + if parsed.port: + port = parsed.port + + is_secure = False + if scheme == "ws": + if not port: + port = 80 + elif scheme == "wss": + is_secure = True + if not port: + port = 443 + else: + raise ValueError("scheme %s is invalid" % scheme) + + if parsed.path: + resource = parsed.path + else: + resource = "/" + + if parsed.query: + resource += "?" + parsed.query + + return hostname, port, resource, is_secure + + +DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"] + + +def _is_ip_address(addr): + try: + socket.inet_aton(addr) + except socket.error: + return False + else: + return True + + +def _is_subnet_address(hostname): + try: + addr, netmask = hostname.split("/") + return _is_ip_address(addr) and 0 <= int(netmask) < 32 + except ValueError: + return False + + +def _is_address_in_network(ip, net): + ipaddr = struct.unpack('!I', socket.inet_aton(ip))[0] + netaddr, netmask = net.split('/') + netaddr = struct.unpack('!I', socket.inet_aton(netaddr))[0] + + netmask = (0xFFFFFFFF << (32 - int(netmask))) & 0xFFFFFFFF + return ipaddr & netmask == netaddr + + +def _is_no_proxy_host(hostname, no_proxy): + if not no_proxy: + v = os.environ.get("no_proxy", os.environ.get("NO_PROXY", "")).replace(" ", "") + if v: + no_proxy = v.split(",") + if not no_proxy: + no_proxy = DEFAULT_NO_PROXY_HOST + + if '*' in no_proxy: + return True + if hostname in no_proxy: + return True + if _is_ip_address(hostname): + return any([_is_address_in_network(hostname, subnet) for subnet in no_proxy if _is_subnet_address(subnet)]) + for domain in [domain for domain in no_proxy if domain.startswith('.')]: + if hostname.endswith(domain): + return True + return False + + +def get_proxy_info( + hostname, is_secure, proxy_host=None, proxy_port=0, proxy_auth=None, + no_proxy=None, proxy_type='http'): + """ + Try to retrieve proxy host and port from environment + if not provided in options. + Result is (proxy_host, proxy_port, proxy_auth). + proxy_auth is tuple of username and password + of proxy authentication information. + + Parameters + ---------- + hostname: str + Websocket server name. + is_secure: bool + Is the connection secure? (wss) looks for "https_proxy" in env + before falling back to "http_proxy" + proxy_host: str + http proxy host name. + http_proxy_port: str or int + http proxy port. + http_no_proxy: list + Whitelisted host names that don't use the proxy. + http_proxy_auth: tuple + HTTP proxy auth information. Tuple of username and password. Default is None. + proxy_type: str + Specify the proxy protocol (http, socks4, socks4a, socks5, socks5h). Default is "http". + Use socks4a or socks5h if you want to send DNS requests through the proxy. + """ + if _is_no_proxy_host(hostname, no_proxy): + return None, 0, None + + if proxy_host: + port = proxy_port + auth = proxy_auth + return proxy_host, port, auth + + env_keys = ["http_proxy"] + if is_secure: + env_keys.insert(0, "https_proxy") + + for key in env_keys: + value = os.environ.get(key, os.environ.get(key.upper(), "")).replace(" ", "") + if value: + proxy = urlparse(value) + auth = (unquote(proxy.username), unquote(proxy.password)) if proxy.username else None + return proxy.hostname, proxy.port, auth + + return None, 0, None diff --git a/websocket/_utils.py b/websocket/_utils.py new file mode 100644 index 0000000..fdcf345 --- /dev/null +++ b/websocket/_utils.py @@ -0,0 +1,104 @@ +""" +_url.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" +__all__ = ["NoLock", "validate_utf8", "extract_err_message", "extract_error_code"] + + +class NoLock: + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +try: + # If wsaccel is available we use compiled routines to validate UTF-8 + # strings. + from wsaccel.utf8validator import Utf8Validator + + def _validate_utf8(utfbytes): + return Utf8Validator().validate(utfbytes)[0] + +except ImportError: + # UTF-8 validator + # python implementation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + + _UTF8_ACCEPT = 0 + _UTF8_REJECT = 12 + + _UTF8D = [ + # The first part of the table maps bytes to character classes that + # to reduce the size of the transition table and create bitmasks. + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8, + + # The second part is a transition table that maps a combination + # of a state of the automaton and a character class to a state. + 0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12, + 12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12, + 12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12, + 12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12, + 12,36,12,12,12,12,12,12,12,12,12,12, ] + + def _decode(state, codep, ch): + tp = _UTF8D[ch] + + codep = (ch & 0x3f) | (codep << 6) if ( + state != _UTF8_ACCEPT) else (0xff >> tp) & ch + state = _UTF8D[256 + state + tp] + + return state, codep + + def _validate_utf8(utfbytes): + state = _UTF8_ACCEPT + codep = 0 + for i in utfbytes: + state, codep = _decode(state, codep, i) + if state == _UTF8_REJECT: + return False + + return True + + +def validate_utf8(utfbytes): + """ + validate utf8 byte string. + utfbytes: utf byte string to check. + return value: if valid utf8 string, return true. Otherwise, return false. + """ + return _validate_utf8(utfbytes) + + +def extract_err_message(exception): + if exception.args: + return exception.args[0] + else: + return None + + +def extract_error_code(exception): + if exception.args and len(exception.args) > 1: + return exception.args[0] if isinstance(exception.args[0], int) else None diff --git a/websocket/_wsdump.py b/websocket/_wsdump.py new file mode 100644 index 0000000..860ac34 --- /dev/null +++ b/websocket/_wsdump.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 + +""" +wsdump.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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 argparse +import code +import sys +import threading +import time +import ssl +import gzip +import zlib +from urllib.parse import urlparse + +import websocket + +try: + import readline +except ImportError: + pass + + +def get_encoding(): + encoding = getattr(sys.stdin, "encoding", "") + if not encoding: + return "utf-8" + else: + return encoding.lower() + + +OPCODE_DATA = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) +ENCODING = get_encoding() + + +class VAction(argparse.Action): + + def __call__(self, parser, args, values, option_string=None): + if values is None: + values = "1" + try: + values = int(values) + except ValueError: + values = values.count("v") + 1 + setattr(args, self.dest, values) + + +def parse_args(): + parser = argparse.ArgumentParser(description="WebSocket Simple Dump Tool") + parser.add_argument("url", metavar="ws_url", + help="websocket url. ex. ws://echo.websocket.events/") + parser.add_argument("-p", "--proxy", + help="proxy url. ex. http://127.0.0.1:8080") + parser.add_argument("-v", "--verbose", default=0, nargs='?', action=VAction, + dest="verbose", + help="set verbose mode. If set to 1, show opcode. " + "If set to 2, enable to trace websocket module") + parser.add_argument("-n", "--nocert", action='store_true', + help="Ignore invalid SSL cert") + parser.add_argument("-r", "--raw", action="store_true", + help="raw output") + parser.add_argument("-s", "--subprotocols", nargs='*', + help="Set subprotocols") + parser.add_argument("-o", "--origin", + help="Set origin") + parser.add_argument("--eof-wait", default=0, type=int, + help="wait time(second) after 'EOF' received.") + parser.add_argument("-t", "--text", + help="Send initial text") + parser.add_argument("--timings", action="store_true", + help="Print timings in seconds") + parser.add_argument("--headers", + help="Set custom headers. Use ',' as separator") + + return parser.parse_args() + + +class RawInput: + + def raw_input(self, prompt): + line = input(prompt) + + if ENCODING and ENCODING != "utf-8" and not isinstance(line, str): + line = line.decode(ENCODING).encode("utf-8") + elif isinstance(line, str): + line = line.encode("utf-8") + + return line + + +class InteractiveConsole(RawInput, code.InteractiveConsole): + + def write(self, data): + sys.stdout.write("\033[2K\033[E") + # sys.stdout.write("\n") + sys.stdout.write("\033[34m< " + data + "\033[39m") + sys.stdout.write("\n> ") + sys.stdout.flush() + + def read(self): + return self.raw_input("> ") + + +class NonInteractive(RawInput): + + def write(self, data): + sys.stdout.write(data) + sys.stdout.write("\n") + sys.stdout.flush() + + def read(self): + return self.raw_input("") + + +def main(): + start_time = time.time() + args = parse_args() + if args.verbose > 1: + websocket.enableTrace(True) + options = {} + if args.proxy: + p = urlparse(args.proxy) + options["http_proxy_host"] = p.hostname + options["http_proxy_port"] = p.port + if args.origin: + options["origin"] = args.origin + if args.subprotocols: + options["subprotocols"] = args.subprotocols + opts = {} + if args.nocert: + opts = {"cert_reqs": ssl.CERT_NONE, "check_hostname": False} + if args.headers: + options['header'] = list(map(str.strip, args.headers.split(','))) + ws = websocket.create_connection(args.url, sslopt=opts, **options) + if args.raw: + console = NonInteractive() + else: + console = InteractiveConsole() + print("Press Ctrl+C to quit") + + def recv(): + try: + frame = ws.recv_frame() + except websocket.WebSocketException: + return websocket.ABNF.OPCODE_CLOSE, None + if not frame: + raise websocket.WebSocketException("Not a valid frame %s" % frame) + elif frame.opcode in OPCODE_DATA: + return frame.opcode, frame.data + elif frame.opcode == websocket.ABNF.OPCODE_CLOSE: + ws.send_close() + return frame.opcode, None + elif frame.opcode == websocket.ABNF.OPCODE_PING: + ws.pong(frame.data) + return frame.opcode, frame.data + + return frame.opcode, frame.data + + def recv_ws(): + while True: + opcode, data = recv() + msg = None + if opcode == websocket.ABNF.OPCODE_TEXT and isinstance(data, bytes): + data = str(data, "utf-8") + if isinstance(data, bytes) and len(data) > 2 and data[:2] == b'\037\213': # gzip magick + try: + data = "[gzip] " + str(gzip.decompress(data), "utf-8") + except: + pass + elif isinstance(data, bytes): + try: + data = "[zlib] " + str(zlib.decompress(data, -zlib.MAX_WBITS), "utf-8") + except: + pass + + if isinstance(data, bytes): + data = repr(data) + + if args.verbose: + msg = "%s: %s" % (websocket.ABNF.OPCODE_MAP.get(opcode), data) + else: + msg = data + + if msg is not None: + if args.timings: + console.write(str(time.time() - start_time) + ": " + msg) + else: + console.write(msg) + + if opcode == websocket.ABNF.OPCODE_CLOSE: + break + + thread = threading.Thread(target=recv_ws) + thread.daemon = True + thread.start() + + if args.text: + ws.send(args.text) + + while True: + try: + message = console.read() + ws.send(message) + except KeyboardInterrupt: + return + except EOFError: + time.sleep(args.eof_wait) + return + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(e) diff --git a/websocket/data/WebSocketMain.swf b/websocket/data/WebSocketMain.swf new file mode 100644 index 0000000000000000000000000000000000000000..5eab4521a97f777caf2cda32b428dce037211640 GIT binary patch literal 180224 zcmV(*K;FMYS5pcQo&^AS+O)j~V3XDM0N(62oh)%1*Ha%tn{@X%$ZXR#X`7@|G-P}k zZB{dywmzRNOH`JE%C;!VQjn!6dn&SIqrjtVP(YS|2=YJod`UVe=tA*gpcT)rI0J^z%= z@Au>+CkKPUe!aC<@;(043)q2-@|nDLkKgWg z2}s^xbO-#;KlN0oSFUt(3r+yp+N3 z?g$YsBlQl0%Nj6PsTbsGtc&CkM}wbwL6jm)NrwNY2x%lp*>lOAXdNnn<$EDYmUN2s zT$Ge~>(;F*J`2{D3lE#I`GqL_@AHB6);_S_TlwXbQ40j6{3?Up6}7VG_DP&5sED$7 z4NgiJl@!%4YA82~nZ!?F3!iRc_Ghq~x%c$y**i8aJ|U6Ai|O9u-uoX&7Nw@8XJlq& z=WwGR{%4-FXh zEbJ()c~+rfx{sf4F%X^W2oHBje_Mcz#ae>=7R> zV~-uNdNsd!_QanP_st))nLF>-u|Gx6|LW$g#1-={eINbNN2?BS#~wX=CUMjo#~1Ra zjrm~<`^fj#zDQhkV$h(t?Tdb2z??mL|CWSZNd-Ikz*2jN$b=!)B6O-N; z&D=Bkz=XIrChht(@$9Tqv)My;jNZn-_U($3i9Zfqwv_wUgjuJf*IYbxE3x_Pw@afx zTJzf(?uhf_eob7r>yJ~Q##@YO3(wBx zzB^&zwdk9tR^Ce7zVw~XqPN|cc9Yw>W#NUyal?j==TG~%DZ;8 z9NNQtXU3!h2@@{8xr;Mx@5w9NU6bceWbgQF<3tYdz%ADJLo>&*-+A-ko4ox)_J7a1 z@bS8BoaQe_j$^ENd&4y5+CS#c%*05A0lX zob%H=C%)os-!LoV&O-MwsDsZa4sGCdNudl&=u2p zzixPMJ@4e^89#6@o>+R6b!+<9!#Q&nj=sT|Fmdo9P7`noJWO~PO-=oqngmTvNli`B zq-gk%=pp23B5(GP;iMEE@*WP2NDX@kl(aNSnwlgnP5oP-(U9T&hc^xH-!!DDKePqd z;Y}n3C8U%L0i{scyb1l`DVyBLB45F3*Z3Hpr$bdJ1il82x=`*GzhT7 zG!|kt0*+yi2~B1Yb4*k7AV@VgkrX^wj2V)`609G@gdQFpG^R-a52I-erXL-Ilz@kn zBW;=PjsE*qPoKm+J^A2UywRt3JNBLVW*l$d#CaolQ@-2r6L0LQjeD8r z-yXDCm$Eyw)Ynb2vx7_~;wXs+lbxa(})!b1vu0X`k(8Zr%RI zTa2+gHy!28zP4f#_uyMI4l=JCZEfKlm^$__w|U0K?Yyfa_N-%$e`omt*1St|f8vf^ z+cKZKd2H(_ZtK2_YuO8$7W~R-zBzp|^U|TkuMT?i^YhH_TR+~)UGu|)QOv_j7A)cJ zSiNBf_ssjtu5yoEJvE6je)pNHyeos2ZDFlgFzZY1qLnM=u-9&S_Y7~v^3M;jK78|Z z6Z_NML%-x4*|qfwd)T>~^H~>H{Bek}e8`z`?CEpYu3$}S{&pLC{>znjSl?Y) zKY=r7<>2$Ixt~qh#aMgmkG0G%ww?K&JN}PTZ!pgMd~q*(|EJ%cV~&}3?ht$Rr)NIk zHjh8JkhN&#x;7E8xH|LH zaOSYz_AckOjJ^IN@AJvy82>%7VK8%PbIVxfywyjSb0(b~^D%SEN2BI&W^eg(E9cEO zhHl_}^wn>Jnd>jT-+R`Fd&hB4Eod3VSUqmbCHD6_cI@NrXj=CZI8@qZ+(+%v6UP7DKY2@z?_a{Xb#hSBg44T?v-Vvc z`5E)dq3OqY&7*dIz--?1!CuyZ#c!`*jT*M~_ zcqfK^GM+np!QL^f_b&@@q-N-q2ZP$6m!rj-lb0%-SwS;$lgSJdh@Z3jMj^9Ze*->x$C)~|FL{8 zbI743`#Ceu44cI`zh=sJ%rUcXjNpFp!-A=dsW*1s_w~9pN7*xe96p#kZ~14_x!)hY zx`pw>m7iyG7c81Kn7#1y?8B_xJO3ELn)%0X?{E)&`sHXw^Cv9}8MDUhsh#lKku|(e z-rTd2bM)AoKQi7Q`Q{$RuGS?JI5Ur&Kg<2~_`Gj9Ki``4DeK2sGk@lc`)2hRcI%95 z&CD;?FP+Dpa%I_U?$9-hMzMcbH}ni^!{$W?IBPfVy2g9=^ygc-Z;yL-8GGpQ?-y~J z{ydj|`N&%zGnyt3pUj*+@vSwSpMTkFTyg5Fvy8*7HzzRPJ-lfWYyZ{Zhq*tVyLN&% zInUv%pm?!F7x-sDZa@bPKZ$+g#J zF|MxOHJp8Q*4tn3em*sL0_&?;ho-RSt!-}N%sq4R0^{=3q0<;suI)X;-nn$=8@%=Z zZCcD7I`7IuKkT1>lQ-n8xuaOCFJ7L<`Q-fSHO$L9$BgIvG;%@7k(uW=aHlsfUBx{& zY1ne+M>lW%&i?ZAoqKs}c6~OOy?EL6H<)iv*|?8&^|u-47<;~0w4XhH^T@@_%a@n# zWL@3%#v1NtpEu8De{*^BY|a~#Mh#&u+cxcU&hMMn{mz~y<*I8_Vi0r2C+XlFzpQImyd70%R2V+y5a0a=g-bzwl107%$>Avzq+Pau0uUeFE#q`(u7&%{=$^S>};3mp|tIy5ZB0IIA~I(abrxd?)Lx zeUpFTE}i+oQs&z~t^AJr$I5dHxzm3-_ZDa6rR@iJzkL4deBQ!?$G&FG*#FiV?&b-* z$1)}#S-P6{)sT%pa8Fzvmj1(rvxnH@TRz*#xW0eRD8{}&-apOy@w4F{v4_q0u6XO# z<)3i>d}Hhk?zO9j2XnWq8rj4ha^U@Mc!!oXx3X5uYhKQqFm%Ff&gj!eFS6HOnly>? z^}KdQeC~*wYl=Qubz&EL%#EeL zFs58TwwrNivea+eN=i)!v&CAv|ab~nka<3lo=hwW&BU?stXU}hD z9l3sC7;EqHiK7`ucO9L;9JloCshl^z``{XP_~7&V*~`a$w~00V+QwtNua|!LE^pCS zL-#NiZ@QSf?!!fA+5i3T+KY^3pRPN?Jvn;spWJm*hJMA_dTaK4)~Mf>F5$K|Hw|Wg zHTmRr)})`t?cq$_w)!|{&AAab+3Q<|jbtu5_~Ul&yggtOzZ*JZHuv0xmE$=#mwmN} zy0#=Z|-BZvMDtIeYWGv#Xgi z4{ZL9_s-xOYk0f%zc-Wd$(o@LEk8WA&qwoCA7f40fAdGiZ)-0uv z2A$q_|2yZ_uVHSV+wv=W)YofQvyOc6+cMU%$#1;LS#V>;H@wrI{=R|Tv}Ich_vZd_ zo4F^}OwK#l{J}@uZOa#~<;;C&@L|TmU;jABSvB#aYnZToI1bN=ex!@0{hjr)kT>EhR;IA4w3^gDCos5QG7vxl9$@0%-oK4edR`}hjx z>}!+eF)m#D_TSsL|NI`~c+19NjHO>5UBpsjabPo2$NzjnoPUhAsegPC7knzNht z=dKec*$0Mv{5@mDd*^x|+dVSj^Y5o$V;x<&avA5u@sWF2Z_K{5koCbgYX`I5nccdE zJLAI>Gq_QMIZ++{%ji$P1);YH-h$f2zn>LHMXmbqrQS#39reP1##8WaniM>Gn3U&K z6rLvKqjhJ0Ci$PQ+>7uBzs@9Zt-m$TI|uK9YofWw;Vo#{G4od}pK2rR1$DpNBKcKU zC-x!vfue+kn6G_$+c9|e|7_h_(mu*|Pc-ST|8M&;NdGOdN7j?^1V232^A}Rz@cQ*L zq}}ya3y%B6w27o$i{Q2A$hc9jU!Fqx?f;JQ6EaT!r*m;!!4EH-YbEnx%veV9O?d}D zAmc_k?0a$CQCIh#B=EPM(BZgAC#K-Iyootv+?MBj`^mU1o8Nj5>5})vP|~i|Kg5gk z<@w*mao?SAn2g&p`p=sBTG(@djC?SLzC`&?h%sz>1`9vQ~O&K!E4ovdmbSDx9)0sm8@G+zyG8X`~+Ff zu}G)$-(4l``~UNM9;t8Hl7;IN^}9KW^xt|d`4=+&!2d7~AU&VHxCqzx$;vF!Z_60D znv5&xd9$40J#b?o3){PDl>~lZ+FNIFJl4x1(ynQpU?+LEe6gXK%&&==I05G`ZXJ*7 zaqCcT(%---@o$m-TK|)F2bGM%MZUfFhAqZS){(H+vfw3AK6Jv>?bNI3g7G~ zI?s;eqeAr|xNx3^>t9-0O7I?dzhM#P*-tf-{sr%Bc@ydWG& zz4fufGYOppOMj9e-li9mQ6Bt{{sZT`_{swWzt$@+v2flu(;p=Lv_!vKgzI`~^pnUZ zQ~P>JKdS~Enoq_bxa*O($@r`0S>Gk~t6rWZL3(Et6d*lk+&_%;JCL364arA6o}7pH zJY;=<^c(g5zqTPBx!YNY-?h0%$oQ@1XFkRCTrvH7vi?op#rb63O~c-vfa|?`%s+A7 zgVwx;_^f$j6X|!A>7ob6Q|!HtbYk870Pzr|n+ZR*%z67fp_Aatxhcp`1J?~fd93}| zf$}nbw-@Clcldu$&Z35l!THr(T1)!vKeq2dl(UCx8UH$*4#&Z6D za*=)?Kl=j8=XOIL=})lshbd&fQA^LolX(qHcwz~zuTsJw;|{F4X(0U#bU)q$<>9{% z;CTI6-@lIVZ#=UN<*E1Nvt+zgSDr~nIsT-(1?6Vu(_;u71k?8Zg7k`NJb-xU?HfqH zt^B}!NRN{>VjS<)*KOJ}#>{LXl$ zWB%$}n~<-^T~Lv62mb5q5tQeLOJ}0|Rh$@s>oE7{H?hC_3K|ffn#(Ve`9!5LA4Wb1 zreJ@9(f`gz`Ti{W5SeFF=Ef4zPyd%z{0ry5!94-#d2sxJ(^!7rG1T9Ob|xa%e%3y>9qrAHnD?;V zJ^%g)=lQ_mQtYpFZg=D}c-y{@l22 zEt$uv$1@oym+t&NNO#vNGwOw}kK2$>9;1++ttWq8jq7`$ek{_vY3p++zatN7NPEF_ z$7YnHj2Ew>zWt=>1dh9T_7})6moKCuKRr!-iEvL&b0MFudtxxk^`2dyBVY91JcW$6 zs_@I3XfH>VyodBTkXnXtw#Q;oo^pAEY5RBb1;npOn?uT59_HRe_$71Ls87rfjY54g z@U2TozY|fk9+)x{^+v1WB_7VRnZZXr-8z_s>-X{0)da33^ND7Hr{ML+P_GX>mp6>i zxoN}Pdr%K8di52=_vsN&BRx;vI|0Z4?&DFYUl!h%OYm&{p#N&5bNYq%P`@5k??kz0 z8bsvXe@&nk>2mz!1eCKcCY(okEWd>GZkq3Y2KCJwFOSCe%8xzRu5tVbT<7S)M^S%U ztTS;Pua8Q{^*LLA8TsVkjS`&K>o5L^_#ZFdi*h&Db^!ItmA)@yzpFQ`M!hlZ=yN!) zob5d12j@M5P=D>6wg%Us=;g_%uikoNCEBlDr(Z#Oml==Xd&|lI%GDWD1?t5G!-FV) zNe$@d2(F(<#`ReK68bMKy0^C=U8e2&5&3k+MGE!CxtWu&p9PtyUt1d-{gF@Jt(i;O z4_v)-A@b##mjYxxn)Z&N+rL-y9^yaJ^9ssQrjw0$KAg!$el#^#lX^jq%fFEMtxCSM zg3PCXj|*J%JLXb1P(NN@dxPfR{rgG2_4Rt%KD=!B632V+nLm(EKYIxMnt`L<*oNcZ zcs~y5`C4fJ`SQq{PF(Nn_Db}RhQD$O=UrER7U!3`jfLyJWY!Nz@3Xcy3BCHipgV)| z?@xLM``>jX3)}U0=3~@P-?uYGoR429~eo5z7;rRSN9zuM6OY2M8 z_21Vt4fXy@4Gf&WeDrt7Z(}~SVgIe4eS_mQzKU`hWjp0Sy)t6h5?uddEBhip&Ej1_ z{de4V0Qr1H0QoXXHRmZ@uZN!=gZ#F3$VQZ}e>^!7=}L{9ih3lz|2Wh~qu9-;?`IA_ zhWa!{`uysXwR>h4`TZpGiAvCT5$u~hm~8K|0Oa(H-euu<$YT51)PVJ+x<*f6;Q3uUwCQT=G9S$Y)UxtwjAe$JiU~ z_pHa3VEObdt&&bpniHRr3&rX6MOgKe8svi zQJxoy_hLWqh_|8~MHewq4}5s>OQi3LslOwCJu>T8l=tZ?ePp~_4A^BA_jKpD|bOztxX^{VCo8};^+8!)~YXq9h4J@;JxMU>};tO2P1 zM^1SZ`9S{KVx&{-?qcjucoXB8R{of;kp4;69!9-n`{G4hrz@;)ah?+noj|>AQQ44g zuHC5rTT`Nw5Z`sZmLp!9IT#=GU$pWiq{qoAqmciXWL`k~yR{bWK}-IMQP}SMQWMJM zHt|T5=VcfBpdYYc_C)jxHohc4{+k?l6y@NX{vPxPGLHU=^K1TBFWO(p8-{UUt27qn zN&Mdwq+9VH?<0Titb7jRsMmjJ#dTRy7KM6lNH;ysyD7$j@+$w8iFhCQ;$d8;4U!66 z@9$qje`?kK_c0z3EE@ef^3mMK(I0LtaG`x@dStH;={E1J*{I(p&f_7S4?XuR;ydZ1 zVjP$I;}TNdq)=Q!zf0mNM7{a-E3f1FoKV*zADxnUP~L-MGm(#W3O_>rG@d(*_>BG- zSDw`k=^9w|G19GP)l{@^nQ`^Vcgnu6A>8bbZeV=T_;w-k!GPS8Xun?m z>}RCI^_$1BpFduqZ-)RTusEJQq4to$e1&ku6T(XW2-B{9m^vu~k3Z7o=U`b!{s z#Ek9l)f6FLG;FiteuewMJ?QVue2PN;Vy-SlI?BHM9OZF!o)7JNa{q~_KZ{h0P!D}G z;eFKOAB-u)eoEJThJM(okteZy#J{yjkE7~+bbP%T?M7?0@s~5`k4qBhIL!Ss)(?%! zK)WR6EkwN*vug|5y+5oMk$z7WS#jN-<8{MztGvoW|1og_(s$Jh*T>_yS&y|MzgP4^ z{nmfs)i)83A76R^`6~CNX~@5u|FPo~t;bg)-u5T&LA#f+=@R1k;>T6E9`a)sF;1C2 z$cui4W5EEl6H|8XLAt&jSBmoYi(nt>3-RXzas7t-2O_;zk9-L8pI$tQdiW*p{WzbY zTtCW<^o4%7Zcl9e3hDpS1{cb~g@q^3|2zJq58It`u0}hMKWP@?nO(FQ=ecFu4AfUG zgFiufR$PAv`BrN_MAQ2}(^2oPQ*S`Jeaa0WA3U2h73HcyeLwDFXg2RaJ&^NECi45$ zeftn^U(tUMpV#x#QUBlg%8c?VYwC~l{k7po{di2GAA4Izy|Ka_(&buaULVdIILFC*1|JZyQ`zyGCevcsYSCq4;{R;(XN1xhs z7~>QR;~LuC%gRG1#S6?Q^7$|q zOLq|P+a10bNrRuF2cb*NcAv)qIT`(?5#j6XPKxd-S`l!W$mtfYI^Z-?-g``LXFprO z>oB5lkKf(T>oagQT5$o@(7TIT6Xo^aSCO8QEro>LLQVovQn$lFVYi8$YP#y* z4|wYewNkgsVz=^@GK1fsba?{)SWFfL{E&_Y03-k=2<9>L1YMM$OB0jptncUbnD|N! z<@2~*X3EPibQ|EMbXnXnur|Phou?8@S@6V40*)HGh3~;GfPD>4Cj|^+;wfn;juhtMW5ZM_f;u@51$67# zzA0JSelU?3rP${Z>KZ&$tX?nHrt0L?Sp3`p(vvX#XKGe@a%o2huEHarR}8b_H2 z2u5qE+5qJ;QCwvif#Fr?C*#20wI_3N;QS0 zkVpy_I zw#V&-Kd|InH{u-a@w)wPliR^H1}qjB#0yO(mh>nIu* z31xA6sctSe2q}#xOv+`Y66`L!Umclbtd(+6UZ@4GiEEcmC9?`6L=%8tLpA8!L>Kw^ zLBd{`j3FptDvqQ|b&60iiWvxFj5*-+U>OfM!vsjhDpeJ#LRxF}2$kiuD~ly4+F@#q zl$mSv+RaufS|OHTB9YGLg!6F@H!wMo9+knvm)TLy4Bmz$ELXaGeglXH)X@cD#RvsG z2(|oFxI}6I?&@g@`223Cmh#fWdwl@N^->n{a#8hurHluH4U2Dy(-SPEHVPKQ%=fz8 z{*W4`5mXzTR5!hsvf6!q$}4p{Jr2rGaqTV>v;{T z-t0C7$Qe(*5n9lNNhUI^vW-eT41s!Sr?NP6Na9FYj4wQ;SThw#-(xVFr5$FB2RP@Ql=>ZmMJqoYeDb{v}0v}xfwy8ns5^MQ1j0Wi04U%?O zox3K|Z(^iGMfq)RbC1vv@_OKVKZS$z0GQBfiM2HKeZAYt$TFM56){jw>ry`LJrM2J zBccvFmN~)F8$fYcf&GcLi=)jNt@;FA0OWK4=-T4uL@X{BI0hs)*3@RD`Ct?3LXvK3 zXZ_-!FAs4%fF!N*?msJMLqpq;1sdL(a!Gs9>78^<*;a>9c6A` z*Bdml)8j7%Zd16uVv~vT_(?raD%OH!qvBB@0U6Bk>Oj~4fo^00<*Ifa^&(THPZxt* z05;rRZ>N0m8iiDpCCsFi;p1JxO29ERn^A2m<1te{c;o1a}YFqj4 zIBC$7L|?&3paT*Kgu@Q#(moT(gQ9?yqVwDUs4%U#8{r?2mom^HmIgA{dqXAdR*|mk zVU5^C7!s|LuNN`>x6SO?J2Uw0z-+?hEHF=eV}uNRQ-lUQsL_JD>jrNX5o%C%g_O$* z&V#ayRKz*kQ7D>texx+Eom>D!8(rc|?T7-*(ngV_ko2g-QWM`^YJ7>E#3dSLgf3Vt zU?#6Op@HrZ2_%p{v|CL4V;3j`>Mnv$10;nVA$>@qczRkD@bt7E;p%;)-yWbUJJYe7 z9!k7qYQ)VTP)W*8%hf#$QcgSo+W*mez%v3x6=~HyT+#;AT@RBp`)r09s@Tv#TuK-% zQlz242Qd4h^>$E_l))LTchd=iEOBG>Za*w$JAO*^P#y9KG=$PjPc~lfY#_CCWh~^v zH5{0|*B?*ZLvfwK?l2e~RGi*pAOxrVMGv?|A&er#46B9&tSoYc>U&3K5C)*-gmh0Dpd;1^HssQ9u#Qoz6?`~%KL|aE3IaHm zN|t_aR~0!>aTldUYdWH%%yxq-QSU}iO71ebiT+3oD+Ho-d=M1reGzz~>=KM5u87ES zOvqM4*g*G}(6KwP;V>jF1g0>Cqmvk!arGD<68j%RiYvPl`o z;t2R`u^0}~n?}*$&pw<_T9+asVMcM^j=8@MS+7+JY| zXfy~^V*3K!nRmU(J0Zu9O4v>N$4;E6A`Rv@KAt5^DW|q z$#!_q?P1vXZ~_Cr?!-;+$cYfvwz-78FbE9qF8TW5_JS`->8G5`#p- z?nb-AVIV>~&%3|T03@#ak z4Mi*6ioD#1msA+8XQ#F8K&ypUY;%rrcktUQ-C9MSqvWwbt28||qv`Gg*WnU9Hp z3zA%~AqcN%cws);@22A-HrXbKHoJ?-EkF|=5_Gg8iispyK-GJ@+Cs~LipTGuW7_ZY zacQ5BjeC14vDEj#9U76khwiW`n1efSggm&PiJl^4@k|@yMNe z3e`sg9rQkEz$QB(Y$z2zc$X$Zme=iITKvo!e_T8JK^aTyeeAoF0P-u&%-wvhRXJM~71^v(XUk1X>{^CJ5vMcOn{? zplDMFoMm#lm=+7uYGvANOuPN5J4IWa9beiav@d$*u0eQvNWN5AoF~`lfvPc~q_T`_ zuXDQ1R1z>7?F$f(9Ks7WnL5`;)#@E?eA(=e+{NoB@d2B-vv&g`8ZoJ}=)~YR1)}Nh z?_8A=sk-B8(h&~_g9(nZSfkG6p!H;Xz}Rp+$j=^+-^(TI4Y7_P0HO}bURrJ8-;ejY z1V-*8T*oK0b6_aPAwnd?zoMu9rsC%5i8BS>0{H*XH=B4LY*G|$_t6nr93&#)S~ptJ zb?`BvqPh#ZGxiS8BJP z-o}lkGh&NB6yHa-XA&Yl6f(k-f8}W*R5$vgN>1BH6)spvF!mga0scPF`Ljw++h-Mq z|B2A0@34S{2Xi}<+kyEQ2A2s}11z(Pn#{hH^r}?y8g7t#&`xj$hzdp!hg;aJl^~K#}bG zyC{J1myyZD5Wf>sd>?Uy=t7e%;Gz{Fu!TC{1OjLbW_vxNjj33_JMt}$O92WFAIoWX zv7Cl_LV}>(2Pkpf9zDnw3my(6-Cm~KOUmlqUKTttbTtHfTzCOjupk-ZBXcdJEPg(w zG2@bZ!>>jRt!9y#|v%I!sS45YcWXzAgDyL%u}N1!5BHgow8198&>D9R-`BIOEbs6@N3*y}bD=N|;hNyR$t1>Sw%`&*Iorwuw-&g*OgGgCB{1=?e>o8^6t0GB3Fv9 z`);qvp=(&ij5Lmte0IEx`vJAQ&<@)j{R9HUCZcs`qbQcf*uhG9iRTjIhR!e*jVVxH z(O3xyzAxg@JbJfppE=}PB?}ZV2R%CyMMWG)F6eQnef+NNX_zPOJ_hZjpsVp${rw%7 z(05b7mqZS1V%eDXIxhLhqby@8%Xma#m!(mmxA~zvnM1UIL{Ark`6E+i6K^G6Nyo1= z)@-pxn)cwGdLWeyJENMRW#v)zw|y(lfx_-UqQB{c+zts^#`JKPyd3!gO=d~n3D zz~P8gJbYWz4%IzWZkONcc2eGkNZAv&HR%F~>GZIi9tS)&X28jE`b9~2@WF1n3)Y<> zc%?ciIlJ;;m$VL55)YRmnkbZKfi=7L4qWNyg}jiidkj@_A(RrOXFPnDNDHbNHa7Uq z4xUsZC5qzi$|(fIlPV;C1DFQ-=iSv)`!gS0nOqC6yDKyrh6{2@UJ!}Ht9zKq%iZ1@ zU`!~22;N7&gYt_04z3}fIGq;^Ger;a%-wZwJ4halj41R@<5ttN5d`6i|HewwFfkAd(W;uM z_Okro6w?tqsiIv$chDCH&aF0_iFexTAuf@+9Ra7ymjqR+_64zIhYWl}(nFlqP7W^6 zM)K|p-X4-o;@W6n@GO#h@~^}Ip?GrX-(bYQ1r$02#aEZA^qQhlwM@&XL1`DI-BsE} zX}zrBFPtQ{A|DjUSK{~%r98CpUOyjCEjS#dn!*^nFAstuNb%{E50bI;*#!K2l<1{A z4ugpb=VQZp(t9YabJtKVUkrr0J~@dDdI4UgF8Z(p;XJy&n^vwVlWX*9u}ZGj6&1+U z@vqWfAp5xhx!1z=#YGxjr+RW^>b200xGuEob`Kab%Ih*XiaHec3KwIf5-v^f;Y;VH zfjgk0k>|=QxEjhTulIDo0W0{@BDGpB)hX4v?4n}1nkOwR(n5wy-f~%Y`uqh*4LJjY z=PtTopxVA9R;gC%l;T2Vm0Z@1K4jDGv`eC{vRJMcL#JAuSS^)@^NLb63|pk8^ZAi{ z0$r~zQdg>qO11y&@|Vm+DJdEFD`v^g`s6T4crZ0B^bniYPuMS|pD-gOGlLC;adsnr zkYMuAud>D2edRW?@#ul|{o8$Z;$Ps$in@ZUpXh z@Y|BHdSB2|Mt{@gwe9l(?#f(skw&hUs`VO3(> zSf02*o)9S#7eYS|M6yx<$pvKSoiA;%TzX#{XmN2N8INEt6YIn=uzotZUM(;07Ea|B z!MYRPiKR<4an6~%Qc?NPZ5X#7t$SOo0>nX^CztiilUL{! zMH*ORePL0#Tq6~0 z5rk+jNChd3$3z$ei$%~Koq(?B1auQm-UuA zcAdAYy&atH`o2iloq_SQDxlLw!=!LYn8*l1mKLH4db=|o!hWL2C3bQSwYRQNiyB#9 zuGHnxDe{M$deW1bwiT;n>0Ok9wBOsIR3lB(3q@H3yz2^RabJfDrA#i?Lu_(;U*MmI zD<0}lkxFXhGI_Z60a7c223qZHEd`zH?$o%eVFjJ*IyZih29S3-^rN>`-m!UCgFkv( z64P=<9xd>rb$+EO9DTO(BF{R zL@G%evcrmP#L}2Nu{KYy1<#ZP*l_7-bLrXfLX+TyaOH(iir4DEk5Pr3qBu!mk+dL` z;SwH8OW}oBjjxf45N9X~F<2bDQ^ULz1DUiGl8O#XIxnn`qllKsq>$hlHB@zXfyg>|RRoIYg!hyqQb&;7QY?uf%X6MFh|TK9k} ze62!HpKzuUj0Usbi-)50VC%buqG|AL^%jHO0ls~x(hX6J1>*Ex0MT|d6M+I=s$1l& zT(~5WblmO$bnk%67w{01Np&di(Jl>$kOSf74rPF`-P@@w^t;5}$oB@T!O`Zk^FuXy zlOdj1f1giprd)P%ARo{+LvQ3`YM~J4 zlq8oMaCK2uAS<5KV#*{;Ss7EN;z*QwMKM!R##9KI3K6?Xt|?-H-^p?r>tcX>!OJETI zr~h5xw0p^^hQAA)9#8NP7v>gepntj7AD|CB16Xu`=$01LQ{fOMl86`DiWVTyvzeENS{1y zsis{%Elyfgs@7>L+XH39gKmJhSJ$C5lOGC?+KRHe6=`zCYGoB2O_7>tx<;514^{e7 zh@d+Z3pqNat}v1namlATm;au~_J5H@4em@Rp6zBQH?zQ34iNFh?*&1x!2=#w969>~ zyp30i;`B75V>Gv7 z-f-!2H+acJE9R3=uJo=2xm$>eDYl(^m(bD4j3-$w#LQw{sYcF~S@dMvl6>R^{~mmN zD@n3ECL!1F_IuraNQzk6Vj(=_oeEDHJn8Ucz>^727PFw3RiI6QM+lDyo>VTq8^x!U zJ+T)&t)!?zuPtB~>$O!(wUMLO8r}8LdZnw5Bx3ayeh^ksyL2i^^FrmJcXsG43B6^Z zx16KbnA|QlHVJjfZ|jlY)*+WJrC-_fOG3Y7^h?erT}5L|0mO-Q*6YLSDb{F@qeGNW8<&4Ece=eZC!xsZ- zy8oUXuEXZgmmZ(!SlHo?EIC+hu)8SlBOQUrCjsKKgWaP6AGk{=4|bShr^}7K+O7f9 z){VaVTkmwxCs%<2?3}E$R2C#+{cil`Erl>T-h($R1r{)PIfCf$nU`+z5H%1(i@`+2 z1;Yarx~<)5*(ud1$xRNoiwyNr1cR3aY;plKA<_bm0d>1=viEdNqr?I2cNPb{#;~@P z5F-jD$vmPk>WeHfxVc5djqx%g2jW$=LCx%!%mpXPEB15gBRaA8Rh4eQ3$a5qdHW#2 z%z^*s@IsrRF?cx$Z+a4nF#2&U28WO00G@V#cUo1npY@ZkJUt-T=b4-V$pey~e(99~ z$vH3gdFJVyrw5q(JoC(pPY+1$_s?fue1d~_GVaq>s&(QDy^=c_F@lH_5vv6`mg{ieii0~*ywFBxw6ciwU!+OU z%B6rP=x)q%eSmC^K%i8Yfkv0Fb7jmhPyAQawZyN@nC4^9u z)@6+dYSN*G^=igz5LDL&D4$;%^m z*4|4A{5C2K3M}CbxLl+&a6bhKq8bFWs0-SOG=T$~My`Z88U)DV=A_O;bn}Mpltl=U zK*XUzV8;8mIf8rW1S~t{b3Nr3AUo#>*n&r&7qHzP%5@((2Pkj`00y~-p5TD`loUP; zO@YLl0o)Vt*?1x6yLY%;KErHw8fwe3G zL-=Mxzkh^?*in#ZVI-(I?LM-4z3F*&=8&hjPZ1t@it*Gv9qANC?Y;<(om+Ol9XQ$p zoI;2m>ubAL9!ESb;-g7HHOHtxzk-G5*BKP;{i{cyz z?A%`Fvw}xD9`DbgPolORvG3`nEI?k{v-D+ma_nF1e;OJQ){pod;SXn&ncU(4G4#_v ztMPEecgV>J-xH?sr|;QC2+j-L8ji)=&h*9X_zt&?V?$Ss$u;8y`WmrVOMl2p3TMMN zjgvZGA@14L{b3INf(N+dD!GPDu7JlIr3P0n@oOQr^zj{$AHIko9+{W)-h(DXhfAew zpWp3afxhEnQ0n8L!}37*%Di5G+t)K*g{qLWX2Clec#`<`J{}}Al*Jbl0F!M2;Wcyc zBq#YuS`vVU76@wbFbA0h;|UJ=H-{SH=zr|TNRd8z;74Kn78Y?}*yJ92ObtbOAby43 zP2nE+k$dUh7=r`d9zVr(8><0{M4!zaaG3SsX7ToLcZ3{VIu}EFMuhqpe1fpG1jTD0 z3~i)9FZp~#aA7TpngXB(P(ST&JGsee#rV(_$54*m0m5rP;7b!x!@I)85~^DnlK%{C32;n7jWTG0CIkY@R&Q`XB(hh zT-etZhchm-4++SD9+fikqZ%HvG=P<#M>6CZKj~K~W0M!3o;d-*-x|RWwG30VJ@mT^ z93N1YVpqHEE=CL^p226tF$cWL9PnCFD8%vVH@$V1khwg+IwV10=$r5(4IVqX@3ekF=q@Dl4~uz`M< zElkf&j$Jn-!I12p6|I62W4Pha;H&HEF{OX%`nVn{)U^YP%R2=LSeevlnM+;J(tfJu9pa*$U9i zZS6w0cR~=IdPuMtd__T5Sld0>xx)}Rar?<6iP(ZgFLgyk7vO-jpuP`>@dzxUz zO#Pn!i(9K`*V|8*;4aG(kW(zaemoz&&j^-`?EkWWaUM{+1$ctpZwDb4(6@@96)4Jn zPj#wm+o%-K!V-deR_%86B_GUabOeBp;7$nj>wO#R&_AFQho`hdBHi<)0j~@gFre=f z{azaI^1q*c=EXj*zVZ0NjjaFA$R$ftAMFUuE_Uamk)8+QmdVT&t zU9i5Pv8|In$q7XrZjJInpUAm#yi>}TD0B64wM;2ibLkuPOrt>~U; zv_Y%3JiWTGS{_hIgSld1sXR+suX2=%%Bsy3`3|K*?J&8j945O&R#lOoVkocjREiYv z&JHN$3ZXgIR)-yxJpcTIcll1s2U;klYME3jHpr!Nae=?em0qN=q*G<0tSqO*L8Y3+ zL0L&#PWlozVRHMx8-K~7>*Nz2Mq^-7tTq9^OtDTMjD60JmD zS)s8Jd@EDS8VnVh^g^j5-{@2YRNAy)p}11U7Xv8@6cnzmJMcSS%XqeW~uR1 zIUUu|i^nKRvzAnpr5JLvu_u)ztwN@&Nr74D4Dv*3}4$ z%G?o*yTX?*{=Z<{6~27&-@;g#YgCC-a;4hZT&>cWDl3so#3iL-aawLZP*Nt9*h=#I zi1Q1JC|_>C+L&&rbvtEc89|4*qEh8GWE2(3EJYcOWocE45?OksMqFxgCRYcm?RK|P z$rq>C47qAyg~z9rOT86UZfliW;jXF6*EW@TU8RVSRUaKzPKHi$ShWgBef#a<_k0g)2hAJ%sfS|(^jJisEhO5g(b2QX{EH- z>NKTk3K~<&ydrf*c`9FASyCh_udnhHWmUN6A21*IiOfn0eoAH=A%QC_K%RHBecZK`68P@@2$s57}F zppa+qx%ima3`Ho3|g%~FZhD9TO&ak1P%T+~EZ>?H*fYl$>1 zy{1owDBYH2s%U^&XbLSlS&Ff$s;FESEG;Oi&-CP1>q`ApfGVt`N)@UySFTT+RUj)4 zSUin=?9y_Byv&xCTA&H!WmKf8_~Hgnt;JK3EL2o!tA&RAT=4MRI<-7iQBvjhW~Qe3 z3X6jUmh!T4YfX7&g{eSkuPD`}l-Jg0raBz{mJ(5I zVO>>PMtOyfa)aL{D>t}`BNNQoWf@e4QInBIl^OEVN`fwRk;S8}sN~<@No;Z*Zll40(o?9)``<(-zftU<+xT>O zH8jf>cH~)goiVQrdCJrfv6$5=sT}!FT(2^O_%FQ@lyg~LzR;9fV%3Q1>nfcJUvzuf zla>?NGokD`x|F@V|5f(7nn>q)fb9Lhm`P{pwFZl1@^mFn1v|cX8ga&f0-;p zD#}twO&NLKJigejN;NBjX#pQt^W4UiqHK>&Z1?E$BtCnIBg>vvCGlifoWZQpKJwIp zJhj!5Sx`|Lq%5V$4u5&RPRkd2K{o8UlsG*gYH*mc8&sxRM_yh*MuoW0RV6QU^)b}= zgw<}fS1FX%+Djxwl)+RS%vF>Hg_X9HiZn39Y2~#wq8g{ILZY2 z6?RK;nY+^1rzlfgUQrgtFhSWm8N=<`SP5}mwEB~j6A zCQ+%%M8Gt;WerB~W-BWs4wXh3l+%7zo;>KlGNnROR|v66eQve5GVEf?%QWuF3Uf*o zaFS7!-e@kb2Odp#skGK$zBSCF*|)pM;M^60{|p6XjY_%3V|F?kY37y(^MYa1WGgk~ zDpJk4;NbywCQ){vvOH)laVh{-Wi3gO*B8jdPKmXqHpNzB&&>{|NTen5@?1dI`4y$TEt_~f|`r=eVJL&3I7bbK)oRsObZ7_#hlTII62ER1gX}L^n_gE^=PuUtNe9o}TmddbsnbYKYf)-lL9sBm(NGf5 zm~9ZLrkiW)6}dWXAE`RCkt%bS)T>3+T96DX-_b`bEK-;Y_~Kx3iPBV5lcmY?msp&+ zhMFQ(bxN95qp@3r(x9m*ke#ZrG>8lAg^qG*Lz>B|_SUDRr+G5#wb{}dT_z|_p|3K< zm{ncV0Ko(35wSs~Q%N8~A*-xO(TPj)l0nr5p-z(Ac)P?w90v2YmWcgn9*5T_6K2@W z<|?I3gE1~BDyWs4z%p3HeH@gq5pXbN8M1|G9$SUD zxVlhFxfOk+a$9Mkslrp2>(9*h2U4|l>9XWPPbL*iQ;7OdN|nQ*E;L$8veP9ZVKF~F zTUJw6?odjo?6d}9O-fp}E#D+@$SjhwK(<02u%$LQGm27j)!8C-x!as&_d%r3zrV8@ zF7{GRd%zh|#Kr%gDdMhmrz)r{3F}Tzm%6hwFW+G-cL)`?+_?HSe- zZcy4$^ih8n5dG;X_)GoS*roowgYN3!bakn_6q1Ija-j#fy*!1s0@Y%7`*G0-q_#5x z6?6-8xv+!efwQ9%Q;J|py1E)hBRY)eFPX#uWet#TP&JaLG+nK87`j?Ol=}Z^3B#t8 zNd5miV+uN)-i`X7IRfa=i6{QAEWsC}J@{MhqAb74?@E)T=!yfC!9Z3)UY$Ip3>;61 zIho3@uhOV%x38y!GJ8Ds)y;NoIYOG(`>;q0~#I)q3+)mMn`nPm)#T z%PPq)v1jxNq=_?1iAAomn=}etptd}@pfXigCH47hmFAGU=&n}<2YnWvHD7$YGg%|DTTCeh)#`>qQA(yzTF?;4?PI9dRXJrgUzXdb5S9mw*=lof zT5Yis+-GB2nXB5L<_%EF3ZJajnkCM#i}Ssftoo8Nqqk0JE32)v7Q2$`TqWjWOK~5S zv_hk9)VTW8meo@_iH-k!K}DfB4V;EPLSs!)t*tIymtR%t_X`UYk}6rTwJ?{``b364 z<<;8k5>e11%dqwFs`|);1&z7+{0BPgFol;Qzi#3PDKX9e&q}N$sEq7Q_`76TjVs>) z;%qYzXOM`0qu7(xS^o3#+B<<&Apd3l%5sOVOGmH}6`A2L6 z9CpY4S7(gGo@t@Y=i8mJe3{rDamIvQI%6Py5$Z@$nR!*xn$F71)LxlYMR=(mO#>K} zPHx#>Ic36JSB0=pT3V%m@T}AV3SFki6=k>zG(M$S)hK?s~af3rEEK8QyYCM+o3`4%Ztd=hhG+Jw;YmG;`2EP0BiQZ1>gE|*kR z8iLM%P${esR~G|66bIZ|kD(w>lrL4L1uJC9+Kj6FV1-w1@}#DVDl7c?zREm>$z9uM zqNrk*(JINy%PdtF6j`M%N(&s7tSSC~czd%SN40NF@TKdaEVWcUs9Pbi3E zH3nlqs^0_ijG3o512$$h#+c!+?~NlPGBPvrF@-~w1HaU{V`TXU*5FGTb3rN7BY&eZ_;2nLD#3?KjX1wWE` zL8#Cd@4@n6;Vbt8NazWI>Qtb{+z`FZ@&U6OB-Lqg&%*Cu;M-8YW~WCyantJ3<>1$!XQ5d{T5c4 ztK&=1&q2NXW9hw@-2xxKF1`1%0}%Eb(t9sE0HePlz4x*MtN2Ojr6K-xev?`$jEZ)is(_r6h&f-5xbjfNYe+{ig(ZiV9|a?(Vycp)XWD{?v& zVsLi&$fkPEEa<(fkiq?~RUwug(~&)>$okQ11?@B3=EmLRD0otei}B;C33B^A8WxbW(wHthhCeY6M@5rPTq!@Xl)y?if2}i&jBZ$NmC`C z(U^o?z;!St0L>wYdVsgKHRfPxNfsMEZW8X1bh^&Z)Qe#x_u_W1kM(7y`@3;FK!WZ^-@PmTryW{+>AWvg?UgE~KM}XveG|6>cldp( zn5b0uscr#KMdv$IF%4k#59s3eVt22ySu|-$m%QQE$FaLN{n>spqS7}JgrB2{P#(a? z=HU-ze}Ws13L1Qe6$K!BZV~|xb%;tc6tM9kYWCG4=zB{wIHs(x-fq9<^Kj=A=pIPe zq3zQ~p^xIlNwNsy&juiSDQr;}bKXk2Qcew7bK`+g<~`>sI9`}NeBtCM&Cbe%BLhfzuF4=Ho$)7xs)h?I5#} zG~gzEj^=&4E&FYhm>DJHSJ4TNJ4s#r<4O(|z@OrlZ^i*C%c_~ViP@ER)Czl4oMPiV zTuSWj(5H$y2KhRADAzQ0j1-W>|AlS)?0g(wtB9UO2fGhaj^x0+Wlj?=_FbTn+)lNi zK^|UfQ_Q#PuXQYcEo|^T+XH{665nhhM7kzI@hKy*R01{CE3&X!f2q7~>nooONLu0%k%NrwbKC1ND0lb*hoP{-p-Ky@h z&gYA302csa*!j1WJ}k2+#tT8FAa_i>vI_ikRBZD0y2B;b9ha!y^C#(Bt<{$`J>?hm zRUhm=fGi*Ezrlq^kB`Nm-=|duooOC6IX(;M3$}(+^TF=SOpJB&tnSai`8m-$fFB3# zFlX7-VMBQyk6Z0bMYrTk;Fr&bppZ6>N)DeG z*OE_o z2jBrrXv_wZRLedB*?VD9wL);WX|gsUNTkATTxKtH6;!Oq*23lU11?O3 z*}3G2;88f4>boHco#{bUe!byVqIuFCiyswll$?@W_CArO{KF0p*=4*N>Z-VZOKcxGpv47qHR)0`g2xj9iIe*i0MpbCazU(va77OI>BR3tNN^a>HM}ESx{RzgWiuq1y)gt4Uy-Mfz{MiFnH>^! zyP(T4oHG)g+KqN@m~jOOQyn`)xMv$ZF4OUR8q-s&X0if>5>IYc#9^1r1OF(d{T^v2 zm+4C9e%g0DQ@e?FaFZB@PhIVjSGShznHw{$63<)XAFKcOIr0Bk{kH1oW2^+k|q9cgJ7`tg|&4!=gC#6Lub!WUoyEzlj~x{E3|7=WE9E+N*S4Q?|b^AMgyH z5{X|62%vNFofN9_EKeVv6?ZAH79xM0XX7Mk$oi7yD$=NrwTn5%U1<`Ys#rAT1-S&k zRX$YLLoHtSitOQJi$L;B6++zd0 z={=ybKvrPSr;8P(J$gx(l03g=7o;`-M;A$Irka-oE6xTsbyhhz^?(m~<>JA$zjZ5C zOXv}?RpIBE|iXI}$y3jqd>r^#uKv2=mh?8w+J6m_xC z6suV_&e%^r0s{@wftF}Lq3A0>0@ZBFnj)KX!-(J@^`=j z=-0t!5j77#1IzQ8@x1mKSiS=b(@(;}W=d5qCO#*+R88tbJ#H~Tr=$%b$~6M+U>mwRVv;ppVn8DMt(`o?QI8mAGFmvD5)|d4 zRE3@TyRh(iONzg?B|k}e*^=S=EeU8ai>h`!Y2{CBNoo)LZ`-<*DKkrkx9>CyJx&DH zubDFXa!VSgw1AoraE8g@`Wxf)UiUa*j}&ZbZ~mumN%o|r`)!8b`=KtJjEbsko2rL{ zpw#ta#L(*ju38UJyA%>a!CaAJa)#*4(RDwg>U2GZxMkS9p7`xK>#v(F0l}r}_pxkk z)lFrrF(tG&eH&qc5&_etto}hEWiQ;*Fh%qeVPnQU!D5Wt~n#RIo)gbQ1z`L@z) z;XHQHHVsm;%r3)TWPWx_!ixub{c1;TNtRW1C$xp@cuOE1cG=J%?v6XiD$W{BF-mXSpIlZ$2<4gGYkG=-OLQGK-$YUFd*;R$>62#Fo4-EA7h+%($;Km@?5M{O&$^ML6~ z28HTl!y-<*JD0S23Q<(tBoT6xBEucRF4Pl=&65NYc?cmJ;#zEb86XW+C4?d#HdNU4 z5z7^gZeFYr@EDYHk4)GO=>@Hr=788!ZH9MLdi)e5O{fnK= zp5kziZDZGybYZ9B94WL_9<4|}Vrd<8IyCt$;IEJ!?&RLIP;Nz{K04_;FDZG^K=a9L zj$4F0kS?Yl*i?}Ad4;AW9v7ZTLh1H#xgO2yy^Obla0$n|`T!bLYc+PYWr|X{APol* z)1mVLyQjMvI3omXV2?4!2bo`HCsD)h1py-7hWZ;z*2=*is{Gri0p zj_`v_(9kpIbCRG_{cycf;CHSHBw04lZr-c@jVZ_DATZfh#e;R-pJJwXT*WCA+F3wP zPH;n7;HtXix%crpeFSqz9u68<1v4VaP4ZWve|DQ#kNd-Qf}?_OR~CZAi-8J}-n zG2dXluJQfhI3y>e2fphtZsB%Crp}$0%vp#iKQEn~-sU1wl8rA(qJ80VD}eDE0lbeO z?Q9MsHK&-@n~I2cSFRV%aIyN=;GQ(=2Ho3)D8g4g#wF1_-I+MPs%ngC*~QBZV(A+F zNh`DaCmIIvM}|TCCByI?n?NDaGi=E||M-DT`0Dw7_3?fhk4JR|u1#*hsvW+=<9J$F z*UQ!4$KxMK{YR_%J1iWxxHywZcsvfgc$OY#b{F2-9T(0XT5F_ZHM9}|9)Bg8BSocX zQCxS;PM`@t**l6kY|!>Lc4s+iG~Kx7n3O(_2#p|2+wgAOwJpo{7w5uw$%M2A890PUchI+3b_v?B zaX6)zXTB~)|5!u8#BHMz@9&jvDjS370}MIq(~j3PMc^H_si2dw2JW0oKO%XTX*A{@ z;XTvW)h?-A5l)M1$g5d@z6(j?sh7f5=%o7zEWF#n!9Q^qnQP22CO7Gzp?0)uu4yNb zrOeIe%!DYayL(OQjgzq=VVlIqB0FUzTC9)Un5=PD8gXI2HYx(qpvf@;zKZM}dw3M~ z!FOIC^W<(l95ZxG%V9v$o_tLP6zsUScVx7obj6NM!nFNu1~DY-kM|)m2qBo zYHz^6=R?#KS1rm&JgHk6^&COEMjkhvtRD}{c(wxT3&%MI3l3_1O?bGrDIT1?7Tal# z+pGJHC!7Kt{Pgr+1?PU_`B&t!g5O%eTO#J!Pm&j3^phMIa`)0x@~NNXZ{+>j(ERff z|N42Co{pb+;_p|ltjPEIbJVOh(NXgP3l1$ofJ0)B=BFO1ny5b4&2vp-EH&d! zM-uu5rLE*qXAkyv&2czQ$s00S^_XT-(knueS_i=U9M{Y#pby+5Zz-pD&Lct#YM$RW zhG8&+J#6u_v%ZL^c!W-ScH0SE+23+IZ@bMwf=G>|1DxQ-)qg0-5B4_vlY1Livp?C6 z&+#-4&C*b`VH-a`9?bpvydi=+_ei^67*F#>*02mq@N1qi`?movc06dWz3mgqL_9Yy z#i-;Ya@01c#--KL0N=U?zJ&hdcp7*q%4Mf7$J22}_Qmo@3?6EZYtXkAR-yz|EbL#e_z zU_g-r+V&3*fEkxgpZBx%m^T(Y3<1emBcv=w?6r7B-u8NoW=B$-r=2O-2;HJm z26MZawl%EbmQ2gpzGTky?BM1FcBPXCOJDA0S9f(+Oy zpnIOnr$JZy8~%fh2-I@^vu*T?n_&KS6TI&N^ttMUUwWA|Er``{*3cfgyC+)Hq4f(n zD3Gm(uqo(RDh0yiBXWBIo-K8}b>v9w&cH3~!-i}#jo7vm*HjueB4R!3raIQ{2<7#| zhLj;maJf*A2a#U~%qHsqAKVgyY#)m$i0UqE(rJ4rFQ=Gz)bMeWnVd?DFn5CVrsfKp zcly-3aMvHJuyR+~>;af%K-!~WBgY8akd1i}=UJx4y2uFp-uZiE!vKp?MnjUrL)UVG z8WUMDaQa~1gK7{0%H8pnp9v!r2^*9kFP^%+a%+thq#-n!O7gWSUV5Yn;Qh)Wc9ad% znTvj;3;j$^ZefTz&hztuhgPTQsnJ?AOM{j$H>mvQ2 zN8x?RWU5#cFsAGtK=~^l8WovWui%F0!A{c;o?!VBA0c8Iys7xL3~6c`!%iB(Xm_&N z^QRC$`(MBvy>~ZxODfxy3nei4^6)8~OD@xEs*YyaS=hI2px1(3jLc5ATd4@e537#S znEgGbp1n@tySuz)!w;cvBH-J%w^0(nE<59a`FmUaD!H%a2JF)> zO73g9t;k=M+}CmgR`wSq_qE)X;`(2Z+y|Ut34(kxrKg3IcFko#iwRqX(lwssf?R58 zQtU>Lyv<=WULQ^#_K6$FOIf;pHg@ra^*s69tO+qeZ)%Q57shg_a7$^Y-rA#4(w4=B zbhUfsZ0_huY`rT(^|4ifcM)$y^VC;-zT2+1aX526?!SNGJ-ybO#sv2S(@DVx6_hhRNXlEx;N)3ld^5DK)k-b>Q zH`z|&etT%niT)rXd*59O;ONt(!^feo1$amwb-o=dwPr6EFhSmy+f}V3HtjIsLngIQONpe41wtwBFl;|5BIyMY0<(G0EbkWGiTI>CN2sm#FB#EJCr_0q9 zDP)7?X48)Jif)#Q^DUyiTV1gDMFqY^AM|P3?Nym&r>Q-j3W-opSF$x}K=O6#o&&9B zS_MdH)$3cuPp&<6o%PtVLF{#ftLnHG>CT<0ki!{?U(5FG z4@yQ}!VZ)2C^yiV8`rpXB0Bfv1Xr?BF3no%;*`j<6*f$UQY|GnqTgs>p zOFYfnj?t%kpH+^GeXB3m2vLrayc2Q$U}@_kHZmm8sGEOQE!%5nBGaL0kML3`#PHP- zbF)yQz^k@pbmzG6a8ngdH!K7`rGyMkA-5mRTC7+9b52Rv=vdHg2`T>f-}H7~aZCLt z-|a6)ZhnP>uK?)KVeG0#WAM$9OHwbD5`&FP2U}_ko>TF z@2XZN5mpR+;2I}2tF53D7AK1KC5rUjio6~m6*pu3u&L^`<@-atW`m7{2lzTOHwy2~ zheODc>`~>(-ZNs;KMOFAZA+`bokI$($SsndTjhE;P~OcU*Y|6TTjOm(9Ug5tCyvUm zG13A221jBSy%$GM*i|DGXATjE!w;=9$7yqf#uJ)`HRVmDUG9_u(nEwAsyHybt5pE5 z1#O+k>K}}|B{SHzw`=U=R>1w)N338{7wxWg(mXe^QK777LbKw|^*D-cG+xLP2KEZK z57Zq*iiZKK_mDZu*V}jul}Bva%;7kWSCo~awK-C^!`^e_>SQqRnuIfTCY!B%4OaiL zk>KPQ3dA>BzWYTjH~t4{IU|et^=1*L&Nerf%}A4obWVRl%QgDnTg%`7#t%8@9zWRn zI=1?YYoXNkjOMtzIa0!Iq)vmW&ABS*u3U+2uQ3arAE3rV_Qdf%&4p#lh9}}1QluW^ z;Hja(l?cGd7A68S>Fk{5agIggk`Y>7rV#E9SZv$z>7B=J%G&kE2#`O4iM#^7SxH1GY9g6$bh#587B4GwtBkc$?!{eiW#%q^Y==?_y=@ ztN&=@Em=gtb@ji#q{Ej)`RDE*F86`YeMYw{?b;r1Mdi{NR2Q zUN#EnU3*Pqvux5mN11Xj6eOyHdknMh`Vc#t+P-hQ;b$^@YtAXY{I*jOkK{R0<~0jW zqUtm2y|kNrqI{#qkk7iL4hDG4KZtkk2n#Vbr$Ga%6_gWV00Wwx(czj$}R~DT+OBrexc2HA%HtNfSKgyI;rF)53uX@fZ$);sbGYSg;ll@EuqMPm5zGEfryVW3D?S zQOdzY+05}&?{wUcw}P!`H4SBgI{W2Gk!B{Y0R5*gQE>)Yg9 ziA{kr`fHPnkyEcdnF;gKB=b`Ao|DN-U*wMEQo{lIAlxgyaI_L{C5I*mN`;_S-{2dk z`>8pK3v3y0S@&Dpo;S+bnnvP$u!@G4cF^RFdNkgKP?iS33GB=Dp2`x|096mpMvJXT zG@=vTj-q^U&CO7Z@@11`{^`U_XjdBc{nGUi$*ftjfXQRl+)rfRM^m4rki`i2)>x5g zF%&lpL9UT4E=Yz#$;k?KNcq?wGt$|^F%!wbx#Gi6xXx{riyfY9P(IivHk>Wht2Y7Q zWAUg4I0+T(ATzhFdmO9QsE6&4TsGTdJRS-(H|FX=D}XSFvt0oTpy`2rnF_FqI+i?7 zx*)?Qj&AxfSu^|gx-~gm_tCA*m7D4w)B-;X_r%)gqQ7CORESGdQWQAS;b!$OVY;W0 zFjwGCV(n?4o;C_{oJM#VzLC{gz8e5V{y)EkRlo~OSW(&G-Kh+n%|YKmmc{UL;-s4D zz`mK@>le`ZpWpqE`TB=XEbSN4(7x5LE7S{=Qp_%>&=mTRG}z2idA)mW{u@>Qb*%NL zkkcFbh%}Tsx%jRzu(9cPNVy`xir)QH{o<$KXU|zVDiZ*NWVg?t-rJ-c_IYi=(oa`& zN_}@!hr*7@_sqz`p)Bz2XLIJ;TnYJ$Rpg&rHvvX}^|LQ4D&I~}__&Hlj&zCKuaydK zD#xWqxm$~_714yYxd_ajz3Iw|eC(3*Sg&~q69-?5h})UNkb}Pqq(CYz%ogJG(;B(2 z<6YJ{%>}dCNVbmQsg+Nvcj1?Iu|~N)n)%x8Cr>g?zI82HYFa}=ecEK{dP~+rRNiu~ zx0~*v1y+B$hv$=GP)&IDi?Usjce^}gjNLtiRHdbijpSUjFe^P9BWj;%KuEgFbay)s zK`sqlvCX-*OSV=dUN%K#PF5sM_~ESAtu!>Z7!~;1?7K;QG&`Ar6qHu%b3bU82h95| z2e5n!%!BAl8?f!KX1Ti_SLEETZ`<>!rH|M2vCN?&h^h5R+fPYqa&CKlZ@1K-UdU!o zPpM8W+qYsktM> z5MHv~pry|{oGBU~mMH{42C1|Q%A;UHG1EL0SeqgmtpP)L(YyJX^QzD?p^0hP6N)cR zN(DZ}yv!OAES2RM(o4aX*`Bo-8=#X&SwU5XoDr(xmHfltz!RyffU_^-*p$1Y zi5)1pXAPy@@!g7)pp7>~X>1_b%t$wUpAC3av61a`S0cnY!HTH z=Xkf5Ch5l~?EIp&`ag3Q?iy9$!w{a2nm?cP-3&MyT|&Yox#;4@4rbK7tB#Lte*3cK zw^7*&oHH-W?-1l{OnCp(tl65xTf}EO7&aa8E&Q`e3_XU>cQ^=D|9i9NrrG|d} zn~Zk>T@(Fk8dQ@A=5WaH2apt^)L$*#!av#P_ce>pN?5P#!-^o22_C0xlRs5vHz9+BO;QEgx_C0y6$e)$i_vE!qSNbHe zpPLIDc!9LfqRi{t-~pa3SdmJbI06~=#G%zJWf)89JmAnC?{4X~sE-3AdnK8~B-lh5 zZ{)kp$mSKk6Nreo@>#S7c+uD98HGB7+J;AdU#+=psLfV~=FNyu1)m<%!;ZNn6Q^;s z5mgMrFzbS@m|pTI0|(DwpMkS<=VsTpw4|0(A%v1agEsN^j*)l^fl-a_)L>sI)WDb8 z+6{!YcN))Y8{zMvnkNaNJUrKL^mTTitmcBJXX}1Utj%>A@4XZyV?yq0#REGF@0!$B z7*Z$#j4S*suKD8y9j7ylB(hVQUd>r@7w#NSmb@;+dJkhC?uOVOH7C}G)xXrfg|zlQ zy`I;wWvGB(D=Oymw2apJ;db6Cz9N2ujqrkvzX)g3-@(}rHn{i`8~m5H=i-Rl#a8=r zdw#z9o7?kepZ5Ek>le4i;ZO@QdVGjpD7oAH1E^&}Vkma6Vb@8*(1{O}>&JUEg>`PU z{X`w+Iqx10FJYET)0TOo%l&-TwwB_|)m96M73r1LHB3y2ouI-tM~>f##MrWOil zJJRcbNei=)y}5wHjkukMcd4|a$8 zKVo+*CyH&q*&Q!e|7drp|9-n8uQ=Wl4!PSIN9tIfndJ_i-Qw%h^)b&}sd$P5pEbv7 zl+@HHsFDwKy-ntGec#^`WDZA8s&qt^E&PwU!76u!qlk-f z0y)(eYHWgB@X$LI#qH3GI~QxAS;h&0;>K-kv|xuyQhL2YWq3`H-5t`h;H=xbJLT#> z+8x{xBg>2h?(~p$$he74P%#sUR@Y|>qA&D3#ZI@oN^R_?HD1$pZ9Y{*Q{ukfoSR{+(hn;gi4j zjH`UAOH^Kh1FtPypQ-vGCwFgIbnBAK7C9ckP5|2ZO+n)l=rAi{QbhhOHL?#q1Nd}W zDe^=g3B_pI6!RMA(BIwU3%>d;BLw|YO6XHz8Otxxi9uY7G!zeUy$fe?^uXx1`6A9Y zzW7d&hWvX)8edCv8L0kgRlh0Hmw{04h5D;9{S@2&j#Pg}sLyWa7qQNt;`g7Vml@HU@P{I&=A`(1Z1yHzS^ zeLM&FVD11UX*&Yp2b;T>d&Dx$uww+NEVLULh{M%3)p0YVvvZCv4-sjvJ?Ef~wBE3h zdF$x)(j$noR_Wpi=~4UK9ElO*88`}Uk!R}rrVs-c{Asg=abkB=lDj#ZM@e`aD2h4e zu9v$jvdIJKCU+O9H29mnk+}oiO)bO=p`WcKWZd!0W~hm+49zP1*b9{;{=CBt+ZR&6ppL^7YQT!sP+G zHy}wggdrQE|EA>R8C3%GN84hD{fM?OTKWcc24*$b9#>?0uUaz;^!NmqH&^q5loRB_ zzHV{?qQ^tjpG}R&6w}9K7-}wYhijOlUj zBON5(l$+}+ct!C@e7^q3)V z`GVVzOX|A^%1o=HrXcCji>-NrvEY0?;2Z3t5*hfH4B(%cnJ+8oUaZEFQyo~E<+)*k zD%D3XoP0VB*L!JrNb{ZIf#&zhk-sVX0$BIm!msZY>=j>>oh?Z9v*p?xr?Tp6TnvA` z%QF{|zLnqnM*q`_{8S7vzmevph3JbmA{ADdc+~RurOC}(&UfjU_nC{1YiIGF;v!kfJD3V*X*TAwVhoc>pt=$}btqq0xrB)4}iwRZgA*$TR_$ zkDeavU_A$JfN$%WAyjC6>G~`?C>K~ZuVdtA51g;eEp;;UO4)|T{oTWz$H)MyA&rL< z<(wkbZ)}*_gwP-&*&gc54Sh(BW6jv4y5Gb&Gm!}*NIcd$^u#@w$qlcz>HtDNgd9c_ zj!jBEj^Uc97hx)jPP+|hcnW|ZW<*^+J~83K3gXK{E48bX>; z?oGZQkD3LkT|Ck%A)xizKBQi-!+2W6VD%hX>)wTt$cc+qljazrW^d_cZoO*Xth zr$Uq?s!@H@ECxSHxF=PuoKGuqjB*3zw&x^MNa<0rhx`Uq*vEh!9{1{w?bJNE z4hLP;4*{~bt1Ba%p>a^m`#6cr8t5dJI{1}QH}qU}v2d9!w74NNL%Ryy9JU_tNrGl} zWFq1BBs?YT6-QY=F``0BHKCA!K6D1zQNWdJ29XmhEuVwKc8zoQpt@g^^EQcD9wj0I zM=*?1Yc&&tdyx8_TlnpM2b&_egRsH0%s-Y1GfI@dWr?R?6(CxKj;k(prHaRa$QZ{SJjnVzr@b-_2D*x1Cs2^C2&wfU9PD81g(X3DWzF;q-d?Nh6_cQ+P_CLqa z|K!`h$xPLLUvmn6qVS_o;egX=$bnd^<*lT|!QPOtx$%v{xNSK+D}$Kz3>Lw%iZ@U{ zNC%VU&)~g9ZM0<_{;jCioD{`0$$A>MpN{7BA*y~TLtKWaQO|>&QH)L;D?@5qcz|4GanDbjJF}^(WCcgVE_GL9*|Cg=XQe}AIjc19y4{cCDgy>5Q=6R;c;QOJD{anB-H&R zn3C<`fE?~L=`eZ#Qb4W0U=>-Fp)MN#keRZkcm)kLWpv_ZU4Rt#?tu9T=W^++%tx zPGV0D52f{O#l*T~NKWMuA0AMl5LN=$Sxngdy&=6U+obo+uD_^>BOd7$xe0bdK9zh& z?FcPtcEKeGtQM*6CD$P>k?jY;5HfpxYH*9cNKLQbtI9MS)17*Bj{>dccI?f|&yt_r zps|-PK4(SO(jeyzGY;`7P^kvfE>YCLSi3pG({R5%$Rf70jv2d|2O4FM^TkG|2Qy3w zfutI&8(X(wj&~UHa}+wIo_*PLF4wo)TW?(Vc0nkY1FGOaYOL`AGu{0=;*9ZkIKy;} zd{Uy?ZOM28=+E#dIqa8k#`q(gG5)=9Mn;sY$z8{?y5YfkyCQjm@aAsX3KLV*Sx{PQ zY7T=kMD8?$>W+So`aAB=jd-4mJvsMh*2#GPXx*s;B63TO+PdQVR+pECU0S|Jr!bGK zYEYn7lG3v@Uhg7a2*6A^k6@}oAOjY|*lQL-YPy`th26T8Aep=6F%XD#L+ z8m$Y |d9AV4?^mvw9PrQ;?M*9;~9z-%7l#{LioYBT)85k^}B6YvsGf(N$ZcXv? zf!tQt>!Y14ErNGghL48>Ul#W^RR&D9gz!F7`o;%*sz2LZ+C`QgItdz6`bDk71~M9| zy7}gD9**$sVV9d78;GX^uR>wTIQmta*Ez$4EwW6zoM0at)F0wZzG=!ML`0};9;B^z z6EAM4t1WmJ^xc%<{Ud2y!XA&5e~6WXIrZy9ckv#$5=tYv?+n0%gm~=dn@#!mdvzIx zTx1mB5iE1A_8sR0B27|UecT+xz)8*|s=MBRqx^+#9$N>4+PSQT z8;k-;M#{qyQhE?i$jfOje`E^1pcm;2@Kd&6&#EcdMRCiWKi`;ApI_$P;$43y!_M5-pL;trV>yWEzkE8G>2Le#pD?(L`l49=^PpMi zW*HwI^g>g^SCc7L0KHC_=_vIQZljuBD;f!okf1U2Aokd^s6_Q+ArTO zL*}!i3f#)k{Q4O_AM*0tT#VQ0YUd0c4QoL<7>0Yi6NY3gTqG!thk(pDP zm$x)xJ>KI{SBWZPco`!MR*}R*nhN&oAmMMPivzQ47N3sMvB3X73*l9uC#OaffC~Wr z46dT!f-BbV;MM06rXOj}_w;5-R{Wgid{1w_=;i+*W__eLFGs8W@X(*7U~*5EmrVPz z#Hw=pWfh8{mIYtPll57d0+`AJ~z z*%QIrV-7)RhP7fm^|b=)2a%|WI9dGzjbbfnCI9O3s$bdxQ0p=4w`%w<_`W{pbGM@<9V0uZtc1DHD&` zz{GQJhl;ZY@?c{5$0tQj_Ai@wCmqaBwecK7e9y#xdSD;YC?8r%Lh!#a`_vbPYefP8 z8YP>|-%enp-(@G+FP}$bUtR!k5|{a)<7@m|wdLaLPio-zfNS)RP5r0#`XyUyi{ST) zkUGDb`qyjD^V;{caA~jqbv=BqhD-hTpDE&UuJE5};)?wGlRvA9uO|PisYTzM9l(f( zYk5-5^Z7<6k3x79{F&gI*`{HfZ|n`#tjK+16E)(ZGtSFSXg$Vl%$P7B`GFXD_v=PO zE-m$edhWb%DYh- zITW;26fP8a(zM`oXA~Hb7t-T`>}KFAk*!_XTMWrQ8GZ7!m~bKfTa#~zZzdny!F-;l zx0v_>nEMO(_XJFS1pXfU42~_uMZcy&U+WLhs9)5euk{Detv|rB<#W%h&d+Jkho*l5 zcaU4Mxn$ut#{UD;|49a4n*SB~4FRy14oVhPb7OvI{>SGa4ZD~$M*YJ4KN03b0x;@* zEc?AbJCnCLpizgnVIubKz8JnR{{UfB8>{IOEAg#bKIY!Tbh+{^r955LpE3Wdza;=) zpY5#g_cER&6*+h({c&pr_K2;>E7$8Msj)1nMhV70F-#zkRc9QU!b!tnyw=lXL{f_m zG1WK^w4NNS?Amac_ZP>UXF#4OlqE2#Hrv)h?Z;7|3g^Hu?yeO_$-+jAb#~Y}F>;!xbh9Yj|M}oUvWi4zUNA4XEi6G15GWIz0w|W zO#=LoP+$zpFx#c_*I1Gj-?ivVZ1l%A{W})@i6DS;d@q=o)Mf`z zd+ybuzf2Gx4~tmA)!)&k75P1V`dM{ay0L$(P=BXSFFlCAqETNf)QbG2Y`L>lT2tn` zzRg84cDc#`j6sGT=3}_ac7WYe;aG-PvNKd_Ho8kQXm?vCwx?b=23mMf`0EyD=jI@6 zdqn6qr9>HkdvyVS&3$+`^LK^Zur#Ahx2r=Iq(DHW%qKKm%^CUFUXWAb#z-+Bn18IS z+r_l_((RYgMkV#o<7^+MbG}jFofUI%<`m&3rkh7l<>9p9Yit~OkKQS#qMJer4(s08 zwZv{)x%P_m?b)jpMVAd7RraRQ-A!>$oyiSuL|2(A(^~wU3xH34T14$#??Udq{=`T zZyS@sirjt5mN$qo#QDWwT{&(%dD z3$ORdFP%`%FVciHQw0mMs1TcZ37r@ccPodbF6529y`CAgobw#fT(DJga<9?50iX_B zFyD`WuSm~1bz)%29A3qltDgFX=J+na?R-DhN{EkFDdBm8-Vy&lZEw~iskW_)z1R;< z$&xKuwvXg1Z8yida_Bf{q5C!g=M9>Gk5kNrD2R;0HPa-n2GOMy`)!J+Oq1LLR zC~`6xb9|%yhRgxSCBAZ}Y zxyku>S~r7|sTj4D?8BD3;6yhdwC88m^zv`LIkH90?fQ*QrqM}*@(df7T-(2qUe@07 zav2{!UBdb~gktF$kYu*}jte4VWlb0u;!U?@lw*%ekudOn%nx1EjX|Z#*2%5uwGL>6 zfMST=hufQCeBpu0wee-nARXFb=4Y>Erq3qnSDuYWZ{O-2pBA2t&iHb+nfIL?ECPHo z7#-L@Z-^zTIokjY)&k9yeP~OUn4$W#1X&k=75-X5OEcrAw~SOYy8$2YjnwebY5uIX zTB@wf#TMKswDYL8TIeAK_<8kFZ?#+n*5GH)SvSGnC4~C!H-Fe3fKSG^K5(Gb51)QM z&D_8_ezOMLk8cPN?(qe{TfEj{J^J?Pzmf%%nQl7NFGNnm=N;uvd2MZBs<>^!n-xji zuz|bdVah#MS;glN0sC*Y$P{&Rez-aP4!S9&zMB*91i?iKRc}bM=!5{5{R9hlopXb| zs3`nZq^XCiHp|s5(a|g!y4+L_r76qhuD5H^2@hZmmE+&-aB~`F3twkCGp#8Ow+8=2 zod!}mkWJ@pg5n^f6$8Sr&yod zrd97dyZtff;-soGR7>PNTyiu&gaVB8>j~gfl0o1A@Ddggngt^edbh(7W)#!ICiPu z;;Ti~DBIH{26w3}ItZsS>t;SEtE23ojJa-6dzpfjq_f@(EVQq;@Hq+uuE2-&==!lt zZsSYDuFtELt8od95mbAMb!AQ&*h|MSzO75D1|BU_6=(?o5T3^ncagC5Z{d~N3zfLA zm}=7H3J({%@ti%JZsQd?_w-auyFItRPNon8|08Xr+h+8XJQ1k-X5*k?sxskrrFflM z&zm9RYgL|+!d2JsTG)k7gYz0SkmpMiw1%X={JVw=8>yvNQ7}wiyo2}2Y^{$W$!i5I zGSc0d^XldQVD8=R>Wy!%$LnyPknEaU5jM?}&Zzu?9d=m8zrWJ%D6D_BfWymfmb{aL zJ|?t2Tex)SIb3j2R+Gj;U)=c0Lor0I33LIH0n1LA7k&)Mcv2cs>2mO|`J1XGe@soQ zU#Zz8+~6+(tMX0F8rqqKJ==}L4^jKqg3(r{nm1*Ye!LH;xz{`2Xf6cKqJT24fH^z^ zSAbS^X#Cg-2HbAw1K;k0(&*FGAJps;p5$r5d#781tTC{&+&M>&oE|vdOIGi-A!4Cd zc*84^`F!W|zE_$Mip*4?iamRqJvq!&2)ySnhE#myitIZ7oDyQz7@Bor3hIS0gY&h* z88pH9B04LinNyd-W6ol6YsWobij_LG!Ddd3vPaejFU%^SZ54la?GFCff^V_Kj_Zh< zmHXAO4;YQE-2CqB4x=^*C-|so$5_BFLgiV4HXsVH~af7 z0*;UBK{`6=Cug-w)(sp0!0@=wK@V2il_jrDCf718mL|xlE7yDF>>+XULjex)nja(y zn!XVhcOO^P6{!o=Rj~n1_8zTi;-%sYaa6}WW}uRJ-t6UYU8am8>~(#mrQiwf9wKWD zjr{BA&4|HlM9+Z^3bBc}!+!F4%r?nes-8QHPrS%$E8${SQy>Nu@j6-I2cF3%k|MZ=*_^2h8Y1gd$H}r%vh;JFT z5oZ7M7dO6(^1C3v(JVwu5meZ{>j#qlQa>1fOF!f?rM~Hh;A{Qxg#vp}^cbkqZ=tX+x&U1H$623 z4lUfZW7C&Of6RCFc`7$fbO1Za6#XjMIrJMzn46I4&x6^|@xZ($KgnahP0?NLF=-K8=@FvDk*u_&`~bG!mTHd zEo|>@7h>e|o#o0e#C**wLnf5GKgq6LTNDe&n3kD5mS! z5u><`=qS*IgYxpfugUA`P&svLAMz?e@atA|XREwNwSX=&G=8lQ7OJkzpAEqJQ6CuJ zq?r03#sB!V7zYnSZ!3gux{1T&ov<;?D zw!yrF-c?AkzIBPeU3($G)xk)9J~`(>jFbL|_;332;TB4Z2K_Asn*1(r^p;My>A2O& zxG9zXHkDJs?w#GlT+r2S6+(f0IIV|sfw)U>r5`2XH0&q1Wzr_*L+C7vw^kn|F%oHQ zt7`RuuE0TZ^4mVvn#iXQcU4C6EL$_au~fg1rz5KB>XsbKP|8`SBn4^fodMSMcLn8T zUXIXXouAEn*o=0WX3WkHu{Ghfm#c7r-gu+EC0O%FPq8vjye6%*O6L5*^~j{;xb!@e z!JqSp|5aI7z6o$FzmkQo(mziPA25Up+-dH;N@HOU5auN}{vog0Btx4ocUu*rA zAFkWgVcbeLZ;u`Ex!p)|Vz}rnX=S8mV8zFo>{egZw4G2Ieu0_I-7NJmg$tU!xH>$Y zk)Tv0j$#Vet-2gqs}d@kxwYan`B*D2|FCY2_Ya|H!@77>Vgs+{)wy}FP5|}9eKGXn z+s6UwNwalJA!QvKr1xn&uI-(bcrnBS&YG4Pr%Rq4dk#Has(-QsoHWu$mEJ+JXHYu% z7U}`WENU?EN>fNYYPjiWQCs=#O>I%cxcRc9&@p~g3`-Q}ZJv8+kgO}35Hm_M(Y}eM zn7NgYFpHT0xo5F^<`cGyb6|i!BTE&y0$c`m_7Obs?B^SOh@}BB&3N-Qngo>WE9)S! z%qd0WL|6(3jc-N`xC+aO3@-!1fJqa7ak#$CEsbTyN|$bK)nIy=dhgiFtvs8Z#W*aq z%+I&aja*!>XJKRIAZDZj{eMA}>b=D3L!=5gIcA`~u^P(BpGTG!#rGClN=aX`?;}GG zcccV|I(3QDw9eB2hp`4@91&UWk?8UiJNxVX!+Y7PQ6-+CX(>2p({vI`!6~iqw z&vt5OL|pHX3Z*q)zMcD{gxN#|HNmwdhMno00J7^l`T zmxG-aSfx4KPP95{eVkeQvfj_0?De&am10?JAh6WEOLC$+khc>%*-PJeav!P5z7I`Jah1zdN?^+sU{a6-;&GKX*!Kc8oGOu`;Ra#xX&^d_Cir80KWyb= zIJGwz;rnA?S~(qdGY5S62*nFbuCOZZZj(;uC74c4d7Sok zlEd=>7y$p{B{VcsH^SfpC-(P2q~QEry#D4|nr>N&x#UMbu}9}(X0bqXz}nJft3z)D zs{YJ)GWlK)tvR`6nm~&&9|pvrZ>Lyi)VLNzHr!?p2ao?s(?cPL!YSjeVLq zD)OQFe55hUuUEHh9}K(0`gV;S&N$h3mvSd^vRmakIE{V*-vT*dF8FzuL>V`l?L()l z-tz*QUhKXVB&En}EuO}pzZvKC7TObXEEW51zOJ|a5F8P3vbD{=Ze@kp-UImxyYx=F zVQszNMcZrMt$U`ej=-3SR*ArE{Y_-!qDvQCFtc0Qp&l-O&D;?IQ_0dpfNx<^?r1@2Q7RQ9N%;MgwI$-y~`5cq!#j3juOt80O~ z*~CGtApmzcj~;p_3b;5Ec&&8pP2`-(Vd*S$Y_S19WnS)cId%f)23jgJ#TJ;?@g-Ly z2f@MLWDgq#TTY(-{5U)VXYA=s^${`An7~C0wr?N{O z2zU#(5A1CUo(3&{{&2FWU>5iwb8h6k)GkOA`KaUa=XXxgk9y%j$0ea|9M~fyq1uBP z2!2#MP)isZ=php5DfVXl_ETPn%3GC<(Yjx*mlQRW=5Vx9$G~XDkW77DKnvoP`Ls= z{%8f_>9pORbi_q`eT%5KTkmD6=lC!G(dJ~LIi=c46~`R5q=i_`2{@pqiBav#HB}WT zr>PTizi<9{`3DgM&-}Vn4*y|!S@$eNyZK@BnsVpN?;u=ZzbhvqT~2#TK9g#~1TxvY zne?V=Yw26M_rF@Krha5?5{#z=JyP%to4W7JPTv@o7H;wf_7HDmUW>{as9)rT9&nz0 z2j_-nd|$}?ac!z8jJ!cy-YKU(E{D}j^L1L_qgSc^j&bkf4t!%IUdY~`@cQmNDTX{e z9H;q6%j0`Q7+ih7mVCYP&xLOP%*sEm`>l}R&sYAl5g0FD48fr6tW{&(rf=RVCdpwA z$0O}C(@7bOGrz*!BcoQuxi`7zR6vSuF9*%pZ8zbh5Er&5fV+cE)HJ1P-EqKmLG7YF ztt9J0vi?31E69>!dQX$1zLV*(vzG@VW;MWt01 zUmW1#3;1~2xq>wo8!;FqH=Dp30%7IUO|l&b=eFIAGrBXo6)gFUW@W;ugx$Od(P(2-V0g&F?2S$At~> z!f%{B!oo&h?N*{9qui0}Y2bpcWIrhH5S>l)W}eO*UPq~XVUx2fuAMcJO_tp8dt7;o zZbtzCVjp{VqF#un8)c8}i89+7<&l=rD4kmk9xkpma{uaj2lR7T21zELW8mNk20=e! z-~xAgqhXI6e~28N_|=n3J5Sx2T*~wRL~8#`W*0~L*HZguGP{sxFH)=jjC%VCE7aJ! zWOJQWH0V@*m*p}fr!k*jBNz_2*QH_NH-Ha;0?I=>JbRKyOqqu0~>^?|C}a*g@IzM`!mD-5%}kvDX+My9|a z_YTjrcnf?gBp|W#H3_|q>@1mt;4H18nMAHvz6t6>Wg&HmpUX83nYCV-Lvq?MYa`5# z()!Z)R&trv^#R$Qy*;ciG019OuOQoNsr^{z&ZR%0=*-d5A;d1p9nrY#8s%P)SrOVa zS#(|7`pA^nCEIz0U)o3F}@|t|9JVI-(iIrAF;UX4NS1V3MQWt10O|5($4WSv}F&`55V%Nw(BFX zSew@nUh_fAJ%L48;zi@`&w!=>>%j6kX#DkZ7UBI~?4MO%uQgvEcZLo5XN2tXn0oT` zBbPP2SA8v36=Bj9<&sm5MFb-mMPy`s3+&PT*w7dPuWmydpMkf@Jc=0lKplu|m2+Fqu7C~9q1Ajm zS%u}|K2*Vwhf=pTYg>i7jP}&={eHj70uwiZMA zk#f!baKty1Az5Vkh^YfZLxWD=s=j{Wo4V<1;#DV812}9Y@&4o&;>Ty-V}GZOzpG8& zdX0XqPkyCNz}fsu3gt%tdM4Q3AxNSw8^O$m)=4(e$|kErpt?67akOm|q@g{kyfC|2 zvZtM3-!-Z|^1xpe6s0B2rNPV@E_T69up>)gWGoK@ ztk(_a?k)*7e-o{(xEYV+4f55MH~Z1CC^sFq;^bU7N&L)@8Z&bwuZTfToW}JHXE$Um zbp^1Ix8BRY=S6xFh_Zv-Z6d~7E3FJCiUTjiDQw)}$2d~|c==E75cJB_^SxW6{>$8& z|MCO+ypO7q4fdlFWl7k{$I6+$DeVXHuuDv>NB#_X(yx){i52fto&Z(^AJ_iQHu&yI z(U1O-r6>hnI#QNG6m?XL=g8%^t8dqSXQzPa_M3b4v#ok|x&GN^{i>g1IbokWI(Aw) zB%7na0!tR5r%N)nD+1N`NM0D){k%yUL1tW1t8_fK7!s^u*EUx@yL82*bU0l|BTJ$S1tX{i5ax*XP-F0i>IwhT*DicYH2{W7-qzGkgnh zh2d{k*v@2Cqm$lAEQQ#L!y(XG34;gPVJuR%k#;#Ni zbrx$OrB9&|X0tQDxff|iX~%uN7a~rRvUHtEE9X>dian{iy?!BXyw$Hq8q+gFJ+JBA zes<@p)*zbi#FvX|bD9NT7CJcDvrf;N$p-cR=v}e~fd@ zMEq_&q#t!03qR&po`4T=RvGE)m!Td5GAc*Zu?LVWN# zXX{JAJNNDT><+ApSupoPV0&^?U7e#^)X8=1W#~kqhZA()RlB*7Qs&;H$hgNCk)7geVv#T&-p7en#`)ZLOP_a$Y^(LHyLuh8tM^A$yj2R^wVHD zFh&}_YdGTN-lZhw!%IDnK+Cgx33Rtrdq(#lrPoo;TqSC-*OYE5$#qWQO=q8--8XPB z0XS&S_PY;Bg>B}vw;Mc*ut4o>Vs$=^rl4YzQ_VjIhi8@Mw?TyznX2#LAOdjUqF+6E z$JQ@I#ifJ$h3osf-~goh6&`*94v9>?H$!`$z~O8y6I}A8^u|hmuYx&h%Z)%V3}^P4dDuEuGr^5U(%v$Z=ii zs5p+yr)3RsH8kGoT(RqDY{rCv(-;?^u^i63J)vp+7-StxDvJh|`W5N&He7_#A3yW{?Z)B1OfvhY#_-IS{AF^`*=r2CavE#vV`>-Qk7k1-O{Z z{xaI^rjnF1gtqZ!_n4JOXPI@;}kGqN6JiC^p6 zqI~&p$2;@K%m4XK?`hof@E>~X<<+f-KJa)x+w!x2mV*3y0&?k9SoSiVzTG@oiywKG zpLx&+Md*@mjG=%h=MQt_7K+f?2V@tT#7bDrLO02TbJ);WvYKV#Ye7Sl4_V-|ik6J+zNVKoa?D-i9UkhOj_ zljN(3Buiem`BpJ!2*`6r%4@xx@woO{FK2G;gd!{Ba~bjr@w+ooel`*1h4@6lmp!w@ z;7xLYw`Nx-#}74=~_ym^_E@AI{VQopxo9b!sBO;4Sq1eA_WPKZhG#wML(v4cKo$V5h#_Rgd`7tlbtTgenU~+1l{A&%*@2;DsvHi)$C?Yq=7A1a zsNjCB1%MJ5Wc6(tFL(+~C2!+wOg1#LT=6=JSB4W=LyPYDG zpw;3KMyS4)dQA*!7G^x4qhK#Mk@o|hR-BfE@~Y`!mn}CQweg1)KFoE@k+HYN(S6Z+ z(Mt2CN<5pl5W&`sIGHd1w#9<^z8kLln|&F`OWR`A1tZlzUj7#&_J%h64o05fA$@14 z10-iGnS5o(AZ6J?fI+cfzzyv0nan;I(>ySxncytm9D9pMo;^0M!JL-7?5o1yEG~`% zRLj0|WKEA_(SS35B+Yn5s{j0Is@QK2cml<;61MfZtaF$6%UjP?xqw4-|K^j>UsJK& zzO%6Glv$Ds24OrEQe@CN>nhR%+^t`b6y_Y;^ zTIrkv2>h#09>x1h{0I^c<@wE{VBemgKUJ6i5f`Di4laLqkSTpo-V3?PLa!u`AJz9) z@B6d2T}Y#T?|pyvw(mpc|MY!-_O?r3=;!x+>j?X0KbYdwHXp~D;~Gmrp0+pLxowV4 z#}WJ0hFz5+euSow#B6h8(}iL>GP}krtlJrPRwPXffqcfnj^QXSWn^}X%VB?V)e*uN zzS@dGr>=Wa+s?EN5C$)%lxE7s``gfY{K=%~IPu}Dwm#;KEW0a8WuCZAUO*Qs>XdKJEg!^b{SK&w;Ji9a1nzy6X~GFvp+diSj`SZuV@PqbN={29^IX(sS@qN%SrYYh_`uWz)G zFT_93$F-z#iRec%!{6bH{>T=+B%J>eU-a^Gr$<^~=~SXg??Eg%eY8T;%HoSuDCpB6Lc;Mb7);R6>oSsKze=W z&pE;QkhR@r<8-+AtBV0#!J`7(chU67g*qNx-A^uT!J(XT<6)S0k`Bp-Nn`E2Dhr`1 zPL{ik5mmZfB^NsqSAT=zr@T&q`l1l954LYm+jVKp7vZcD2%0-4l5Y_n!q7wzjN5xm zMj)#th3*lb`sD#Gedw5%1wMJ2u^1Bt%@73<_+>{?aEKy*1%!4OB0v2UHvwPs2M%>? zIH_+0tL_g3s|&rV2`ln>p%zh-yYIDU@`V5>`Dl`I&%&RP zjg8+Lv1Ef}6y2a1u!h6agjPvP9fXoNJ!J5^(-Wx+o!avIg|3W2730Yg@kQGF`@Uqk zZ3eDSx_K|Hse`}nJ1MQE0n0bHw=~QP@r<+?(nTig&AwSp>)}?M_WRrR;5R3+6A}P` zua3%l^%war^+2Nj8co)FSMv?oaN6A5fPM%dq2EhTQNr@cB>THjNyFV+Ri#u3j^Fo_ zdg*!m4DDOomcq-yi?jKj9am|ss6_3tFN9c#3Bp^dQN`LOx&?^9ii^O0h3ELt({)o_T^>523DBBV2R94vbH z=h7IJb->WRTaX2oIE766MQP6?x7@_}36CKBeJzh2dAk zir>WHpCwlOJu=&cSn+odk}?8GN4Byh!FRU2B9gT~1)8;2z)tE_0z*O}*7gYiy`_1j zVb*?DhW2W+8~IDT+xjBQ7?`DI@d2w2+jSBhC3nJ6PBRx~`oBKCVaXWLy+?w%Y2DX7&Zn@oQSV5L?kI;AEvo?0!;ssE$7% zWU*x5-14W4G5{Z6y~!s+n!~dycLmUK0>5(b`5f@)UlWjnoivHz^iDw@e2I`6q|mDM zwJo$XXZ>@=_M4nO&cz37TYiR-9wEN!d#mY6 z=nWZvhKoz_BS1z$mx2?@CO*9u5hzk13tQjNW7jJ9=e8D|xuFI=Iw-N8f>xjdfHl|C zrk(5BxEjAk#-HKhQnc_(WISGGn>ga!0atg0AUCaRoz=8AidZr^v6S@}A||@#xEGmy zjhZB;wU}iS+-Xfz=3==cm8DF8N5K`SL0&7g=u2hR83rdOGsDRv$r_ONUUFDe0b-;vHcp;2VqDD5WWbS~- zhl5Owoa-1?d+pY2qotfc9*%0qU+Jx*n)l>Xuq%sIZv7E8C0Qq~z-FRWHz(!hZj53C ztHzTsH*K9N#R}K<5WHu4L86abqY5~B-P4DVXXLZyj{ZS956K;9v{~JGO2ZR@Sc2i_uIW6=>eAF!UZun?&tIX)IlI`SzpV2 zd+->7A%?^C4zau-$xW{$!X7pjEYBX=L98$# zRyy@cwe?$fPlXW`8Rx|}ZZVDbvu!Dm$~vphR(omb>E2gk49mlOA8dv-kh^!g9+b|- zy8WKK+CcyJcLX>t3nz=apC$6R3SIeJO*F>#nA4NvOe+P)O-Y>V4Qh8x&s`27uzE^E zvNwt2gs}r1nxR>!qzi;C=Lwcc5j~40cA!&4y^XPb7Zt(6$G)zS(1io^W>Iq=a~I7H z={U}LXnVscvb0}t9==i3inBe{l9FzsO@ra#K)7%)RcSc9}0t!(vB zTyKOmT;MuTB`m7qh!(4sp4@h(0Nt{MFase^VtLY>QMC25m$Jn;HX9Kl<7#uUVK4h$A@PBdg17T9LuVvjTPG3EPd<zS-2+j5W<^rF{p`^QLtypEyF9)#xBTne5fUN9Y1;>bOMf%iC zkZqOT2?5_AlHr2kkij_7o>pJ(FNCOXxq5>Pu@OPHQO%=U*VI)iC9!cf!Kv{4BS%F2 zJt)kP@;5W5bF8}ZM9IZ61-ULQ0!XbB9CBW{>*C^GP2&OsFVvj%ohEVw zv)xn2mOAOJaH-$|H5+Qn7z_RDm}erkt-^X+K*>9Kfu%i=gQIKMyR@%%iFgu$zes;mkZ<2mMt zwIg|iFRN;#i1J64zaQ^?A>QvMHm{wNkDvGGo{W>9P{c8vxU<%n=- z?edaCBLC1ToiSP@Yhbg+68c5hYIjUw)EK=(*_wCkVB*wBjL(I+2U5MKR`(OB?JbO5 zaimu4ZwA+2N~=)>r@sVN}2Dehr|=kUssR3oLTadoETCcFo?B@J$=J8UUGrZOYzOaT*59ZGJ8 zp+GZl+W`w8K11t64OxB0WSeVC0}XhtH~9)eQb~N#qkly17WUKRPE5v4moJot0zKG z)tJe3OY&)!diaG9R&d4(ndXzMS4{VNJ@pfwamZTjv^&^I+}XgRK=G$a?cL6(?kAiq zc;aqM<(3o8@Y=UAVT>9@ZA$K-+Vsrh$9zcAi9|M6hzAlCu$L1q`2H*ftGd`EGDd+9 z?`56Q8k<}(p<_+QCxXyyC^VAGn%m$7SV2b~ftp%xU;h27q&NQR2BS8R(l#l>o9gs} zabC6KWUsH}F^~Ru`G0;O^7)T%l>97$9g|pfvR`O~lJk0_xStiCE-oun9$;7oUC&-Zb_q-Uu#QKtrsQH17 z=mQzicQeL*LPykoX2Q=}pBtZyBit+Z+N;s=?8cD|0+r_omLG(X!t~v>!A*Y7Oaz|r zjhjgN88^|wO!O&intx_C`Sy;|=ivzA??o0HSS)W&>n0;#tcn7jsVre9%<$zBY}L1hP!(;_RW+)B!1<|BMFnYI#bwW z*lyQA6NYEVxbIKu?JQl((-}!uPF?{}zX*3EZ02~i%hut^n}sC5>25bp9R;zwzz!O7 zFLE^LD5&f{LodV|%h0!J zaboriDlfX}wVsa`dzZJ8fLE^m8%5<=?me&<3wKh~dA!zMXp;Q&6*UK5Y6Tyaf^W=2 z=Vt);8}rZ-biN=O4EA9@qi@WN)+{aTyqvX>@))|<-;t#=CO+&64W+Tvsv`e|@W-3r z^($IiA;^y=+2^}I-TP{g0q5s~Nk%3=pW36l2drG}n*IvR38&r0@sI>HwM_FlUooUu zK{~#sU8cPo(XPb`*bN4|rOIq>PIqj=CxsAhr|M2kyZfk%`&F#-)0yErpkel!9OM8{ zK(D`1&`mRYV>)0M?kfBeo;Don#39*Tb?7KVP|o2ou&Zy|hz(BF25sRJPEZj}0Ou!4 zdBruZ_aqTly$w7=gU}|W6)bYtZ#1SFGI^z_mcd-RV?EzU| za1Ziz7@6-rzX~Cvws$g1Yim}ant`B-F=VKzI$=AU-~<-k=P-3Kx^%wFeMW?DH~7v42sHf#v9VVpYzrV#6nO75qUIk!#U1PM6BacH0z zF4g&V4nlgq*LgYs)QrPwJUB5a_r-9CwheU^ArEmIGnTKMG@GaX>RfJA2;8K8j|r~P zF;gtT@EFqErQ2Pa(_rZ1b%+schH_udB~gQKA~D~2-Z}#FH|=6Mkg2qNA*@7ZyKUfJ z8&@? z(`)tqE9zzQ4|4;-oyz3eEClpDz{<^nm-QvbQE-q_7l9w?JLkz~_lbXR(dxg?!1opZ z-V#>(j)8A^lKw^7oh7Gt{yp{^|K2y0f2@NA@6TtCB2^Ro+$H#>0vFe{pDF<$Aqw^& z@c@=T-ZWp{!p(8`v8wayQ@BsIJL=j(c=|7rhhX(IjXq5W0Wy-a=nq`ZGWDVz|HY%HZzM_Pd7 zBzQ{-|Da`ml@wlV31GwX@UZnZo7>>5@A2Rz>*6HR)IAnd`@1m?xPP>}qtI}tUFmb> zdOJ_cppJW~Gi>sglETQxVkzJS{9>x$p^naTjH%hN+vIh)J+EdD-wJ81O!^V?qvU#U zUx=**ZAxE&_P%M@*1)!Udve@PTR}XwR!q!rV}$H&@*4I*dc0^>W_X(kl8cds%m&|pO5o>Z6FBbtVKBSzuh9_klb`6Kmh8U zBPyHa5j*I0co3<&U2(`7g)$1FoqM7uQn2B7@cKx#=;4}FxY)2akFIlRzRr>3#4p?N zkfrwyXO))m++yDBjH?aj+^W38!WLiW!D(+?&ZsU1Q{!Y|g)ik@|Myq+iKY`jhBJh} zAI^BCj{h&u)8hX8w6LO~e&tU`f4qh=7V={Do#{|L!UZ2<@WtBha0NazK@DW@>(It+ z2?G2M(elqj8$b=t-{{$iQObC~ z7GOIeZ~u~j%%?T~(%AcQ)ytYcvL3z=pDUccXFa6TE>Q={*`6UO$kqhF5`hL+PVwCTF(GrPx{%lLd)w zx6u(`9)PD2-c}lO*G3(a-^A*`c{7J^b z`q37d;aA1rhEbM!7S>m*C|+MzxA?4Hl(V$5W1~A07K2H!1Pn~&Y_{*Lhmqhs3@wVK zzfi@C0j7R1z#d_Yg%X)dKN?_5C<7SF-%PNR%zQ_`n^?beuV33Df8k(%bFW{?mA^fm zTGk0bExuEP;eJn|-1|?n>5U4xTIPp*G@ZbGiJx1KimL~Ka<>?99`wKQGPG#3$qKd(&=_$=@*Ue&o-*zOiJ2bMc@))~h|KtyV!s z&#YXt2lRxmN%v;*3ilmp+C3c1YhSo3HX4Uf2M)-*z7DpNk+YWmoC!eb&JBOQ)4+1d z<;o^~s;qTas3!;*dpm*W+ZLw>eFy@xH`Nj?!g~y%oJ-kGIOBHKB3>RgDCEmN4_imk z6`3O2*U+}gyJegyW9)O>zeigilKcQy%%N4JXhr)*1N^MMs3hFvIO@(3JjnPlcw_3aF`dCRK=90H`duoheW?`9olX+q1b0jot@*I8dAl9Jsss^ zHV%Kh{7;KX@F@F!)F40`>>oWU$#gk_Pk4SG)A+>g*T$nqZ}U>F^_kH0hkG8y|C|3t zV4e9k}x zw;SB@g9Cw{+iLM~t^>ys(}@!cJi1#Xj^^C@7$Sj4T3pu_=(OT|bg$i8(jzy{E;|d- zmfhU+WG_w`I5=y3bxP=7)ZGrQMB_+=<*K!n{dKzFT!0e5v}Yh4K530|M3o3V8ckVa zUBGo^T~8cPgM%*LIw`u?@sYPSCk>KS`$QdM#Np1#r{@zeBHw&yN+-O9k7dY zKhhjY>jhBegv@F*H0mot^X>&aLHiqjsM~}gZ$W!8zpe}@jhtJK z+N8M?P2`0*`Hq-w?+H^9gyp8Lwnx?KJkwp@T=BJeS{gXvu(Qz21U0-CK_l|u7(b56dw}g1NpT<#AC%%N8$3V2%upTbO7k|bc&gVT zHO!F21quCGC6bt;0eLv+_nm2Xz%SgbC$*7rYAl@L=PHrKR&n8W9S!`S#PNZlRpd^; zMlB|0w`R2OWGBbaWf+6+SVDL^5SFSO`_}9NH-RPYo~6i5P0|04xHoBXRNK}BCp9Uz zYF<^nw92%~!F`riBjqz%Auy{upa;GJ-@!dyKs-=aUxFK`z!~?EDl23 zw_xw}eQU3^zGYd{PG;3TumVc3kJ&kzQps3Q)W|^Juu+>f_ep!YM$~;^194dJ%Zkh8 zR8uw5UqP9`>{~>hwFO)W#tj4kj?W<0X6*5H*6S6O6o&y^am;M88GwfCr-;L(GC`LK zqv@0e1q>;Y;Mn+reR?7vXA3DrXE*qQg*`&$yvz|si6qYLTIR~PA+Ih3LhSF0Sm<}1 zi|G^?LN+go*lvwYlp1pj86K~q2k7?WLhfV`=1#bTzyRj@^y!FRbPX9yF5#7$#}}Ex zbF!B1moYtR9uXl=|MP(+IX6PTH;NDlqyI#sIIW(+v)mPZ*~n7p{Ky{4e{K(7(m%;r z{mPn|CX+CR;jJ*y~+;EOf;rSd@=x$R3j#B+_o`q%jeI_5g>MC|sa zwMg9=VU9D1KSqiUg0ZUWK)Vr<`w$hWf*j~NU3JbiR`Y|FWi+ky_<)CK-82Iz?&+DA zFAX-=JFq5(-DAh*NCeDo7c8FTE7>{Wf<7)F;w}cJ~K%{m0{NXYNaJ1JqhSgXx5E6`%*J2X+A&Lu%zQu%IX6%|Cy3K@4k-?v6wKbRptosSZ^ zyB?#Ui-A*D*Md+g1W=1;H_fzX+1q2dj4vUHY02AscD47A#QI+S#Fr zjvY{tnL146zU@ynlY69mwaKhB=<%>S(Fa4&9iPZI;0s1v5PZB9Dk45zWlSv;^As7^ zV>$$yA6tVdKX_89tk_uHzIM^BEZUDDobXb3y;jb;qBP?+(nA$GL%w@6IFK@0KQcI< zj&IF%<6rBf%G~>*8SR{*IAWz!MXx**^N&X8%X>HJ@8`RJcwDPqmD=yE;bNapl1*Fp zeeQFovv#HHw2;L6N+I?KeVJCuw!2epV`_-S`YTZj&bz%7K=!$v&xtxd9-iPfa=I*l z*+IDsU}2OAnt5CO;!byL_feKel@sc{MI$i1JR~>HW~`j`g>LJDBbD2Uwb@kc=>mJW z_L(eltk`Rj7ILcrY_F0PF9EkYxq&wn(}S~o>7Mx>4VR6j^>Pk~0^FY;4thW7QP&J* zAzi2mJ#{c(t-m4}Q8rL70OC{8!Hv${+^*~eUA@H`#G(hgKTN7hVfBRtm&;x}%4kwv zF4xO6K`@Km?I&V?V;$OHA;@2Xqri>wF zJFflLKN`d5*24O4n8SA;Xf;8``{)1qojHt^*F#sv+ah=QPrCrHU(`5l3;55?(0dis zYKE+jpT8NQtq4e^zV+K&&E7(S3S)gx56eGa%RbNLmrddi=knPkZgcrj6PPa!LkEq8 zHvBc1+X-6vfOFdwJZWo{BjRqZ=1N?N`<%n*Hok|!;~p$>Z{TXGcY>VYRD!HCqe?7wB|ytpKvC2oB#~DH zpF8>#tUJr1~&7r?obIU7!YwQkOxd1I$xhiSU77< z(gByj+!<#v`Hf^~fko9pq!n8gBpAb1_E)o%?TVyz{6pr=7x%6!>h12ok_^3mOafO( z!n&2wvD$QKAIIzC*q&~MiaO?=rnEg&VQ?E;hYMMC*NH|%>mCR)1>mtMxu7-hd95s} z$*uqgsBvVdNE}~w5#|nwcjQuf$;C{Xz`K3-u0?S!CVtP|yTOP~4s3D+nYr4X>H77d zZ}}3S$6*))-oS*N;)GmOGzev@eguttT8ttxx=A#^mLr?7s2vpQAG3FB>plmX+*7yc zb`Z?Kj`cxuF{0`sr>R2}5ZVTl;~o1eEZy+*pyc!fx#VK4hzU;zYt((T$&O7Tu4Y-p z<|Zh|I@p%wC?F}eY9`n(fSQ*iH^d1#}WIJwX}n>*u>nHj|B2LL`6bB zZN|m@mbJVp)c9th?2n(@&c5EHexAq>ekE=%nK|HAzNl_O#qT9M;{Lm9qwztba`iRd zR4OyldRM8mq9TCa^MGFJQZq7qm(%+2`Buhb_x{Bz!n^v?kHvf`&-ta|9LcqxNZY5g zsV8sQ5lk%gMh$5fbpH8TMaAsMAjyL>;D)&+SPtFbZWOETYW?W&)DYcKOIK?d>im@& zZ2U4ER)^*y#rxV*B-EQ1N-VBms-g&ZuIt&$u&xN1n2VX!zs4Vmez+8zx%FbsPO(aX zSK|^yCSJMfZsxd4<b!EWwHHHTq(H4xzt?m!%xR!e0IbxnJL15W~# zU7V^@@cpcy_&^)@zLX_2fQb44T|9lrS4Xew%3Me2*f$;^B}DXPC2v>WJ>ewct#7F~ zp)z{2sz=;81H#MVTu@mRaA)XvfCc^`pp{)`J>ztdyZ&k3WCd?^2Z)675Gj>ZB95{0 zItv4CA?obh9P^q)Fw-88y}fueWPw#v2c1(}`$?1R-Fyjd#)=wLN^mKZS>oiM_mtO5 zyy__`p_AT}rABjur>hm;No1VQ*D}93PFtTEzC4D>&Ir_QCG+>B@1}`Zkn^PR&V{(` zRg$F-XZ!@>_Y|$PGA|X}KKuxeR*U@uMB>!uAUDm($fY9C5o}*l{ayndY{m~%ji@-L_r8N?SPHxJ|Q2dbw=2M%Mn!z~_Pc*{S=KFWO z%wl}kTQ~J;vnWdK3qH+z{Cry-7;rHuK85s0;a>(~*?EZuLi-kV(@(Y!-k8Dfp{F%$r?6d`}Yj@kh*dE~%_25UKiqOpF;+$nRJS1L}b7XgsE zE~bxTtuOIK*K2YZeMW*TcVc6 zMMy9E6PzFJ9_d0ND`f}5OnE$NrRf2))vQuT=OF=lu@Ym_QE6|ak7h+r?H;6kSl#hr zcRRp)kI@5Hnyxu`pB_DgTYR2se7@XUFbuPN;-s1B&nj?7^zy_@nZH^X5k4Oi&U7zo zgdLFk#DcX73y%krcixOT&|r}<>LiOmteX$XJFqJcqsqB_2{W9u;zeE=*Sa}DHG@9j zaB+F?+TgkAn?TNB8fnc7u*AecA8j-1cCn;_uq<*pqflicE9vU0vj%W`w1qU?l)G2@ z{DV&ofVh{|G-@d{)t+9JGlha91Q}Q2THi@OQ9Hy=y)zD9_3nveIP@5h>E$LV#OG=1j|NBd{fA)^TzmE5xyT-hK z{-3|V`*#nCdk$+u=bvG|@LQPwskg?TWB#9dN!cH~r1$weH^1b6(M$SrKAU{zw;6q$ z&x+|k@|5ToPf6T7rRQ!n^kQ=|HJgVSj^@c)7oT*!P9tbl`{-sI!V7oC_$Cs%%LA-R zK+T{_6ZL%5feeeUb8r?>Ca+q6AvL02o5opT1e3T+q0xGU7*4_JBRTCuGwr$K{Y+ou z39fiTU2?XoC~LKj9JgD+7CK~5lBnv~&|2P)BVUTS9-8Ep-WS6OWNVyK6)&7`V^b%9 z+z=-W4OdTPJGi<8LFIrBhA5|matJ4@aQ)cW$8kPfbT>>YTvM59y+smfqVBxGj2_9u?&8P8#oEFdq#SlMaL3=Yj-mZJ zUozBOZRKjA<3KNV>&>BOqayo|L>Vj!5zUA$@CddnbxAD3J&rK7~UQv&ObD{_|zz!5K-6-K0DyGRCIJ`p{D9>rR1g|st zM9LlHJ-RYc4}_&g)1u@%g`(D<-xe!EB;m~2`s}1cBUdo}MLgh%{PP1k1ID3CvTJmB z2hg_*f1D2H0vb!KOJDSo{?ohv*h|`Vf7nZUwNZRPO{uoPSF#TZpjt3&Y-wASfcW4X zJMzdx{*1NG`L%Als%2X7FGwv8$#e2 z(M_n?`|cJ4u`p|=4ft@rO~{G_5?+h-6-8y(kRKGhv6W?q*M_Fm$)%3noe?Q$et}KC zfxBSYQe*6SLBod(D_O$9Y{n3mO3d)mW2<9XdXTug7&3N(taPgBxW)|!M6b>jxusfN z;^h+wjX8cwMuTa*Rt2t|A%?A_+}uc%7LX?-`_Mwt`5LFYIH}PnR|`aeDZiZ#;$1%; z*A;c5Q`K?~5ywYnAaLQaGStX!;9E&KjRkkReS0kcMTH~jVhcB4Gb9A14g*d4?app7YQ+S2u${& z(C?v`)V7V2Y}=+`A9;Tz8xak?_dgZc_XM@)nt^b9?Evm=1O$AKTIq(2Dk(&gM_D(( z#U0N$xkSs4T2xeD=z)AAPVq*ZtPg!Y1a{n;TjjCHPKp0qCZWTdR^YeeiIqCo_H((y z^ACB+HvPJPvAhmaZQ<$<`GqUt+H(9}72meaoJb zd&ilXqTUJ~Uo-SxwafESUC6y+#^8xj_HReWuh&-kpf38ecKPo;aUW321Zgt$rG z9dHdOQLds_1Cx46As%ZcF7rG}6HoQoXcTq~RqBpNFXq`lbDcsZH;5H?d~D|~BiGTT zKP?Y1AvcHL<;r`;81vzgpi{&i*t{NYrFHLp!=8d-&@qz}&a*diQhsGey9H^N<~wGJ z`SE%m*7e{|W2)i90AQJZ$~&0z4refQdz9!dD}JRt4^&Cx0--b%;Zb@R)Sk4o)g^@G zJm9f)U(D(ujaeDP&MUxXBAX$z+D&t&nxl<)2j)>U!Zk@J97w^{T-8h|z z?J$l-yJuUMdv$nUKe66yUHARU>Rsg6e!sGw$PcPgIX+($FghFYEb9v+J<7dzQjeS^ z(iZ62cta+aB*h-U%`uE+d;o)htiUTeH9F6P5daA-m?r`|*oQ*Vn8qhNgkWo7LU!4* zJ2b4-+z76>&nw9v<%44Gc9^`htBs-ul#C4LbTTDFlpyy^CLVl&adNn;T3U?toLyM* zmUt(d>h>B$U+b>9;9LtW>nP}!S$oqUPVsSR>hB=VF)Qb)QN5nHhVZy(ewc%%PH9;x zen&mbkd_n{I+uA-S7#4wlyyzAPK2LNeoNA!FFUTKYD1P{XD{GHE#-HdNj6MiDHYrT zk2`T8jGc2+i2-rNQRc1S>ECqrq55($9|UeC8|RaNS_8~I? z+#KoZ&D#E%z8<`~$8T`keY|he+J3q7qlVxwD+n?qyrn#S(h!_Z=2p}3qKoBzfh*s{ z9V>IwcHBzQf1vJ|k>I513?ZXd{ffyeT!@ ztM0Tv74&>-E;pYAgB!Vn3pgQUe_flqBH#M4mDvHK`2nViJKI^Qaulc4j~^y8bL>mO z2L`wr0PbXuPJNYZ>Pns;MJZRAS~=9b(Ok+w;3IHB1paZWn-v+*g_&hSm=AYq2?jU2 zb1R|3j)tBg@xp87a*E@u^s<9*nXR+Z+(zMU7!xLh69R%%R|16Y<#4i%ebEy}($ZXf zFvvMvy_}hGMuqN=5+hbou0i*!TjKC87u~S*Uw_m80kW_2ZX|`V&p0RvXUf4JZ9-C!9ft|$Qv9mUP;813AIiHbT^-0u-{e(D>p@7~V)N#$2m6z9 zC6?UNwTqlx&rVdL@q?wAs1D-!jv}!X;YlR=G(7L3D>KKqSdHbyY(x?( zuMo7b-r=v=Z}=ZDxv!#u&n6fBU6cD44d(n4gSklwu4YhS;=f=pe^ngtN72VubN3d1 zR3^$WkXo}qov7wX5AX4%ZPo5T2qB@3wuMAfykM>4a()Y;ojR@y0|qe)C$9IKz~hl~ ziZ541VA!lZhB_l^CctFv8QAwK8U{9hFgme6O)!Edp0wi!E=-?Dsq=)`j@Y>H%)xan zQXlXea?YLRF!c!!l~CfxvW{HIe7P}4fn1kM?W!WdMNUw$>ytcAiFP=))!3au2pKnl zo(F-R6~emkr9L0vn}*e1OO4 zksqlY$K`0h*K>AvG46(W1JR>u3^aMN#hKZi*!EsCcyU-s_hrE&c78M^rUcX3g@ID($3$JR;N z)10Q+l0NDpjKpQ{4T2{Yvl8%Q$SRaSmXmsfz@BYZV}+B1gE>Bi69>LYBaXck3`YB2 zgbtF7oPxbk#$a5-r@F|boe4YVn{9{X00XkR@ZM@_VEQ0P@KI%sY0oMNbr+S&o;stw zU{Z*WNOccGdstsUxI-WY>dCM;_72|NP@H_6kSB5{?^i4cjpB%A0ddNE zY*lj=eR~MTJ)_V)5-Qb$5Fi*8lKY!Gv69VS%!lDvh#Q?4?U7UM`)s^lXCyZ4Is&I& zuYXjc`TFkXd%uf5KFfdqsW_kqw|s9C+-a_5<)D>3+<9gyD}8Xyk70R#A}5Wqinz=1 z1LvC1-T-9ja-E}M>@U)O1eN2ZOHrV?b>bB@f#i;tDMnAFvb|h;q`Y%W7_L@s>o*`!%z>9t zKU5Ja&-gB0#yNA0GDX!KYqXcb1qfbe#nef8E^y zN?wjF}w@KHlb)1j%NERI?MGyiOrkw?0$O&SAFqrldEr z#s}i5i&p1o-7Qn&4<>r!;ZYWvb=%&-JyT0Z$-2VHp_nVlo8cA2$t>3Qq?$l#Rj6$! zIy9Wr5I-nneI$IZPR6h^SP#z9mC9wqg_mUf&cynnxCiiIT~<0&0>mSA2;y?|JedQf zEV=RQa$69Vb+W)3=ACyxWnTZ_?R|WdMV||xkkc}3UkA3f(g4ysK^F>j5y$Z7B(Ahl zVc8oPW;&_$%?biap;?;0*Pk6FdS8`$)D|OnG+=ga>LkW`t%s(?JUEa>X z39KgmRsvbdE}T+pHU}J*q1VC-KwOeCX29N9Uyn7A{`mC28ZKVuzH|bb{O?cymz}o$ z->!63uD~5$-I)X2zLtV+ZWn9kg{hn03eM?CDPN~_4DOp&S6J>}Ccb4; za&P(`)=Ey)C4klXy;UfsAX>dHdZ0eiC^DR|A+q%k%(}_{w_f+{z z1fMHgGIn+{>aop#-HrdiFWBb)+TP(ee?jolZt%k|`1Nd3da#MTBb3-W2e_nHAM7S` zL@Q`0&6#)_b{zu-AfS~|mcaN?yf96BGBrVf8~K=l5A|k?ttU`qj;_|ZsJTPSXk%*_ zC)U|Px`YRYZ%YRRgCCj{3s4NOxfg39oS*`t*$cV|Lnq*VWfx;)uY0Lkt>*dQ##}kq zBa9gRn79BAHJC*&^8+-s{ekd|C}H6WFKG=P(Ly^Ct6AMy2>4jB7AA=KBrZ$wKql_6 zocpZfoYub+t$RsFJ_4w+k}AdNnOX#LbXQ3&&tD4@M@f^~fh+$T7F^OeD?XjamgEbt zuo_&HrTTicR)2)A5Nc8nhi<2iT?Axhk#dqk@=MDL6E)nntk&pq#oQyz?g8|@xL(hw zAF2d??=(Gkv*kZW_H_FsDw;NrK*%W9#R_&5I>xAaW4bR6;`w@NjY~)Y6Y5OAxEIG$*$B?0oF z&avCoTDhuZpu8 zq#?m%IelDo99a&#dRcNuY9;RofD>`Q@Zu!6QO>o|6L}aAh1eYkS8}Fn;!U6t=r>Bl z1HQb#*#36%>QNpooVQ%IJg>Bwr+QcS4_q!Op%$)tbGKsxuTe-cKuTF{j0;BbDF`)p z1|$&O=7GBpRAhzkNv1p=Za_jK*FF{RMM6#3EO$@;h7kC2SAgu{V<$*Fl;(<1TrI~J z%@v+oTm3ha{@ZiL2z|YG*T;YV8-IP2{(b)Mf1?Y(ZT|+pHnzgQvv#auvid8>*&;{6 znddt9zGC$9?K(Amy8hgFP<}Nolg%oxUC#|>(~)adVR5a#wxT9(0U@@iXG(2DcSNkuSSN*F|=WMAw1R=yJS&xGsFU z{;jp8v?8&!dUrGRlA*IYw9oc8-IO84Pqp2?gT@BWfvb&v52=JRB?Qh zm$s18OXFPj{pw#?VXLM6$r}6F5_@TG`KycUt2OrABKNme+1ny}`f8c|Vx!#(#ZKV* zD@eMS;*nJcbfggV!9UD0Z`^8t+%EJvLZ3<*@*aqkpXIhmAOl+9)%Z$`Ss?N`U{(ub za+kuN5g}Qhy>L99;?9)F9zlY`Aw*A#)swb~*ALLHI{#idCK#M)yVh!eo#0H|!5A*SO2H9&^s*+bjX6GD#5e`?E-XIuWO7IXgjTK<7iZ2yW;{B?u0 zd~J|^ck=&)jVSxzXl{f>j$!p6QKX-Yrz7U+dD-dwc)QAd1TQDX2gGxAsiw?NI~BH2 zI!XkJ1|;ty% zJ!(&h<=?cF)~s{t9cYLX9VK(d^T9mQXnNd)?92cnNM8YnemUXMVRyS;674bY4TQ>w z2|JV0*N~Q%``A0(V8CKnvVpb4TP0E!XtbtK?+yLVY*y1LfF${t?y4NtS1eN zsofEnkUZ@m+jXt9l#N9{)6w~mXP1E?TJY(A+s=pk@=eK2Rv7dw$$Q(3GW6JoWHfaXP9mA-_GAkr1kX?e^ny=#VkH4 zvHzucyiH?e1aD0#GZNlHYzm{p%JQmauzbGu^&Iq{1i(IpzZ7iR-nWS?>M=xJEeF<@ zp7A@oa(yYkhp9#Y3u|v8!t2o(=Tt``wfcJKHO*vi)J4;ljIsm6h&Zw%zbsUzaKJyAcXHgJgd_P0^mDgBu z)D^jS#EGjNj}Ub}P>l~{ID_odJA7jgXYa<+u12%KSaV^uR6F!wUErWci;KT0oScF~ zp3i`1VAPBOP3d5OWGz6-Z0$i4t|xXy*_;FSwztiL-f*d0Ym#r%8Hd4uxF!COQeDv) z>nA!NPOO*3RSJs_p$D3pcP|#rk1I-4of1$my^Pt-b~NmOY;XS>J1o#0CwFikI6N60UTzb7S2VdUOS}4%Q5A zx$gLjn+v4AvNQA45=|Kma%8GNX-%PG1$8V+eMubxJEP0|zH=8*_IC<4O7r7%U@CAd zjEth0DKppSFr9~(e|RxRN3C+dDuaBSO9fCDX1AYdgXUE40-W4Z$MR_J()tpsmc$$o zjnGi@3ax%Hoi!%AZx0-;nC^ZAKsUT!ueM@c73$&I5uf!FbW!wL)#icU=Y(B5xs(Yj z%>^Ik&L$0$T_nkI zZPC-)r~iWSWK*KHq-*LUP0f}{+;+<`d-~6GRaQ~;x7ADi+xhw!((u~;?eTojC46_# zl}Gf4EiN+DQP}j^Hi{y$(5Adrd7IqpMVwgXC25r+LxqCWL|*s65SIGHXQcZY<5Ae> z6B8m2Hh4RKXv_I6g^XdSWHmVKC)u@s*(CI>Z#r}@iE_`epsD#;wCnI4FBkNT4YmY> z*Q`NuJR5K7{3)ewf^|OsETcAm^lTV1+7iQ7f904fMzum7|LWT?6vqTGGAQ>{>ikf}6qoHk)y$!jU5(d;l{fDB zEuv?_o({}GSI^T8v$ z+Vd^{|3gFKZ%L8LYJvGJ5&8TFDL?aFZf^NYDGKtQyx)u-z1DCtO{!s?PeUGv@#sh6 z`LW}v-DywC7>;$3C0352_x{${iQz(8R?lb1a`71s%7mvOt(n0wALVH_`^Uuj2hcuN zXS!9fHdAA?YmW9!NgYkQK9Pt53^D4b+0oGMa5k4micjd95IulwVXQWVP{HFJdSuhF zISPB%BVduEt2~U#?%JX2+l*BQ7b~CLRZ}%#ad`t#VsP(Dkqi`IKYFdTBy5K??c%r) zv1Dq+l`Fq)l=gCEJmZOcoXRU9#8?*M8f?jFNzM;>kDtisFx>aW5kDhaph)HSDP-W6 zLW+caPGy2y&$}28%N2^0lDJ?FuHRa;!RrGm6*qz$Ygzxo!(sU!`$N|k>wlvJeGD@atr$O6MR$Jh!2qjs z@SFm&oA8_o}SetKSzS?Z>wqEZ|QBL+K^zyNW8*-PJ7#Qr=L4G46lv0S8Kw3 zUYquZt`6lo2jBEMK6< zqRPt?X>{N)^;9$B3P{cqQFM?$#@L;%?-b?s4~7Po3-izdCbDVwcC|pNbxr`@HC++J zCcMsp7;%)V?5RF@xLt?}%@riPwAh(j4#$?Mt|apCNt+h|tdHeWJ1Ah8{UJ>cFCH?oHg#M2LC4Cg*H&E(c<*>f# zf&S}TZFFO%XIq61)wGcl_XGL9wB%?AU=p8ev_{zwz;U-!gHwQaFY(O3jQuHF# zcFicrd8Nd)agaSdYVMayyU*l@JmnTh;~6%$YTJd6V2IG8emSUI2+F`vuRkkSobjQx zfld(EN+vlN>9r%t&+YwQFsDoD(5X0GK@e50eI@3B3pZ&;?{*KmTJ8lryXeDhr9)C= z(=_HQw2dkX_dzpSp-+z;Pe&jPs0TTLfQ!PbRplQ-rQxw**s~3s!nG0FK!DgacJ=qH zzHEYf=?OSN2DVoxvjM3+cs?~VA~zaJovm!O#)#mn=^4o0+ec8-LQ~VL5aRkAUI9i^ zQ!`a$GLG*G>E(PrGxhyW9$gUsQ+U~gU%qSa|hX(E3A9 z({EyrVn_vNCAt;u@((bf9aFPnHDA6NYHQd4M03CcH_f55L>(H6p@& za0O^gF65@sz)hE3-|(X;1Cq;=GWy*eE$5>iTub zlLsJPs`1_rx5G`@aX1E;7oNH9E(kk6*emy#7r_X$t^0E<4dI=+qM~8Q%!MTmqOt@X z*DFcTmP+WPP+6){%h@w*5?E!nqGWA2D8|Ki~}hKMH58QG9oPChJYZ`OTjH>sRLd z8E5oooaujtGr(`-Org-q8_tCGyGr=?{n$TS_uQ)JOAS2);RdxxOlJ^bo4b;epo`)ObZ>JN z?K8(0Qm4J29A{I=JDw}breWwFc|zrba&kb-+#WbvoK7G{o~nrr9&>oneeaNI3O-kg zzspb}0)|D|DyH}fKZpa9cSkdt4e(AzeW=X@4Rvr|05d>@$Qi8Tt9BcAZ9R6U+FPx~ z=}hnH@gT0Qt!#rc zA+{Jt8tbosPB6Og2WYnwTStZf6K)>_Oy&-4o{l{Z=H|mjQ9*A9k|nz+=oak!;M;RL z+;b`*4^L!n6=BkBwjI1 zd=g#wD_3T;0JkR>WTEMC#B<+8z;xqoi`AWmp_Q{VMA*T1x_@^R1*-)Mef2 zQe>9np*}a1>pS7;Q6sDKQJ*JjSVrF88Q#K!I_`OcKme;@lJYxf?PfK@)ZAk&W=9u$ z_9m&V%erH;=N-nL#Faf|R0tGIu)C~28YP5gujw{q;^=bJF*~AH&%(h-4v3Saq^>kx zjl#iPSWzPUy9!VF7&~-GmDQ=&ZEloND~$FS;O-AM z3lxjSI)?z+%~^PV+DDXlHAJq zsr2bzMeY1II`Px{3N$qNhB2#g5SZ_%wXJ+OACFDiIwo5_QNRmQ^Fp^?>@?%^>qJUBm!I37%sx!Z7i!UYDDj=K-~RnS z$>j@l3d9PX6#PNi>&xzXE4&)t2mM~8Vc#3Mi0oL9CE$-~znCHvOPmwxW$`||fU_e-tZt)r*&(&Fc5{slU}fBS#s<*!EOS$FK) zbEkFIVc=p6L8@Elv>Lzl-}C;RRw(95A2#(9##-IghI)_Ob~ z6U{nxRlOoTeE`nNH4{u+;%T6+0891LL5=gw@lJB5ZVC?$Kq94iN%Zqp0H3%l7rK1Z z9WAf130PfWSzYW)_Rw#7hOg9t!$uo7c}AROAKq2?mGKg6e^(Y;S}RKobF_NEVlzR+-r`Id*jj<99oOMIsU!d~myOWmdqRh!Pq zm|xxE_$Ja>%WaHhH3HS+jpMGufA2eB6XC`G*@_?^g{`IWr^j> z{@nzyKkVRjRmpkY8z~x#tk|g;xnFlO_$+|^zAoEO>vCODuQ2e7>+(~#^3A@zD$f2@ z3-_WF`Ru5#E9%)*&nYe@zQD+h_LCz+zC=1dL^+jLin+yD^wk9dcBI0;G}<}pMy~MU zht^5T$4FNMPfXi$_gK9Q`;YG3Z~mz4ErwMeEy@zmG$o1!;o z?c?78S=DftPnhw?W4<~>RA_r|?mb^E?RWi~-+0Qf;o`?Us}=h~E~&q3byaK)k)GG$ z_9m6BPr@uOB=UJ)Ix8h^^`pP^V&$H@IHuo5SyvVF8cV=GkEXEl$a$!-g4<5f8QT|9dazz{e!s9#ORjt+PDi}pMyGn$|qaB z*2~_ z$sTk$t>Qo!>afXC5=>$ZL)4fM;+wklby1lk&p?7Bgvim42d$0JU3ezxmG`s^DM1PW z^DPtmS^(82TRZ_~aYxHsEW=|xs89cI0Q--FuA~N&DNX?2V89ism;;Wjkr8K`zLXO2 z^uIqlB{F$tp>O^OoBfmgk-zLhe4|zJ8?Ab#HQ+?a7HCaBwbI`PV7?#VUPC5S+5#L83e|LmWTyHzT{@S;C zL4xNX)1;W<`U}WWHS^{45@dq5YwAL7mgnW_#$nEX#$kTMV4nSsU*a%7VlXfH)4#%D ze#BrlUh$d3eEa+D+uwi2Gn3^^!({)!%~yW)9XtOvg7TXq{JJ+qQ}=~bH$?tJD-bH% zl3m@L-g?cT9DA?FA$kz}UWa;G1#KtncYfE3=fganpU6cB_Eb?0m$P7jMT1tVx%dMj z`7TtRwFunzC_G$KC%-slc+RB#ES{CJ9JOTsAh@kfqFc&@yL7~@U;5LjP6>XlLKjIe zcQ|Ue4=tWS2LwzT=29W!y*iAw-PoSsTGP=J6#a>m7(g1vJ20jAd-rmR-By_KT_unC zXs2S2<9s1VOV}qwSv{m+#q@|m>F0))`fG*k%CpUOTcb$)gKs^5XLU zv8U_1_=|q$_g?3-+{j<@JHI#2E1v$Mf&P(w-u%$NZlV8VgkLt%-+1`T5$;z^{8xGS zwlBVB;=jtnpU59E@n7ZP&tZixnfR~r@YPm-&BUoc@NmkVKfDv_i&TPjorQ?KvGnwM zrhdRX@olwzWR)A|r$&b_f6glvruxKjS98>HY4^RH+*cg;eU#&yt$aOF$Dc=BQSZ$w z81sZcM24ZJF@fRU%9oLe&x@ly{^_M3EmO-r3tIH zkl5$<60@@5KHm86x;MYz(60=74B)ADk%IX=PilE-D}=M_zne{;gO6p5?2`4l{?1c< z4J$BCtYGAnq{OFD+>*=uujltZ>gF}Ptg~01KYZ9l>nqnYg?`QjecyG;`tDfx{mE`& zd_)~TuDKueNs|cX0YndRE{X>3G(|}dN1NK3!Mb$7D5dnA5$5 znBYJ<;MCp4`EomSKn~uJnB1v;zUM?hX6tOVFenE()uGC?mL88LF9pqlP)pS|z*#Pj z0pFA-rd>E2v<>c5oCGl~^4mEh(7iEBc*PgAve_-gy3qxO@ z)bbDV;P>zUk^Ig7Fnyf=1L@-yKL*v$^zpe7;q%phfIj{;>Zbg|)a|Pgenz~S>9%7Y z4DVB&iF4BK4m|u_)tZ#tQrRMVtJ8=Q>XP{`-0Q2qF;+08vQlF_%p}b9dNEUJ#gop> zcIa?SdS~CbB8Oo?}#Ivjoj$lmU%+NF%?~#TfUt+Z)r0UeUxp^v`Ym&C? ztfV}$REohbm$7UVftrleUSaZl;Yn$4QEl=w;&n!I0PkmFY_mqbUk4Pn&YQ4F$+e35 zL*n)Ix1PWAPQ3mCWm*1V%JPE|egkNi&p`Xv&9U+w$h}?t4}hropAS*We+Wcvp_I4h z`yUT59BhZBXRkB44#)oT0A=MO0?}$=n)Wu{uA??JW&fyMElTqcjot2+2Arlf$><6v z>p;<<7hU)y+SMGFO#Pw0^f=e|Tcc72yPg9g&8RZ2v-=$a4#2*~fV>S4H0V-j1QD_RZrNRaVh=KB*yP`z{^cED$PZ^g9P>Zy z4Cs?&%J*kLHuVeV6YuICD`1r01C;*iHw%9j7W-wl`BkF%FGP>^f6h73Ck2x~!m$3u z>A)W#Y9YHfh_2-~tI7Mx+xpqNdexu#lehJ=clBIH^p6~FuBQLj%7xnz zA{O12LCCBD&J^Vav`O69XZQ-_a2Z87)T0d0W6SY&*#l=#xEIAmWkpkt>8_6;98()e zk|iXIOQ<6V4eIEt)1%I~^+ldfF-Hjdo1gGT&8CfftfvM+h?`?o>pLEFMll4SpN@y) zc=a_p3n<{M6Qs{T*WGcQ!@gmIWsAAr z(8CjnXoorNT^ljC7%RuWHC0{d2(ekplS}^LTi?DzC(q|VVZGkTGrzm4|FO4udOkV$ z^N8fn&Ox^L(w|2pe|8S?7H|ANksBz=O9ny&mxjv#v`BU!wd4PWa1|g z$)Cj|Kb$@MAR_riJo51UIgrlE$#;>kpZM9|e#Ji9${5B`|4awh=d3u2^9%95(uTKs zs#jFW->uj;qN#sJG#S#7zdP}HiJE>f_9vqGan#dy@yT^X(WoQ;g$U(`XYfp4w{&Cc ztz@8A_;FMka;#{K|CLccL@Yl%gBJ?!$Erlm$#*A9U!8}174!V|rv8H?mI8*Q%}lN< zO4Mz-b6iWG^2r-%M^wwX0k0Ge?fkAGDFSCywhD9HkR6(Y)Vt7CaRsrBQ+ob&orAAe zClnTX?15JKqS=R2x|`JQcEhs0y9lDc4^k;S2G!|$QELdfWdA>LZ_?zrmaK_wv}ncZ zne?7aZIar`+WMKvj0=DuK@f{tV4gwD(+&fP86ZIr)34V7BO)_0GV)GUrC#LCYe*6V z&hUNie%wE2JE%W3$lL)#F_d5alSiU+pK64JsrzkZ^QeDBZFSEJU{?$#VVjuAeKc3U z1w_D5&Ht!T`09}pzxs!N$c|e6#dg$Uk6+kP>(#$(NBwylM)}8WnB_+s<_}pQrvpJ~ zei%@PKD_7j-G4M#3k7{Ys4oPEJvcHK5oH|B$j)3pk>fOXav4}6p+!~%EEKLh@jGsC z69@LR6AvA$tT~4?nMS<*LO3^hE(|V!E&+awFN~K8z=0E`-Z)8whA7ECs<)`M6?ppr zh6+j)9ZIbO-Al?|&70AY*f2;1A zYF+rH1@ie@-@hYe{~+tlE9?D3j5Du{^D|#4{}R4X{uO*Nf8&cE&|djxqW%95`6(?f zFmVW0RNKJk{63w}BQV!S5>J5LtQ7QdOHg(|`qC9@RK1E6;zkmb!*{r@(N{DTZ@F{b zD_|sdGjc!Z*sf9X%Z#e*F}mKSIx)<;(n1KusTnr`+UuQZFqnB-Or>XZg`q2bl+J{2 zzWgU~aSg@XibZsX410gyp9-~)OPke~g4~}XvGInN|9wUKXH{0xcHio@2$FU+!OPlm!4WfY`Qc{D1B0w4MokyY}UH z{G6EoXUF52n6JN0=wBL-=fUr5^!P81$9L%aW2w{B9o!jimgZLUV>QXM|3Y{{R*FP< z6e3OGBKK4Y2OhqpJMG2`vXhr7?L@8T+{q&I?_#*<_oTHwraQv}oJ>jw24S+iMz_+5X6ePk)jne{8*cwXsE^IEkNvcG zy?tH4``D#o2~KGo}bkJaX)xdQo!PRwpr@m)}8 zjMQuhHO;Hkw#cpFP2uG{{7$aqhgSPgdU>s(^xf_FOy~aHa*uB} zqCekgy^s8dZ}bOZ82{yCzMXKQDT+LBe<04f|L4S6VPFruZ}IL!oRtM{2r?ueu^~PewaIaXM96OG=J;dA z{b*G^4v_vJg)9K(+d&KoHHaQlLG2*_%xnXsJ~-|ETAZfzB&lkzq%c?CLgo9hu&_o{ zjpOGsT~mgE%}V+F+~|!%*W{j&8)p8J15S z_TK>D_tS$v4#A)E@m`33q~vnN^R;s9A4Bx@xbr&{{}o6F>){Jf|1+5WLj2hbz0Yy! z*SYvJ8G4`N($BV5e=0-ob6k4uN&M$B^ghR>pLvIWDnsux61<_@Lh_$|^i$^VznB~H zvseKz$=8k=WNuHPSb`jMVOVwV-pPcaQJlD`xA`MfSaCtj?3^N*!o z{3`L{JxcmL^&*Hmasj+j1-x`{$HTC+XO_s0o4xi35z&B`lhL~*dPyzc^UB=|aZuFg zt{9E%X5qa$o~WTywdKmT=N4IJ|9qX8b_z@_V*~=7L5&+hga@^ROiMVlJ6j6wCs>z!b6l0spJrb>Q;O)P^{G0x0oiNGo z--UgD(moMY#VAMiy{*hd8i|X$zH3|ofqbPGBQw+V><}ma$sECtH~y)`+`oTygJ*v^ zc(upn{M{aBPFtr z4)68l#G+W`+IMDA7WFAx^Lj0QUIY6HT>VF;4j}ZW$@{Z&2k`YbX79^8e3GqsAwJlb zKgre%2)C`A)4_q3!?BL#Q`TJ=pDe4$G6rRPSRfxDQ(NC8ihi)b*7vD7`#3ywnrp6 z*kRGj?2&g0Y+Ie^n*-BZM=i0-*@-79!+h6W?Jt+2{L8JsT&~b{Tkh`Z&b&^@;$wLw zWoFB?aHBJ8c5CL zQ9Gp*vOlTyytyXVJbIw?q!71CumtpT36d`_^tPR)KySw|;_s9|aZs7xe68zfA!;gSl{ZwG{thRU$ zeiu9yV0r`x|5817JGeFkIk}Ay`z~-l@0XYtvo-b4c+~}^UhnYyU6$QO+aLpXT7DLX zB8T-^&R~xUz!rTWzRhCFxa_A>jdiwO=Sq3_HT+}eKB1SR^UeZyh)Rw-kLW3E#%gce z<_&aF1n%+eBq;R9G&~XS-}wCb7h?TJ>vPOi;K;v%RqJ`)*F^J2vFcqL#4-LXR;8(a z=qu`}ZTw>g!j*N%;@9fod9U031%vCW)NqMeuCiK!U*J`HVTE$%Cd0|=m|L~S_fVDNNy6v z#?;_O>lT{I(Z@9q+RMIz>=ShEl=7y#0HavJKji%Be&Op(;H$^2<|Iv|-3m6Fa6BVn zQBn1+xg027ilI5TcQ&SYIofo(nxCYX|J7`CWTXH6?=Sz`XFKL^hz!j3zeb1OmELG( z5}%*>_fKnu{MOv|c|wA2zV+!K_42FC+|jUpboliWo?eI_PJVva(BHL3zbl+77E^ur z3@ffV|GVe;Pzuc7SB1M`J(SmhSs?7*6z-O1yMotiPxIm9$gC+*|Jc~9f*L;!&X}gc zuNux#uMOvfwc#AtB2);*LAlBm0TXg~q$R%_={+xW&4J`BZGd8hiInSgjf!XtLn&nS6*9yv|Hjs4I0v6aK%S{$&{86|ue(Iym z#d!eAoE;2XI6lr7bqB-xtm;)xih>IU;4H!y>@@Sm=5$ZoZ>AGB0z&M6tqn`P8a~=L z71*AY&H}nUR9^i_Bjt$Fd$tPYQ#?4DObw3Recvb9#ZD)9!DK?Go-!PCbd(L zyK9l`m93z=(jM}CYz{kq;CNI5_~2?%5sL0MjE8~sFU!)9`{6F#Nc#>*_23_adj;!9 zxQ1vmi=~Z7o6bQ9?9lEjm|xogz?-YB-t;0cxV?pql?H>cDz>LhM(iWw;bd*PaJXNb z;fLWv?jcYh6l3nYrE`ULiC>6JzUL86?@`4^54tI6%IR>}I)})o0#S4B#AVo57-hSg z0SC`9B}l5Ou3mI<=Cry7h~Kh>V5enU*9nVFn2G00KG0ohwEVmaA21Bde(e`&n2pQ+ zpurCqB_R@JZhR7KJi!GfR2>m}iiWP-DX z%NXyYn(|mA9XE|{%JAB{I%90kJ-fvDz`0}5}^J&8HjZtj%8#Q~Rm^F-@5p0mS6m#k*C59hgm=(J(EP>Z! zL9VeViWI(g>>^|TPgnxqcGdEU$DiD-{>bANfvB|ciO0Wqus^}Vv;vG{SO5;}QrD_x zS!Zm6#pjE@R8kOWE!(CrGH!#?v$V2H8C`-YbK$x{K#WjLQ#xw_1MagcW@SmTZ*Myd zEyLy9XSbtRU9-)u$B@WtG?nZTLas=yV;-1I=GwWbHV09mc*h%dHkvED2 zw*Gx!JMJ9PcP=`hcmow@Oc}3NG%rM388C>wh!0M%F4-BZhyD_<+&)l{x;gDlu{;`< zW1KSE%ZQC!h%KGV4mz5_P?DY81-&470Da5o|aaOp?W`Wb? zUYa4Fcc*;sQJPG2)#7ej`#gl&=s@`b?2YZzK^0Db6_v}tp)^Vh4-XeR-Q2iR*}oCy?Qc#dm1T6a-Y;GG4y`l|yE9K|o(a1O9sqpkry&)5R{`PV@N_7@yGf1zrv zzXs*YRQ~Fio+W4BRpIr3PheGeeU8X=9_wNEeywT~K86AW_gA3+wTU=9_uW^edKpol zm6(M5?MUoRo%n}f0hmh$Ft5H~7?$IWa`m^K`qC>lv<9oQJy5uuvXcAMJsIj~$kI&o!8fZM)xM zn|n4akF|X~d5??)xoeTB59aQAzm8{jew>)*fC%}7bZongf(#(SV9Kc~1C-myA~Hhb zo;kG2t`x5lf-TzVL5TRUy_UM#Y_E>14-V~!!UVR?G2al=xt(p=Hmez1vHAi{!ag%j zA~Xk8fS@C8R0qeTnXPCCHljtF+#E-p_rt)Fx+Tsy3q5s6jo(@?Ow<>mNB30MD8#I` z1+gj6Lsr3lO4;))q{G#0+{mTE?hu^4+x1wsr9C7t6kSi~Elr5}09cRqNoEW&OIVW2 zWA7X#RCpZtLzRQU-bcqKnzzZVdGJu$B_=eVa5E~ShNPlGyDI^@YI7g?a(2s5i^x?A3<3UQn2ikZ1maC0AZ~2R%h==s%ms6)JzKzFP_xP54*uAUm_m}+Ao$+dk17XT;cEv$=j;7_F*7~EK4M1cg9i27RyxXizmJO*S+av3MK~JMtp3S#OqV! z%Jl8uU;f*(e@))cf1}#Eel&R;_IcZUKk#9RPhpa)t{Caarf;Eu5w{E*SOn%<3sLGR z_&tdWQN3rj@YQSUivRI!{L4HW&~Kv;p|t0oTlsbZ*0Ww#>HP&Rxx4H&p zJgw@sv_6AB=xbfWkJrGY7hgZ!AAPoGEb_~z`_;2Ob4NZHb5GfdcXN(>HRqVsoCDXW z1xM##E0GsUGW>u7tyi$N8;^5!d?D})^pGC(^$=a7fRe(xKHg7xaJ6+e z5c|d9x$87tuQ-3*B^Pet3NC5P0^7$C5r~IU1kWIa&Hz6YTDv+mJ@^Mo%FG?JKZmnd z*7cHm*bP|4Tuqv8hKrfnO})4c&&~!TMmEC1Y~D8|;$xJ6pVgbD`6Box$`gBB_7A-X zW7Qnc?SVU*PBmMUGG-&||m3*Uk@D@|Y_w(`O_$s3vrGDI~kZgI_eLLJbB-%ob zCRGOyz;O~FB9(os;nWra|0#DUjo~^7)tPk>uc+sA04^r3U?XqRKNINtX~=pm|Ht}K zh75JR$wO&vx9ELzw=AZn%w_=3P*%GXPShJTJT^6gK-1{-E&z01qjpGZ_(ZK8Ofnj* zgDvCm%|8~KGk4hXA)=iq<^s%byTJF2gsa_9IM#6!)>NzOR%K4=E~rOrC`ayb&oL=v zgi^2P@xHX1<=lj(qC4}ZLDw41jcGHrw6i$RJ2qGK&|L> zbhV_Ld`Dk@Ba_J(|39;*NXmrb?_d=GN06w;$TxmY={f5EcEI67U9=f%VMBubB^#7T~wB z>Y9r^`{rBU<%LQA7hvGMf}yY`i>BWWuu1VtFeK1VaKo9_+NN-Ygg3z9?Dc5L6SEJo zT$-84_h)+V*w~k-%=^QZjdXxB0u+9FkVsUXrDLy@rWht2I*)uGLPUd z3oL#FuS#~z3IN}EUM`QKyKA&9I&%O&u4ia>-7oYFtG#)T4cg>1H$j~Jt)l7vgfpUD zz7VZ*tH|MII_U0j)%iI&mzRO{Yr91Eoa^|GuKFWsJ_>$vyd4=Oz@bsEmqnD_(71Zf zo>s6-+Fk`;ge2_-j@i(gThrm8McGGOWV6dME7g78J+ixJK=PFm;k!VcyujL#DJj1Y zx5PNhg`-oK6H!6B!XEZ0!a}NYWa7DPC}Bug=CM69*Hhvj9+ccQonErc47VX62mlP5 zp_ZKmRi^3E1K(*@w)lK5GIx89f-bc~-gDS{417mpG@T{Fg(7}$kGl$0uic=&Zjfud zmxxiRE*Kj@$)Ui?S>dO+=IfgLy2l44bDb=DE(h!us z7b3Xa%>Ykto*Z_lgVFuu5A!MSc)!DM)AhJ@@M6K|ap#@U+bJ@IBIn7;3Lp%1`wPs2 zw`HsTs`Dlu%>Y%?E1ZlTaV2}u`Z)AuteW8%GI^@{&t@sTjvCP zjfF)TlFW)OuueC{7;bofArE+$?{b=J_{>q;w&o6#b%4)~v2W6jCpt8T4e;`aJpe#< zI_f5zC#TNs^QEltBky$CZU0yh?dG?8_L3|xdre`V8jtGY*vj^VT}2^0BeO8=$CvG* zAb%svu2~h>MHWvIwC5Ch!~?;oyuoq9Ptb+@3!qC#zeFwG`WK_t z{1s{uyEWDNt!Lo$>DoUNwemlWS|Z_@g`3O+p;q^V*<}6>Vsd=&HiT^1*xuc=V4WXo zh?pj|;EuC_J7g&77O5~>TD&njG{yX7Bi=Wu$0>;6K@V}ixj|O7J#uqoC0+x_4sET% z*|@zFTkt6_nydF=T&84P$pJF9lA zb`8!XqOKGy5s9{2`-yg0(1SHfP0Q_LjUe-lUO4+JfPv!VT~?1 za1^4kb-hybQR`1EHSLS7hUV=RYvVo7PS`f{QinWvTn`)R0j!RKE^gW5g*Z0;9OY+v zY_w%Z7HuzTx5PF&k|WaCIrGisv}r}6(EPUCCvAotmA`;m=8d|!f@OKd_@_{d443wT zvW?(4@I66-p=on}#6-2(;eHPn=72AG1$@cd-F<}j;X-?IrxIGvj2$G8rc0b z_t93K`EGySYqZ21V(NTtG)x3gH2}AQRl7Y3mt~m{NjO}y(;4s3ncb4<#b-J_6L^w} zeLmF078!0qM7!5}oo$RW)SwtF9RTi?hw}ow`bu2_Bbd$@100qE279qNG5sQ)4(X#Y zjBqXP#nhzOckQ+Zqg$Sfun89AWgB$1-x0Ukp?p}ug=kC+h%0U<>d?+nKOC*uy&#s% zSJidwR9t!af6P7%`@bdU?r-WD{I?35DsP25Z|BvHm2hm9P-OY+{#iTV zZ}INCXc@TsNi2x+9nk(?-dVFkSyWy!+7GLBCH-!Dh9iq2(Y5eZ%tx2LlL->LX2;%vcI2HTjN`NoJmKcAQ>_>9Nr`V=dRsC zPw9_Q#cXR)y{w_88yG*{`q7D&ptr-M*!lmtXf-{ZXm0y__FM|6st( zPA6UnOpH=^u}E3Q?;HBp&NAtnyyNMJA9EO-SR5OsRxfVAs)oAZ!AAN(rsMT^%grmt zsF2jh6x$oj+^b`L&1NkXU2W`%PCzd~Ri_dD@I;3dxMgi?9$pND9Iucfq~ewrj^320 zTyo6}W}H&m+(Mdlk)u&@;+AQ}xOuj5KdOGMBOEa&?yf-J+?#2~B_QnsfghvYF4N(1xrS z_Q<@a7>$!{Fh5i(O~8bV-eZa^YcTp-wcfwI$>sk5&5X3BUQMU(dEfWBvc8TZ(x0H2 z0o!0<9EDWgp;>VlgZ`+l7q_iE?G z=hMd5$0cjA%`lvD1TLn#x^@Y>Zp3Q>rFum+n+(76gQb&62+e(PLb)uBnDEB4+hVVu zIl8uq>wKu=I6OXlu01ty_(H%#V|2_QFO56w_~DRxnhnV}T=xq*ku}sS(u2mFTKMXm z5(Ktr5z)>Bw{vsiLM-uiJtkZtbH3ltawQ^1OP|m=Muj$|OvZKU2qMo@w1;TzHtVH# zSD~11%?S_%ff6kM&LDE7%b{aDv`qi2P`r z6?Pom8|k-fSUNQMSx}hy)!Kb#$o`?#`+e;)szTh~-5#(n^sT6XRolJR^$^V3`aC6$ zYfa6odu1^QSl^Sny^-`0eP7_^$Xuye3@)I0*v^ zAc>LA?t)X;4~1@Fxd3iyYU;mX?gO&dkBhN0V4WKSs6PM?AFtHSe7Xo*BSPEzKvKEf zn@OLC{JrLy7~R8lIyz`_n&C%y@?MD1vI)rW!jsP&DvQ&1*UL4#Bh3`e_SijznOsOx!BjU~kJC6#KnE~%{%TFogR z3;)2Znr}ji+$#}Q?AiP>jP(%*M$b)^wG=BOcZp6gW7Zg~8nX&%z!T_>)PO&ZQEzz6 zVXrZigTpyq8k?i&-mPUHD=qv&6t+q*`vtFc)jr4mB z>)uifePrIn2FCumn=DvmpEcO;=np(5#abQF6Rn`i@|t?9zFj@7VjYGEm0uLC5@-{p!ySs99asHZaY3nRsd8>pMAHIV-7?Qs^ zYuRXDk6BSLx7f&XRXlf7TU&`&wv@!eOb+t$e<%bHFmF2$$Bff0#oSw(`x)&0q!5+3Ka<`h; zEvu(y`P#~ROaaRMpv$zTZ+xl{^}kjA_rQfi%Wz)1w~?#CjZ|>76H#FBC!eckN}7<^ z^trjl=bkZcWNLp)W$9ikc!7ggufe$0RgG%k;G01s4w3mSg@zP3cK)bn`F>|G7r(go^UlSu>!4Gg zc}vL!?0Qi6bTlQBYZd`<34Z;tA@>UrQRwij z=BYo9vHl3S`>LZ=#cwKH((5YHNlG7ume8IRt_$&W=>ej=p-if;+^p;rT39tSP=zj+ zHT3zJaedikF*YbKnGn1XA`&My>B#Y6k8KR$$21;}+Ggj;4fwsunMuPtgk$bbj}ugy zt5ddeCcsG{LN0njuCi zfKr(*RQeJuIhEDgtTUU0-NLhfkBN=#&jhPz$A?Ech!0kE82S+w90x0bh6iVf?shm5 zx1ODfjKD@-it}E46nph^FA`K{nL5>A}fl*c+sxwPU)Yv8xN$l4E6JrLsU z;`zH&<4j-xOQv)`QwA@fyOVI7`pm%%=QI=c2{}fqvhFRqvyr(afLGrMbDmzPsJ6h5 zP*j_9j6|Xv8&YYHJB|+)QW$X;G>5H8L!qf(QXhZ$4^mQTIRSi*$TxY5=l1HB{hzj_ z{*5Lqf@-_#-%O7B?wSdl^=Z*N_VPVaPLT@bIQqmcg!8UVMOSJT@p=k8=q^~B^S;R} zz{x7Mvo?dbeoVS@9OEQVaB48Dt=sAlH$-Z~3H@~4yfvx@hwl`v8}eSsw*(wTQ~GKd6B5%!pdPI{^o2%0k3WKlL?eoMnZix8#cLU_h zjv+ren!63^Gm_9nmB$p_@nNJ*vnNtb&iexx2UL%SONgWbT9JW5@ZCJ@)6I#~Vbj>m z_b${9azpQTHHJB*;50cJW3zS^qLNMW7=RfKL+*&q@Nm@wAChWD?>ZaK<6#i4rzvW= z8>w9H4a~6>z(sntN1E^|in=<%pbnJ;+(*U)bg(^b%j99&ogCojdLmr3$S@evAQfGr zv>}}Py-;#Cq2RqTdZR28xVE3QxxBp)l{fguTXLee##TjFqJ(}cPn-%udv)iOSHr^srY|r}evGgg zt5RLhEz1r9-T;?+zH=&j>1-Il<6a2-UFbnA){usjDiZO?^68s2!z$VELVU5ChIJLm z_V?YhuAnzLhTCgZi~W?C{M6*^lh6tAgOsLD{>puO3XL~v5EFTOZ->}l1*opjN#1RP z!q(+Jox4rP&ZDR{xV|a*xV!oXDpxkS(}?YO>4D+7oO_6$H@01cyP}mGB6W>e3hw?z zEw~~Lb?rJa2mf9?6t+`NEiwDH_^@ssAI~|H|uhpSo zopRJ@Exeqs%nrYvslB?L!V8XQ2&rlcw0rrl^`+q)$Pyi^+CTCMox@O&AYFL*zgBXw zO~zpJ?%w~6sl&Yb$S;q77Qe_Jpb1fE{T-V6=b~~P%Wy<4^hB`Owsgc*Rn+KjmAlR( z;l8IJt_K+ZMPQt#tj&{z+LeU%x1UHzlmQ7XekHmWG5;kAb;ZA$gw~DrPD0>K0SRrL zRkqQnt2y53V43R3bPZe-e$$Kq?{0k099)l;D3X1u9ns&2YPaSAsEe>yEi!x|-l*#B zs)DFi7XF#(``+#1x2Wp3h-w{&->0fCi0a#F02}@m0yH%11~-YUCzw9``m1aD&vgahL7F z&+^*Ch2uF(Q?K_heQ;Q0D<*Lt1W77x`stdSMI9Pl_jXL;BsSdScF0C#*bKnOAEU%ZVhi~ibjr5Xt(qjw4{#8E7sZup zN*vq!4pMUse!Rh5sc4fG&2~B6yMAA=u{2`9yu)hNiAH_1EX{c`da*iLoxsd6dPQ}) zk%Qvm-daqRkjjsJ@l5OlDevqXp63HZjWu|SsgF~V@yf*~$Vb<9(fx4SqVfinpbn?} zgw3x|L{pAB9~b5}LA&^3F#crJvaGi#54@Z15?-42WbxUSdNAP-BGFjypxb^_C{!1X zG3*>Q*v>j}0*)4wOa|H7SM1WZ9_tbLddzkTEDPh?9#`z^2W{+Yg0a4DvGF_N{xk zjK{_IAVp!}V@sQr{`Z&v_9MmdVV8hsJ%405@1MnT#}Bx6d0WRT%eh~yHNp49GC|lm zcuS^dk~QmSJO7HvZcuGY&@YLs`j>lC{F}V|N@Q&H6_J^1$cIm|xVE1u_$R9z)n(OJ zPS-<+*A`75uAMAlZO}x%Kl`hs>0fzz9eM2sxfT_C&(m<8HCfML2PU9nITU#cJYxr} z*G@5+*Wk+YXeIkBF!vYU|NZUP<2?JPH2>d7@}Kadvez)Py&R8dTCvRw_#SeB2i!ky z&KGGjs<~qZ4=WGY;gSkoC5&C4C8Xr?)^v+|`^a$d;dF*O=D4j7s4aqVI2iW|Y`5vL z0642-qdKTJoV-C%d2@_dcVs0viXC!{*rdND{boZ>Eb_>6?N02VJuoU1FIMW5J=`t1 zkVb|-aTtAd_Qk-s(RoXgvqfph#_rC&8f_Mp;BT~3ljmc;ztDN20swD?!wlF9De77v z@7^Id`Gpd^U2DRQl~G->5eK}u~A$Tm+muo20M$sHA;xeF^&9B zIb%HcUhDt4(x2DgDi2=+Y7#qpvmZ%fMpq8ZtGLTn*R2fSRaRm2x8^T8uF&^!6})R{ zms`dEzKI^L;lQ6wAQN{SX5T*zsxr974Y+P+U=JBP01?$nHo&W-iRqngq=IOFpqr1? zfTR6Uq>}qVCXXh6qa4AQN^RFzTpgSTNP=6haZiVN+h8S%Nq7uQ0tkxE^VZF&2dg-o zYx&&c!d&OxYnA(9dwYy0y~hc%;G=?wG+8sFUM-O$cTL;;URL=FK`|IKE2({NQ`bAk zBuYvSYTNLA-(QD#17|+IwF4AC73qOxQga#}&1t%+v@a6mCS4WOMJmhVdtj@O#1q_+ z*Y)5$kw~=+k5Z(iiP&G*8r@=xwuccs;;&J$=Z#&$Kcu$a1B}{vk$bWz>L@*8dk}8Z z0T;anAz zCissk^qNk`>;cluuL;J?w`(d*ENaz`Y4DSj4xozUe^2n9*&#o&&wr zoUP@%AF=^xW-`8j4LlEPr&tXMK&2m2oPRUHSvlNNYg4ocyT26-;RLqPz(};Q4{*$AseF1Nq??#z zqb}kFm2kq%yWiRvVtBK)B6+hQ#^Z4bV%j^Pr?WnykD|tvBW_4Rf^j?QOtvosn$7zo zY7{wly+LV@%n=3)52Ja6_t0xDWTsyd+MOXRjrat)lFcpFw$SjX$ny>o+|3Jdl?H~V z`DBYl2CC7+ZVI(~C0W}8Z|5Py%N>*vhDh;Qmm#7JBZz@Y+U6ixMd2kr^7xP!%bGHlX>)_m5%fBDzBwdwf*E?Lum3*ux`J~GJ zyE-iJDiPqdP(QCKdHeG&df!8Dp3e1kUpxbR1(E(hJ|E9A$CRV;m3(wEEn?}j=kq)P z{qFk^>Ba7toxv9o6dzFJ>bkXbbpnCfx+R?mp~sFF^&&HiU~I3~!=nHmooab7OnJ*J zg=Gfca1LV^O5WMy@rI~TA7o8DE#aA9rM(5nW-_&^6HU9fBmW-R-g?rb;w;0KoguAuhY9|e{{7{DP+@)UE_qjnf1|Rnj?UfvZ|&t-LVWJxS10HTtylot z-%D>LWMStqg=h|5oxgs!s z)N6q3AHWQ66o68~w{+=>4mscv*Ur2uD*k(rzn0PlYbyZrH9hXhQp1{ub#Q;L?)bA# zEN}%FU|cJd{q7>1b$jhL#;!k6148V5leE@8Vy5kS2Oq3#64qkpXTWm5nfGD_%%95 zyhi8xC2-KtNjRELk+!nS%OXr$c`tA20K?c7#rTWS19W~g9t%mFAZqP^#jRi#l|8ZS zp--;qp-7KuB+JKW2uf3dOoAPguw=L9vs?^cL$94|_ol#o?nOxb*fyV7_{w4}#q%dVb$iqee`j?Uubu z<{B2%=VOTF)j2|pL*dfIW^~yi8WDfjTpxx*D}e9es~(4CAoW@QnSlRZ*yjw-fV(Q}& zU&nop6X$*LXRI@Gl673^my}ks}lbA zUt8mjZz!_5jlA;3C}wcvj=zj7g4qNfV|5h5>`vqt5yDDM31R&-VUvam&XH-TMkrYl>|Ac*^TmOXJPyD(Twy}g!bvo2FP2+)3}r!l3`ILMd&c9G@NZ&3>B>d_{^7%~^h3M_BBv+2BEQG3_}A?E2e}dKu>CtXdImBL ze$C#&OSh{8+V@Yt$teA(=wbPjN;CUsIB|)8yjGB_7R47&_Gefh*qLAO{-1fXZ{3-G zc(_cjrxVY`LOA{nvgMtlq8X-5ScMBKFH7m4~E?*{d19t1p zOEBpELg*q~#ka>+TO@yj0iTMO7}Rq4jpH5K>3E9feYF&&E!aUGV>a+k2%?@A78q#7ThZn}^27it3n~ zo4pVVoaxsyWX@!3Z8{RV`$C)mL)pVex$>bw-(j_(mla?Zdd>56U|wkAThELWO&|(wrfbLNEJuGxrFdT>&+@zFBdY(vB+YXzy!OlZF`a{M=aVI3R?GLTN6fQd21oM9k8AR`SWbHHLSvE?*b_c; z`4Mr@Cok#TS9^ktUW1ne@MNEr5G=t_^P8vW#}5iTrNS_y_9-*=L+_N81)mrPZ0AoX zx5}EuzZlWi{w6V0!w z1WJ`{1w4bqNf*h33kb3i3HG)o%)l1zrzeSY{d_Z?RIZ$@O)0~5TM+$0jQ>C8-mJ%U zrE3qnPRF@$Y$E~U&p8PYAcm5N5=jlf_X6fT&ol2hiKIk|q&SKE^(ji-y}Nt&KIiP$ zzVO8c)Z8LfRjgX;S<|yhdv^Gk_q=1~g8gutG~Z4+qPXn{$HX9_1?oM@4L(#-FS-+>JVaD|- zCO3W5BA&qAg^E2DgwfGDm9*V9UcdZbm{-wGt*okuU9Xf#@pn3FxPBb_Sa37~^{_ZT z{|`Co#45V{0p3TEpYkOBnepQnk%YTvD9FW@jt80Gow{hClzIZNPMDR!6 zs>)<9Xc1FD%5K}#VCYs1;W~>MzD}O}eMmf0frO`TE;rp~=*1sj$E=@) z*Wwm9^d;Q3_a&Y9J!M0&emvcD3b5nkg+v>Q34HmX#&=>gDUUTrWKU*=3D-5_l79E_ z|0n!ak4^_ILkRDES8v0ud;{=q^PnSzmMVy_eAmUvFMO}l7tgCfTX&#~&1a1L(Uz3r zH$Fp;OE;Y_0AvY@Nz2c26gbh$ap$>u9=J8M*s193IdRQVT^SR|ojGIP8v>zh9n8$0 z1EAwP2StNzk@?K=CRyC5VecKl=HkQiH+5JZmph7>)xb+R?t;?3CT=|h23EaNM5LBQ z)g1kN`h#}y!}AZn>VMn&De-FD4X;0cK^6!9AG5pF#D$G-6S5`{Ht6|N3lc^>lv4fFO6w>5V?X&cl_ z3u)=1k{t42htXOBB-l;w6=G^yEj!cO_&AH(`4CC#d4r+*nDW!R z8)kP7tllZu^-jEWO1w3SwG#KR5ugWGgZhTC^!c(bHBP{E6i)`>?otY|THSX2Xix#r zBqm=&+SoE@2)xbGTy0KL-L=BbFHaTcDUm-8!qJLV2^xiVTuKw9-3{iAIM_+3HQ$_U zsZx>b<5H#af{hDGx&1&XUoi^)3w)5K{AQ`KSl;H3E^y##Nmm@lr*qF?qILdkmoF2 zQNkQ=eW#efPA)>G&Z3f@!TwLdx3UQQ4gUWIWY#Wr?vMx!B98g|E8t`~_acCGfIrYW zfFJa%tp}dyxl$UG$1A`E(DHfdnCOGigQsmm#-73FBACYX(c=c(0iJu+Qt-4g+{QrC z&_ME`8JJxOu0j4n!OB=EuoYx*PKKox2V8@Iv9?>}w=}RV)0ZGaFc&Wj!7%{My0q z(evjm>h$UCi(4I_SZUxGdFe_A_JN=I@Ffkl_7s3|KAr+*^(HqSV|cy$QPEt8&ZA3V z>TL@!N(5LvF>YN@-n>M>%zZ@19u-1hr58`(vLD_$<9wy%Wxmq#KI)6+*m=e(z#biz zEv+1bg27ljeD>gZ8-^2P+yc3yhX-_O`YcW0TuSy)$vqrTQ)f z`sIyiOAiVn+QKyXhf|0atXW0c?hqp(iMRLQMAa)`1=H9l z4&7R+8(BR`sk6?SsS0Pw8e7N1h|>u{CKKPVVr(P(>C`t_mk}u)hl)r>gbpw&!CvdM zhsu+!Yw;{sP?T>YpuQtB(xF4S%huiY0Li6loUfujOP9tePrJkEkQR=bc`w)j_RHKh zF7p;MHH@tFOr5-@HF;eENWx(}FvlA2j%wCVllaQVR`!8hnjH zWWeT^bnNN1eMHVkX5LpKk2-mvHy&S9Uv`%B#z0?{u!WGz-fBbEgU$%!$V(` z#V%%!eYB;oK$kppj-*#3gT47@XbZ@Z%_5l}GQCfnuJ#;#2MK+-ZBfgC9Umo4V<4%_3aA%-St@*Zogllzq>xmyZC;E{_grL zO%1+ZpBWkeN|rkOM1XE?^*Y9sCZCHoEDp zhzLUk-83d6!qG{-Y)WdcCf!`kCx2cw(&aqJ$MF>TSHnG07O=PBlO7@Bu{-S1LRx9I za$AQbkK2l5c6DK&i9L?ss(B7uYJDE=dV;TvYo!wFt{j`FJiK5|O0JtA_hNO}iX>ty z+IEZ6PDR#fs@;@3A8b}-d5o?7(hO+6A*#GqdeYm8K$aG`I}@MT(k6mz%Cocuwq}g?zCx8sqWSbqJb6o7Yw%cIot?kM~uypGKRxkJ){C@{zalHxIcj0pg4eDz8lx02cYkCvhnUca}D;a-7R4oh#vCzk(=e0CY5jlHPrs&4QNXPxxu!Hu-_Ep4b!2iZ^XswF(_K3+)qneC((Pe*F9B5nN*d z?tCV{Ej9pic;5NpoogH11f$(9<9O`|x$KKisQC>uFE+p%?mYsnEQ8-XPS0+XR`={y z{v$`RL; z7wlffdv2FX(GkG$Lnwsj2(@FEJ=0!F=BYq-TvLlUZ?ztRuXigVEhWf5t5a85Qb4OO z7<9fK$UD_)2Q_X`m9hA9$@82X&(mqcx16ep?zS+F${uO+BHgWIUNR_obF%>M z)Dj*ioH=Le+A-jWI5tFB<_Wu_tvZSX?F!il(^Ixj!W!A-Xbu)JU(wMw_(EAmYRqUjE+$ z(Uq~INY&6%Y9%S<9FB~lm7JtV2O?!}=IHuWzFCI>_wv8LVL-fZ^J@a&-zy5lf1p7B zzZ>u5LLl%4_71`av}e0@3wGj19Zmd7M?WZ5jCpCfY`i-cf3DTm9?r$`Tj%0GIA3SUjn^|%@Kr}lMLTcg?6$cwrWh(~!q&aL z7Ty|mh`_PmGg+X0h8qjm_`B|@CGmD4`uKGK>#x8DSjlhT#_xcQ1?)=SIj;UT*Z@!X z25$Tg*Z|7x8@Ta1VB_I3{|RpV4%k>=!Ow8xcfiKt(E1r}{0`W7xCVcM8@~fK9`2~0 z;KuKOjTh`YxbZt+1H8>|;KuKOjb~E)SGe&zVB_UmxbZXCU~mjG!&^1Fw{@yqNLraq zgdZ6QK(tm(=cSkcfgO(GfsQ0o`x1F|(T*AT#-Vmib;}8b63GPu@nb&;8Un+i?1ot- zy1@%}h4KD!uSP|O#_hB}pF}}#I|>#~4Z6wVNQc&E);!;?M|)ocX?4wP0la!m7zwvh z1mopD$a#0Z0ay^sQf+YodC+L?N>B5zB)m zly@IAAo*ublo#yXjbc)eM5cGies`lx68U~@O#Sv7C(1it_+Pk_{>)jj$f>_}Cw+02 zfN^ne&XU_FXBC~W_yi=(oa7&H%=9Z9L*Uo>7yK*ud2z3K7b5j2Y`2=mkYNOFat4ne z&s%?exQs*SdfTxImlLj3qOx|~G$A=MD;lQo+0I%wv8K-DPKJF4uE@cLXJwp8%Pqs3 z?WDUHBPrn^^qEe)U|X9jDMMtToRreW2%6IlN!=h0ZYM*&TC3Lc_+yE7r^^mb`l*jR zbBmJ_$D86U7Dnd<13TfO@lt%+L9eXt3P1pdF-?W=&)f@d?ufTEn>UBV_bHzEf-6ew z)Blm0L0|<1#*n7GtX~S&vA_ngAfQ$4-9s;YTK~%u{P?hUA1B}uoLNlCAK{s=VVzF_ z8s`z9`6+m{oUA_!Uu8?+wq11UvzMShglv%knGG$R;>)-mdh6539{SO-Z$9|_MMr~E zR`}EA5u|^Hf`Lvq@U8FYwfIQx<`|%V7JT5`mwuuPx~ZS~89bppf>|vyqI=J2Q~$%P z-n8X1i??|Jw?< z8j8-XEaI{5w_psl0`BrsERU%)D2kI#;>OUswK4UOEiu@MFBCQ=q-3Z;Sxa65zkT>vzrK^>&Y+n~i^$ zH~wjey;XYqB_lxk4YkcPb>@|V+I^fOBwFUfK3o4S1*ETaqn9OJkbBdi;8=i>yI5-d zSf1tc6a$RASThE|#F7C$=?<54<4N}xa_z@jeMmfhqqcmd)c~?;A=?1Be0kz)nyDyg>~5a|iJzC7 z!3T2p=6VFst%AR0m}41{e%8eqRym4ZwCqtR+46))(*0Cnx6Ye^_HFg7BgLxnww_?x zK*M)iXRiUQbq9MAjZfy54i1WmUV`Dun*CuXt)XCopp(LLo<1puElthLDm9wYYAHBC zcNZwRa>xtTP^_f#ntnjO3eHKg*Rt-~XQnr>@ z{v_M^3#JaY>2AR0I@XJKW&j_c6n|Xk+xd64vb#nAteM~#PGS`G&Kj`zDHepn{3&n1 zd*EO4)#?l}9-{GA3C&T=0DA3gPn%ung%at#@dgYM0e037t1BX5c;`dnYdQ7_yX;MT zev>&bSYkYBY#b^E;O9v-N?{*IZ{g2`AL`p8kp(F&-Hj+)(IlMNp7usIqeO{^Rb0!( zRz!JPx^58uV%AtcLYGiLZ_!%F5Px>>>fofYY7t$~&s80;a=`fq(5AB0nSMG#^Axze>l0 zri3vH$;YZocU@4*hqD3XucjedcqBYcd-)e-wWmzgY+`hnJ;Y>mU8nSj3fee&mK5K-yTw(p)Wp=MLNt6v5U2fzr>r6MMe(G z{r|d%I)RD@j?#gV_GsPuv>!?CuLSN>&pgV(`~0s8KF;5$(QIK{IlReG@4>k8X(;g& z!uBW<553qgl(*s=>&ld|0F_aXp7)pJtufhzOZL%${sJx2377B(u6n}`@6G9Os?cZql})vrFjl{Xu2 zDYp2gkM~El{%9V6{i-N`DUW`X<)_cGd|)zcR<+AeW#+5~*M)LsciYG7l{xtL8DRB% zwqYi9ugQMzcl^#JfEB3Pp689(upLARB9CjV&~h6my|!Ylk+qHnyk_Hrv`U0F!1e&g zPRB;rvvkhxss6qL_VgHMG{0Rn^5zU@Q|N99c-!a+%);E(1lV5PI^Lygo3PmC>*|~m zM|6#Pkc}jRsB}wWJJoQe80X4ub1>8Z<8UqCx`JIOm`<1a?(8u*bhj9Q^tsS$ty8!; z1lkQDEXS#Cxdc!{Fx(MrSI8%XBv(xrtOXjYrOKy=-83o3BpTs9-fx5Bwe$v&V*MRw zlkqlnB%eygY6W&wo}Ky&#af%QO1TIUOD?kB+x3N8QL)mv%x|NM1bbMM16E1Jwo@j!a6t!)83wqYI9B$1yKe zt@2l1f>91yqrB%z>H)eq3wMp8>?IcHSk~97nq;?0t6Q6OSV(qCR_&~KU2o~GvKb(k z=S9~}Q=#MFqCSWD){OQ*-``LdbDSqn-&;tpIXi%gNazk$lIFS-yk&bn5LDw=*)9Lw6I|q^m7C`*BZ*^UchI zZ3OfWwDzTS?%$D;(w8XDZEfEhmV|9iY>#@NCn=WkSplS}CUTmo?5P`$lXc@HMCaIE zUCA?&6L*u7P@+G*{EMD=@^#9iHoP=Wk#zF78d!H74*mhyE=CvJPI*$qpVdVAjhe7V z?p;mXmquUHlHC|GT52=)IbhHMfYEE<*z=E#!;Pf$=GU5vPdB|uu)maFZ)OR(IADh4 zuhd2L&4w=)qvE~eNwhRlTZ$fi=YHX6z3{eKA8zl1zb$|%^)7sut|f26w+2SV$GiT% zu6r{s78Z#!hXV6r25MsaG%l8glOL{~zaRbE*k7=JcJy!eJaRvNYxH011i$nR|6C^s zOYM2JbD0w7oQ}ll3w%K3{UIe8E?QT?uvdlP^nx8ECEIqU7v`OLQQJ#R?x+L5uiVvK z@Q}F?1aF-j_o*{aJMZ=h21QyK z-*EYiY`S{L*F05W%0UbSCgi`pW0e zF#=E}Z2J2B7wp?Rc63`a)fkXJLm1w~&hK#wT3#HNOEs%ww?2Vez)+~d^4Nykcxl%5 z12n#euK{LU8vp&chkS%C{}urIl?qw*mF(+1$Jcgo6UhFYMNn#F4;q2t*)Y^T6b*A zF++^*z$}tmpWoLOL?e5jvPfu6jE$+=>w3$_V(yw4)}~ChsaAS|9L22&EFT}XASb^k zM7GD@g`IG27R%&fz=`Uga(;)h=pDs8JRIa~N zfL!o-QDXllW)=>;M~Uh)C`O+N0%uo$qMe>WvG&WE0&4XR_U~(3;f1XXzZ7#+(nUy8 zZ|xPAXd$gH0a{nPKY3rDCy;!eC1_!DK6V^+hy1T0HESvA|Da9{obp|)myFBEzdMw0 zW}%O?3yzjellUux;t3$XRB;&(gQ^Q06pNm(Y;hDk`2e1V(?%ktyF3FEX^tS}X*NBs zN#yey0Byi0TUMmf8%dQ`?1d?=bq~DmM$44{N zDW;YehD$N?Q@D-Pj2}9033~&hmjUqiOx3UK98170)vtU3%r(=3{@kJXc*3ix_qke- zD(Oq;6urhi-`ax*>rtuv49I<(x%??0xA*iEBa~#KYF#i+iPBJL=ae@hN*rJA}vdI1J_PR@sD zS^+a)^3-atVd}&>g*fXH1C(}7$b8=Q+r4uR#=V^j)>Ih##Ott&MF}LbF8M8&Sz;}F z9FVEA^&=p69syF|kY72kzK~;3F2Q={ME} z+R;c~zYp}=cXtE~-Zci^&flNMkEihk`)AJM$J6+N{WItB<7s?U@&ERD{CFC{>HcsY zAA0n)-k;JmAaB7Sez`ZFoH@Xl(YXibbgF-rg&#!WuT!XqCF$*R=v2$;{PR5je3nb^ zqR%~=z(jTL{DBV$$aSs%Dt*B9OmyD3f*!q*J|}Y#INmzU2iNB^4)OYB9j3#mJh}sZ zxRb+)(GkkOy7O2sO^p)*9Rn6yKf;TnYEp|U?H7SCyp0SuyPgf5OV(j10{KA)YA$EE zQm7j+i|%l>p>CV0aaF7Pn>rRNLCoA;w#J)YTo?6`FOy&@Tm|6$GwW6m8xwVwzgKj-#6l1 zis8$@jR@tQ%7=g>&(~A>kAJ-U@s0$D>6aO0jvQklh%Wwa(a~2uBa6o{)fBkZ*K0f@@J^%h!Rwp|>D&d(Qdfoh6tTYSe zaTX_~_GWc{2V`y%ad`tW`D*~E008DSguc{>e%3Sq!7MBjWzqZdXYKUy=^yV|GR%Kv zlRjE|!;*P{hf6rIuZz;pCu?hE*Ek%TJsFE3avO^Q6E2%4*7SZ?pC zyqPM*imF%w)}(ZUrq!_Sn^CiiYu9SbNo$grbDhMRFd4o9%U98C!#SNG2J*>T7U(P% z$>=0+ytAwa&V8WknZY;(u{tU9m-C&b(d2D&C*Fg{=JI^Bz+vkgz@dyKZX zjIY{2zWNascMwgut9|ICIkt`ysp{AVhqvmMc^rWG~#i3uvD8?uZ z)~Mb_axzD#$?US5MZ2t9{uX1Wi?E`%>P_#qs!t)V4BjR?4**l(rk&fZrCs5s8H522 zkD))z*SKG+PH-UUpt+$LSD3X3?_$rAFGw5|;qD?y;9U$Wf49n7)5Sxz2X@Gtd$WfW8lAMx!g)h8wa=)Iuu~<7I>=ka;K*x4>xZT5e ziw#7v$@?J7v?dzDvxMAZ!fH);-%Bwr8^*|yk9I|mv}bE!P`T*{l&WdLev+6sf^j{T=yntZtf`2c;XmlSCp!5?>&^aE zK6}RI5lVb;3=w@Q1lTpp?VlPy{f)ah4tKRz&O7kgUcwgka-M1F6ZhRl2J^<2u!q_H zS=Y-?+ImTUe*f*a*7AMC=%qwjpBeP>;>YM0X+)q89JENk{Y$7;)+-N z;-3i|!ak0N8`DS{`MdWn<+Km{oCVNrSoJ}=)~}fIz0+m^-D`l_przBL$rRuTURkt{ zR09H^e20EZ5n4jHPfSJBlJCTe)|3Kgv!rl6u18b#NFiEgv9vGvh-3fUUgl_g2$|x% zB)Ko?Lk}A)f2MZ7rVsrYI)mTtMn|7{CgFd=SU)f%nSM#W({A8heAMahXz~U78+h`2 zQhI=m>2mUc4aRtqth!oeqer5j1ByZrmfoN0+@cMXn#LsEA51vbu_DBwQV^o9#BRyG z9HOGcE{7NF3}7m32CM+}`#_dZ|GZ84_B1Iz62{e4>d+fu0+m4wn&vhCV9bci6UGMH%gX$%2i+^ zuU&OvbWBDQp;!5>JxRUG=zTO(JL;An+B|AXF=jv~lJLa)Cr8mXn$jG%MM{>I2h{!K`#?5H#!)|V$sUSMWe;C{65`H%zf#8r zShGVzSPePcpw7Om2E03AdA=JVGxu^_U|5m0g>GdZsbhkW4A>l<$cu<)oC%2^^&V(m z1(x}0Te}%*7MtstE`d>rKyVsQaOV5P6aa~-kJpkMX@^(a(=eiFW2b!|X*}lcfCbf; z5Qd+3)9zAfN_^IYwX@!{&bk;yn$TMgrY#t$*u9MCq?eNdy75InxvfHpx?CnBd->0@ z($Z_R%5QadRM&K2X-z3H&3xq2D8=AIRNbm{gZ$&=e>8)946c7L`V!9}ztn;kx(jTN zQGCrWBZ-&|VSh=2>>eG#o*aSSVy%DkHH=S3i{{z<=!C|CVLM$kjHyr69>xWUoE*>J zcFZ=O zd{y+yELB&hO4cXE*^~lUe1=LhYtPRU3WeKTl@GhXJ(*Ort??FT*+XI@jXWM){RQI^ zHQ>o3C#?IE*f174jnU=EteKHvE%d6?YfIo$?TNUL=(vv5jR|{9vt?xD*kxweFl|t2m#8q9dnW)5IAx=f zc&yWbLr!2+Z(a_{MrVd5SQq7O9nDq-ZufGfSwGl6CH0I;lG;M4%==?;~oAjVKvJe$)9)umw`r(N~>; zH&?UAyXuHxOf2SJfXs}$qHVRtCDw888)`FF1CQ3sDWK#y*jQQSMus`uU$CM@)N)6* z6L72kWZuiJ1q>!IbQ~(FylruQyu7E_}j*YvzP@P!BTO z^SE-9F{MvrH?B(*6GnU^crLb6By=X~+TGiF(VN_^H^$X*G&A`HbE_4_I-z`b2&bd@ z0QX9za2s#*sY78X6FKN;Cl!a$s zGC%#dtSSV*g~v`p~J{! z>bfw}Ep(ickrelm*?^U_9P1Z+#Xw$B1ZE6XIYnq9$s08T^2Sk}zD1-t5%gV*FD+ut zzA(D*ni5w%Z_2Zf2)I;MNXt!oJIJ`paMUXZxoKN|I(7I#g3sB?p3jGK!mDmN0lTl6 z2V)I8?zA({{&-%a5$1GOil34zN(_e(b?SP6)2xw%E4M3|2KJpDhDgUujfFA*v+yZ! zmb09_V8%%+1lT+bD;=V@a1>(3s3&*vM-!7MZ zcVZMA1>%R>7YXg%RPmI__Ng=uvTYBgBDcK?i?zYIVn1EX^BqC#@CqqVgN38`;C5!$mC^F$z(9>2N>(G zt4nzDQ#j!Os6=G8U$)JZk|N=a;eXb*rd?E$7kYPw5vOS<=6E|tz2P_KLzmGV;e*?T5U zNEF71uihAR(XxyB_ZeVYFnaOHI&kENCSLk^gbqDM<*{4}PQoqv@X|ebj=kx>$md7C z93mOs$YWnaZSmQ&@FzjCi%;R!UusvER*3B*fBG#S#9s12=G9+#<_P@yrHov+B_QLU z9YvmzBgT>jj@oQOAcsSs=Q)ytWCAC=rH_Df2R}`H;YFl3i=70phNEXb0W7=E{>1YW z3p{DHBbFchrHBPwd-%Peg5f9%Xe#OK%`%B?6_P3?Li_5$cfpe||J${EB;f+&^7P|T z0IhJ4bmm5YTE=I)JL&Va^RL$7@27>6SAd&_Z~`jedR7W!?h1V293yH3zptthGNu?d zPgRL#sNJcD=(yJM4zSU;>#I-S z*ANpl7O|1h5!lR!lhrHfNtZiYj0CtZ8Z%AW8(y4X7R_TdSRxA=vC9JT$eLL{ZJK>{ zn=BvT!*m)?+yO==p1#j~BJOvH3KJKSIbi4xCV|RDlr0khD<==igPfQ{cQ&AUAqNYb z6(lJZPn|;;4NXk6OQz~dTwBNLycX46os^dw(owgeFryG)z%Oax!ZT|_e=;9?r;ojw z;tBofh%5!SuZP6MjE`MTi6s{Z=vHiDJSLtUQ7LfRBtpQ*dD$b+J+$n}*Bw()>OyBA zEqMH_Ez}0!Iv%_-kj3OPe)s69il4vs43fq^^*grNw)@1ZB}$-$4b2{qK#YFfm6VF- zbp>4kdQ!>FhD{*p(TRMj^O@yS?*0i`hIZ?rVFb`I^W@a7A#WNot`#TKOocT@s$PhTbVxL=#w9`@lK z&@)*@P28x&lNz5!t4?cQsN29uJxNBgy_6rO+s)ZsJ3MG%a?j=vYHz)b51r|3V&+o; zG`7I3Ol>ktY;uoP+_m%iIw3^4_s*=l!E>z7{TL55^roQ)<0MWL=^%)wHHKtddK_34 znxkC7HWWw6F{b;@t2WKy=R)8yzewzSHS3xw>TPV1t!JW979qUxqfc=YAp zHa&Bd*5T2}*4wRv{p01|IZ08pf6&1#tv)J>?`oB-*W$^=$1mDjyA4?h$aZ@2w}-p< z+J1eBX>yx*DR7&C#LL8}TyjbOD#hY3irQOP#&;Kz$!NkEAaJkY^ux&ekDs`6z6sBe z=wf8G?@a?JB{6_>`d$%yt=j<`4TwORd>)l0d`oe0{bcID>;}Qp=HjyZVdTq`#Ws3-)Jo{5vyz zZ#(eq9RF&DiwXj>_g?8C|JSeIx0(u0zh_(iDok#^TUqVOb0Gv&Uk~} z(3m=~&CW!TN?!4>45_y>s!-yg4zkc7lzF$3inbzvIxRX)xJo=ywtmG32<)BX#B!a= zVN}dYP%L40)$QJ-NQfg>n~;)KGOs!0+{#^nvBc8QTuU*UL9MKAcC&hcDK$*tO6#tH zLCZ&lMeeRP>WOyBE?JNXz{Z6PtS?l~M%+aOElpgQvuEO`UnOgpZ>?u2Yt&{O$J6dL zEZ^<3F#=B@X6%uG`q^==e{N)PeeOm7xmTWT;T~?{rHS^d3;x&YLgucpKML{G@40~^ zI;$2gr>ic8`%8bXs~2oP?k<>e?a|e`KMvO-7EEGG^WL2`_jB2Al&L@&`Zkm+=oHLh zZxB{Ni@sB3WVLVN^$P}(t1~3YG2bKXEU7q8Ff7|08mf>7O%Jd8U}vqW`?Shr_CVfO zw=1M8ncD78H8>wSFqy9IX3Y_dKd2?RP0HEFBZLu8>S11Z6jXRg@+RqwQCnoU%L%VV%(mpa?h0w8~jmF(7 zvy_6azobFI=5u3cFGm&KA&doAlsg%W;wlbVJF#|!((0(jr0iDqE;hwpK_i2uWTbY63@o_4X}>R9Z_K8))V zx~=4I)d7C_$@h2;$RGS1K<9BEe)>b+YiqKvQS2q-?YlVkdwJ@enKA#(Y5%%6Z^6->gz0Hb$Fg= znanaHq_FGJ+eP+K4kPr`2iHsP9vf@83FTUhno}8TyN(aFLTIhUc~4%nDR zhi{u(0y|~XXRS44GZ~-O?pij39m1k_a%I$>ZOLF310Tu}wm(dXT=tiptRSpNJNmx& z&B`@pBXgwdxi6k7P4i5TwR-Je=VZ#~<4Pib5XePo|q`Lc{P7aC3*|i3C!-`Q?>_tMe zAlP~krM0k>46?eLu8^azgzF|I&UspKMaerrft<~7s|l#Yx@TM#ujhj>QWhrMWc!Yl zzzzua@jaJ4$>K@THPL&$EAw<+uWu_7y~r+5=HhM8&qlMkbGV|slZ)Wl0<`9DSnfUV zEw-NFxCfNj6MDsI=4!Y#V))0;zosUMo&}jYbbeWG8R-hMu*n+Waz$Wf9f|UItDJJ< z9}Njwe<>rwq(VHR15lAWmzW~}?sjzDIVbBZtHisd#(j*g)DG zq2vXF>*M`Ss6%>}qiWJ@(d~}1GoXFZ`-)4Bl1DRjno9i&4yyVQZ$FGKFXA7(TKFYT<=3t^*mMyT2 zX+ImTl)GL+&+9#tL2kW13n`OAX3gt%6j)BazX^FrO!xJqZfRq4*$VO89{tq}c`w+U z-$Yq=2={cfxT3pTLk~|UG%ZIQ_f?`1`f00OR3&!;eB%0(VrwKHOOEXAD2QI~UjBoG z-!^4ALu8?%N-^l{LdEgFk$IXg$%lVnZQI|H_z#Z!VdXN`&sA-@wG`jK>diY9IC1^Z z=g$hZJ7Z$*d*&|t@VZ=<@BEk7#E&O5AhG;S$urOc8a$LMu$1Nqwx=aI!?1pY4YfDp zwR=z%zI!k=QaPole)9BTr=kvdVAj~uRrx#gQv8Ho7K`1M@b@Y=>7nbbN7luYLPEcc z^jjI{9bYIa;HZDF1Ut{%j4!++z~JmYt_Z%6&_$(v;i!UI5X?ebsHFhGyyX+#mr~Ki zd-U#yoT<;(&Uo>0+(2Xg6C?jP?nlndZ;$+K+`nUb5*?Yj3Q8%MD>Ce1AEzsqGU{Lt zWGr7BTr-%;(ZEoF&Qem%F>tKy!sIhmbnXIY2zfW1h;$-pxKS^{B%p9%x+FshgQo(? zKyur-ofFucs;WOWtBIYgl097~(q&h|eS1PYU*-&Fvql{}-3-)8uFMb(;I-9~>FNa= z^?^K-s=p-;-a1d!c9)Qw7DzD(&t%_o)M6lZR#}v--C2r=%KdWG0twxP7E8XA56~~&%X0B zX6I*LsW-3Yd&KZV@47`}>hx|W3`brR(=*jsUVp78CO&Loj+~zb_)8fzLt;+)&39k0 z58wViM+gk3N_Pi%C3Xizo+6+>3ULibW#~!0{@~9S`fN2!oA{d8H{~=-h$V;@x!B?n zb-8<}EFDv*pdCKi1bS2HtJ=9=%Nx~Y*VHqCls=7@L!kr+pO5KaZf5~lyYBjsBXvTt ztD@K=rxZTWS0j|;-JauxzAg7|zB-wQ+hHG-FPQ9XPPxWx4SQ+N;e^c$6=O7QPY3ZH zA|bOX^=gMuU7>VDw{9f1+vJB3)%<JAfs=x-K2leN#nNJ_smCGQ8tC3{$|eG(5J$tAyM z@*j_XH)kV#KF96du#RI1!j5K{Qg+pcNeHSwsZcpn(6g^ZM**z zWvHJM|7j)ZcN(bJR8T*+XsY!o1pYIg|LX=tkm)b|Pqs?R|D$JrR*|S^K7A0~K9cF{_qH_B_Y)vsP8Y)}bwe(uf za5R{_76x}r@b2L+uuy$yv?plK@OBo&XdCJhBo2I)#gmpsD$gxI+#c3F3yul?(=9_g zWjPafr`l+0R<5jyF3MkjyX=$zMirgIp$+Qi23lfDE$V}7QG=sy%cp_tWrQ!n-S zq~c+`<&kbfNYa9(w~4L?IN-};Y*H{ZY&Uu1;0_(blgL41#(JN(+&LP<#J6dt87 zM~0?e_Jz(55>UF?m3xB4?Pqwes%b|Kv}X^W$#=KB7~$Mf@8$PA-#+1DbGs7g)9k$X zZIZ#g=4ul#%FU4q)p*;3iWG!&Q3Ya`=hq6smLz38gm|rZMHLK#zjZBCI|-hwK@H5L z@h{$mwr-4=vSWkQp+T?H*pZKA0rpkegHQy~&7i0j2+^#xnNUpmfj_-^+fLi9-xqi( zQe~!9Y7Nw=$8B4k?FfuD5O_$t4cv(auNDcomWTOt*QMZMos#ro2HS;bI)X^MQ#XZO z=D&_TjR~Sks4F(rkL`pZN#i)_G28{AXQdsc$tH$!JUFO9J}jOr07X|^^plZL_B0(J zHB^BkjXHpQ!q}erV>*Y_(R|BnMsRiNA7cQ*?D7H1)Hl!@ZKE;yA z7;V1VZe_Zdsz^2k0z#VibQP{}Z(74G(m9QAHq_CRyul_uGu^oOod;>T&#~P&;3Ig2C6IpG$YYck)3(gG6Pp+Ul|Z9!|OBvvNJN zcQ^Edyri;f;w zQ!QC))L2D3ktX)mE|e*nh-}H%4_B~I<)m$Ns8xeqNi|1A*Xl5L!T3aXtF50Z4HHX^ zvc%fqe*0uckZQ44tdQl~Ro+Y5Zn{KJAR{Z*h$x9n*R|Mkt-#64*u*OonhZ}AN3d6^ zRVpUmOrqIC@(!(1<7-yBsYGhIW;R`7V5Z*sY?EbkrO=|QHp_(Ei&`~D%buzMuH}PlIhF?ByIL>Ns&yzO9!gO@VTmtOuD6() zOhc&-7)iTUtrY21hDLL*RA#Id`4ksPMRO(90^NK#|22A=7*cuf`BPbuY=n!tp)P5u zQ?3_37sH=ggQHrh+I)Ugv23lweO-Ecx+1B%;YN|F(I{b4e;dJQE436up-uW+dj6C_ zPNo`jp+eZ@`4);;Z;QJlN~GOfsYF&_k{)qn%>gv7*lN2?M|u+bvLq*GA^gM1`BZ%4 z?zejX;d%3!Ch#XAt%4uj?*;gW{hLibJB_VcnfARl_(;c7dmg<9*}3Odw^hia@ct=c z*-oiIS7h_v_JVz+$40kRshatTm1-n?;zoyOm{D{@^My)_>UPX1%=lG-mk5`;6(%CziOn)u+>=Eehw^EO6?+FtGBY}>WUIFbqh?jW})@x z{A%mg$e6OFtR54cV&BPQTxu3e{LR}^u&03w(oj)0NNW`B0^{l%V{Eo^wRWesl;jBa zO9iTSm-xE27+5RR*jfzDYTslf*@z{Ybw;RCyXJ;yE3p(?y>`VyH{6QVSH5Odj}I!w zs|~hVRjz!K(feX!<46=sg&I4l_!!FSeeNL^E2V#>xkwYOaeOS5lld(-qn%VnyBq#xE3ez&4z9ZM-xn$*oE z?MY);s9eJ-Q86~ljPl;Nd+nv;qCxl)s@&}jD1`Me@2_9#{(u|}lQ>lGVS zjy)$^r`BTWu{Wqzj?@n$4O)e^NYG}h)CgTIl>a^2T&BcCMBgA`tuT|(7#~`2JrDg^ zqUgyg6dG--lRA?6)VVwvv7J=4)?n?@wc?;Vna)g=q0npfwW@yKRhP9tWCO7qxJK_^$%A% zB-Kuvs>(MgjcoWrtX0+$Pqjcwq_6?5({-H)k~Z;z%>ktvB{?lz+g|12SdfvkcL^^O zDJcUb2@5~-!TBy}spxQxK0-AnTJn00jg^9plarFO%tArCNm{}N5oMK*QS6J#+sVeN z^KfYGSn%(aRx90DqH@7wcf1(@PNkdUKG)tYS99FkDoVF(ce1HX3!yfbw;>u-;HdaaY$i*;Ql z*V0s88&)70S&g_u$wx@vx-!R!G?I04WX?z9{n-_5m_#4C2>ICJhepV>tv(^kac$RR zhu1jPLP9h`IAC-=hmtDO=7@M0SzAEWh{-4!Nm<*oH+i@O)D)al9)feBeRaEJ6ojM@ zF!iN6TWk{(80+aZ6wxNQ8ce!vvAb<6a-b$9{{5xA^BhI#HZI#?RQ)OEti@C$a-S|r zD_coUKteW7Ho!|wI$Y*qqpQ%pSPE6F-W=AFv{hbC9VS&{<)k88&U@2Ez5!@^kux01~iD9H7 zl)aFFBP>7h9*1fibPcuvR-cJw<9+-_POB5KdTBcj5)Ez7R zqo1-({I9q3a-mx^B7-H)$+S|X0s~}v$c$T0zIDCi)@`(65yb_|1ZCJxyP`8~a`Nb6 z;^5vLw2jZf=kk5%DZR3;vaHhMdsfF(=jhWpJ)<(kuc-I$ZEw}g)N=BhyLriRZ(4f_ z!;k5dAE&de>Gx*5xhcbsvx-~n8^6G*R`Eye{MYJvxZeC%E%9S-^~Y}hZT$65+5qB~ zAIS^;WVPcKfAoj^!C2<>*NH+~+OMO<#`M>V5>ey_{&5ZQ)(^%DzZ@odcnt#YzZK={ zpIjPFsHRmZ`fB0xfOm>*||uUX~&h<|>G1G_Lkn@N90>vAx@=_xm+ zA8!KAw7O+}d`o@^M83k(z6WJ~5K(ZgwUA|h&QP+g-)Bo5Fz2Bb{i4r$V+yckerQX6 z*cLbJ-fvjGUX*tP7k?N+#ASaDQ1r+;q+E6%{Y>8Z^G}rvQkPy(46RDg1gIvy_4~jQ z13#y@jM`(gYK&7Z+K#vkK@XugEJ3pxfaJ6R&8z$y)%-)Wnt_PsL1^#YO#zz>03!w8 zS{wYYXbhTx=i!0y7%uV#--q*uY8HT3@>=~j^F9FqnvtS%FBJj5rvqB00Sd>#VTg%TA~NhwDbAv~PunJSCtCD!9}o;nz5o`z z0w{U+sk+Y-NAR;&Hhju3&2*))PYydVoBQ&!MNXSJr{&L@uJDXJ%$h@M;O&OkAZkyg z)D_!cYR_OajxwuEGzM0i{83kw%i*Dm$y&C!v^=* zAvFdz>FSMl;v0KH+#CvIBW~S8YgyK%9lOI!++1@rLEFf0xZIAqGR(aZmHX$TQe0+? zl<7}+8CJwe^9Zm^NeIW35@T`)Q$cjOFePJL%%q8SA7{9ch&kyKjw{wMg=obX!uUt! zN%D5GU>xd_X!40V zVOO`ngw$$Zo?3(Ve z!;In6#daY~7%wVv+TgV@R5ZRqq-7MZOQg+xIM}P)#_0D2&In%PO>P~2PD1d_RqvmA z^Qe)>181bU-<3i;^zl)DM90yp8fy|9bzMk0&+m4sGUYJ0DYqk@=9WLTo{LQAap!lq z-mSO6EyeY_X(zYk$-kag!4;wdxF&feOp9Pj9$iL4Po+r9g(J2@w2dlOnkZxz+ z;n;*mgMSzk5(*zNtGJB|w`rH4qUO6t&!fPWf}$5K6=Lo>*faA+))Hk^G@oTwGV3L; z(~HXE``5@TxqQ}8+#Ab1`A16HFJx9#s{zYcg_qA~%{oY7f7{-7&JeI=GKCKKZ(|Ct z=}chE9G}^t!WlnEP5?!j=O-{F&^k$DN~_&;W276eA(>pxkfz7TQlt~6Q>MqsNSR3< z$I+xNbr)YJdD@kKNL-UgcPY)1r%N|{E_lmF2vF{%{`x%OOW~223R3bXsMR&`CyNoD zz3LoO5t)WRz6*JxCNimbrp8qjnG_c_HcjcOy(j(bIlG*F6+eDUdH#rc`p`Q25qI|{ z@9atz20m6&d{IfNX*N(Lgh;BXHdrNuNUP~KU?nUk{UIvS)9yHzP}ilrkW}|Mvm(%i zEv}4rVNIzaTX^br`6AHuORgzdcq(-HlI_TlR8Mjdl~#{(VNI>k7~dzcCe-XQfm~Qt z>d=x@w?4BX*8RJXOroiM_Ac3xP)se_p;TO%?b4iD1Gn(h=;E4GBe(G6e)b;b@}+$I zllpiwaOkz2_C-$y?8PwdMRW=J2=)%(6W}Z0X8^!}Fk_FvfN@6%5e78q|4$+oK#T}3 zKA_Nlz7NBI=6(e5AONyY!~iKSv|${glprg@0gD0m4imzhC<}B7>=?i~;7^~o0bpI+ zI8S8Srvc@84S*U@rT=mtmH}-YoCdrN02@%I|8yT3Sx^h(+dN#}-%ktjz?&oSPJ~a2 zbvu={pi?~pN#7>;e&0QS2Ka%Ah4fDhCcYVv27lo`!#xNG6b>*P@I7#TKFBN}Ibid- zXPU-E!`9r-fkX0JuUvzzTpa*9pkAO<|H(eoJt_y-Hn=SSJD^^`U$N`MjXI(8ckJVr z4t4CSlnixbhzc^qfjJ=Xn___by@W2^MmK&9sF)xs{Rh83Ovd*a!UKi}Fb8OsVdDfs zAAmkUUBFz|cLnqm$_2Osn70o4|0a)M8~rrQxAs=V1WEfS3kS1eh2=*{5Rg%UT-X6o?sMV*ux=AKk$Ju!f)kLL;`0WI(w;>3`G* zWl&7VuV1~1@5CKG8RyyoqX}bn(lh0d*bh+hzc6pPT`@zn<`% zHH4>WX8>=Xo&o;-GcgYU4#+})T!JaV*#FL;9Dv`b6cGog%a9Qf2XGeX3}7$7NdR=8 z=$`r>l7nWdXf07!$)piG!VZY_*=Pi&2WET*Z zvu4HbI?|k0>^a<1z(){I!0vXMZSYg*N8op2``?WE^o*#2rVM<{t?&}BzMfT{PI@m8AZVJH6iPGHh?MlZ0 zj{#o;z(C)pGj38RvaPBPL<5QjI0cy6Ke>;}fVB=x1G-9>{~`c3+#ZAtFmwLIN>^=T z!j$w;(-ON5cMb3o#LZC?^SlhEd5m|&>3%mt-4fqlO-Y2|=IC7sh zGGwxCuCwA3^D0eu;m&6AMW@kPBfy@=JqLXH_frqT3)siMN8$ze9T?a@ppU?SfdK>q z3I;e7nAktQkHUb30Sp5=irJ+5E{+v24MGNJA&wpMA4;%tfMvKmxETO*faX}H?HHId zLT;o3-oP6KI)P5O1LDAN-*2|n!Tgy-tPyBK`u9ttNtIw5ng)24xF_&HbRU@ka~}uK!bKrLXxC?? zZx9|flL2J_JNkc^LQV2{&$F zNe!Kp$cATs>mEfei~hgR_(SBTWF)`TFDEr5Cv|Q+QN$;pzeq=Is?8DH^5v*5f+mc3 z0S&`odL2+ABc{nT;RMfCys8j9D7%jL=PG{HmxKWdBl?rEKG+-yY6e86=m0ZrSA@wu6nP)oaZrg0= z6RufJ=pD9u#cy7V`gWpo-sv4?ce|%O>&vWX@;KVJL>hJeeMYzvJ@6^Hlt$`?Piw%J zCZ8afAej&f5iY^Wqrwr~Poo5HL>3|cD~Fg(#w8rg2iDk#!Y%$7IYHbZ;1YkT4$*Eh zkq`5H=rf!&SU&II7HzcpcaJEn5gVn`a>DSEy+B)}_3t&pc1V<02~IyALcdlQKG8`% zrFdf9JWR=CPP!9~7hH3fP5bM9QG;j<8L2zV1GfO81taN zp#-c+M1u&Ja`A{l9b!QLs}7+=f&N#A{`UPxD$OAX8*2PnAB+JR19Tef6rvMRrZnp@ zA!{xXG=io?yl6N}V5iXkPb!3bTc9*xs{mJkF8rVRexWR z4OY|3zq^mmfZqW@0-VWTh5(ua1P3ezKs>;>&v6gL0f_@N3v33!Jixh+zVFW-kOSf? zY8!|SSPg)BfHli1cK8;EFR~L-r>INjDO|%f&FN{Fq&7?9I` zYp}zGLEBXwXZS4WG2lZ0XrGV)VjYGC5DjPw;A8-5pOgXWIKT>kb%1N1CuQ$8K;DV{ z{9WtPjhF#)9p)O44QLDCf7OJ}fW7W_X5bR|85rC@NPsI2&;&@ck8qE{0fYkz2RH_p z+&{7J|ItNYU%(k)bpOb{+CI`fCI`?qD4qYr?swlc?jGJPfCtEL5e208R~JBszJPfI z@C5Yn_w55PKmY*ePljy~5RSvL8n?8;n0F`=10W7iRDs6;{vBe?``QCUIRG-CL7V!#y zM!F*2kazJvB~au-jB!jc9REyDaWT;hXvrF+({jS=G#PKdWO8vtZL?c%$608zlssUE zX1zw-7VjsH;1N8e6wN_&B|{AIFrf27m<_Rj-@GrPD`I0KV}QVb zfdLE!BKF55F-*68Y3aTyO=6NN*>6+P#00OGLREFqqQR2JC}qB?WJ_mbcR!CQYe-n8VeC%i0=2IRPE-?HQv z&fqV#)8BYzlIm)>nt>}#gaUUgZHyd*q@0z|QwJX|qqcCzy$~C}7U3;x0w4B7dLlZP zT?#9!1@^qT@s|Vw#+?y_lK=w)7WU8YV=&-g0P3zkBC%s(#T8~rvA{uun{>4S8Ybgp z*k_55Em=beJ5(mju6bLeaPJt*$p_<0RB8-2L!KtgkS?m2CeFC2)89~W2Q2;_C8xD* z4Lb2R<}kA8s9AJ<+X>d*E*2 zcp80pgHL_%SCU&0Fb$9WyZ{^z0do?f}t-;IjU}dd^ z&G@7ZeiL-&-nD=IF|@>mXkd<#x8cdDTa8#d6BIT06a|W0!wfeC@=yW|T|0{05RaAz z>aCN<7zPr)qKD~BL)6&nF-!{}z0}@Icy2W_p!^6S1d4RBx6TJfu15MNt+ z*6BX`OWZws;NC`ff+_@!dA`uP1RqEp&N?ZBw6E-TRjVAos523rMH%5$SLIcj>!V=+ zSkR0dT_@dhoko|;qX|=jm|(Nh6sUHoj>#tW zZ^&t1udFE&OBi8R#0(Z8akwL*`w7erb}NnbAmb9NlTh=9EoM+&uM!>i3^TbrXu=)E zfSID2uWx<3ij>VoX*uyixSGjP{; z$rz{}UyA#OqwD-QPZxcDs=b5-U#=$E%N%?r5~C>3yC5D5jq{v(hsA1-AUlO>U8v6Mcz^M1$Ndr7S4TE@RIpV7Hax|qF` zc0-<%`ME7YHntk5G!d<5chYj07xYfv&3XBq((X9Ha9*za6g9C|-(;iRgd3(r80vMH z8gfJMzwn=K-1Oov@9wp#>#^(B`V=_C(v8yh)V~699(rgeMzuHCu4TK}+oH1}K#T0@ zfg()->2`SeRme@Xs@BfWeQv$omaK11dT*jE&n4B8me%mhW(r|E6zcHH$6w#MXvLHw z*}uJc+sjSxHeL>^d}wPfe%yEfeg$0?D;?)vF-`4yNzQChPn6|)zvrSKzlQQ8nI6sa z{;*7XUr9gmoFh;f_(}j%8=xut>^QO4w$=tr6MHA${}`(<=tmd>)f$H`S|+Ha&$KuC zlcyw(J2BNmt%FmJ+=CsJQOD-U%54D4wI1&*P<-QyKojo}E%5OqMwqRtgper;7Q_y5 z7c}6a{efex%>^6z<`wgKNxa2decAnzTY8Asg|Lo0_9OT>t?5+iF*w@0Onc5XpHI4UePBoY zIrzTzKC_D-TpQWG_*R3)f5!p8XlrpE`p;XO+`htphSB_)^ZDlh#&~_|&rVlvhxj0W zG3l)E@_0ovPi?I_RIcakF)vcg@pQVET$#I^p<16xkYXvXYw%(YjpU24@ys?kGUphO z9fPvsb8{RUZWi!SJy5MKv81t`WQWzAxQuOxWd^8>E3C_nQNjRbJ4TpxVTrgV=#-Gl z%5jw*;B16R?9ii!KajpU2recvIyPr6?xB zhizSf8iDqiRd=B3I&gbD^8Sv-aBVDHWF8Uvb4g#pX(%Kg5fzbHKJ%R?f)3`<-Y(?| zYkiNbuUxgs@_k>}Ps?t0_goM6L38QX1FC&@cBU^s5`k47s&PJ&?ZBO(PTixe&?kO< zu@X+_q8__Pd+^E?<<;!#?sT8Ghs&j(V!vrew;A_Ux$PL4YrXC)Dii43v))Sb?>xPk zUL!xJjEoz+&RTr5{U_?Jw;6&bcUp&t zKtICSyU;$m;(p3~y_K(F!e5P=+LJKgOCrGn!wU zmTmSP29v}9FSe|?mkR1Kr5zsbpFb;qk7RCkEq<-42i)Hwe6BXX)i1tM=)c%wYTCZV zE8bhd?pI2w{hFoMdax!{6B?uh4dKKsaVkyIMkbl2DMncud03uF7Z25C8ovivKCDYP zd*a>nu7IasS7u5s@lIc&`iZ_s*X9Mh>o(*vu`zFZs zAY0H!1MQx#4Rss;CKz5{OAvSit_64am54XPCMe#3TTs}7y8V?wOKP+U=nJLkrTPR% z9+{hhW5+(saC$@|3cixJ#vS6Ycyh3DL@YI*AlGE_MUl2^=~lm`yMtJBdQd>LkvU`9 z{3VVD%m*k>N;Y_`F?!#@y67#)Gqnd%CrHi^UC`nhYE$-94G~|6F0j1m>hEGi*yB3> zWizHPvNPQe**n_jYF!9x)w`DSmze#|D7SlF)L!_|3VZ^Ou_p2By?SnseYl(oNnYho zoGg7Wz~@yxDr=A7)55%&w$N!q-KElTUDt`TlKpMhl4)b!z6Z?b7oe3yzQTD=LSw>< z>O|8=L2ty1%Ka~t7j+&1buFASEEu8UVH*YyNNgaziO|5*dc(aV8x9XpY(Twnu|V~D z#l2-477ti#V7Aqn;%{rF0AxJ$+uzOxM z#IJm>#xy5nL->LGBN=upmXfWfO$FMnT64DNrI4G+bGT!FM%)5(8$&_sTRIVZ#w}8N zj5-l-s%`+?mtO)q&p!HiAHO^Wt4n~L=ir0D z&&B(K9t-#S-x+TazBoL}e1Ujm_#yFXa)Dy!T=Ok@t=}h)$ffcvd2QcEw_?4x?w-9hb=z5&~* zb_c(i_ws)^=k5P^)Z6>=w7&)HhJVf5LC>mr0X9GV>Q8?Ma7f`p@Rq;_;j4l_UZ5+? zUT7w;OkMU}enXH;VNkha7#>tC&H_XA325J1okq0qkj97Xsg4EOofi+fIXCWmd8{w{ zBE=rXKXc;ofik{>9P_J^2jQP556C?i?w@^RxpijkLVq{mOWD-V*OaFtyARYd=L6je zkr%a8=Y!pfmIrq~`HI^OniZhexAtj&u5ap{7jt4%hILKZUynGU$f@+m*?+Tm%w`My zpxtYVN2Ag!@~Gb@jYy@^EAeRGM~zSqgtn?gsU6W6xo6#T%u2D~Dg7g}XWftd#&t{5 zgO@eJp0^m0wx)7j{b)$#BXdv`dzSUc7?j9ob|1Z`>5c5cwhNOp*y?|BU%mIo8{30# z7fvtlU26<~p}sBZf;&!Yz&$YaUiY*bYI9oM>4w*8#tZdE8F{Vt6@Wv78v<^5LDe#J zVpautQi(GRC9%7D-^8o(kN;pzg`r-U>h?>WV}&JlyBo-t>S~MAm9XXNoSQ>!AqD?P z^MOwLGIGVUWJar-)Sab!KPHofZwgFCBAq+6?zLjaR^@SPY;Mq{7DvY7n|N>8U6#7H z;GO7jFI7*GXM5GYI*Pukx9pwkFuO{2W`v)zxAdLuuzMSq!F30?du*>WAO4{WpSc59 zE;DELtY+5CiSRE5e?k0ooqs#1olAJ-`O&UOdg-P?u<(*;*H z*@oo`*e%VTe8QvXob(F3xdU3LDdjzW^1^JQ=|+8F9D`lJ;!(7|O?naCHt)iJ<-CFS z?FaYQzYo|`_eN-IWW?AS#sIpuf&q1B4h`tU9_;6hLfp$4k#L|lMrmth#M&Cg0KT@4 z0)1f~3HZQ1=m(C1+zS{Hxg{`0@~Ddk!!wcrgJ&)A55rXMAB?vA{SoeRD`5=gk;I71 zvyldhXFLTK%X;iTis{^c5dCKla>V;q#2C;cp&lX2j0Q-a4K1h?L%L5nx_HlWgz47B znA0P)9*AdA12V_73TT#1E$9Tpy3aYfdGF5%*R8iPuSY;VVAtFlgr12F5Iq}PP%4IY zpLBHfp6LkNt&=ggM@T(z*TNd)?<_1(Et^}=Du!2|b#(Wh>j>Yir!l`rkRw3XJO{*% zNl_3xHs&6!7@0?9Jm`)Y4v;-NT#!44cprCk;hydYGiwmr;ct$i?5ccb z?rTTbC&FTpyd)=Vu9LLu$|P(q(+EA=+;f}w9_upkKj5Y3{=r}TYX~3ZOBO2O!%Ni3 zjGwrjE~BcG{p}ee!SvrLIPn)hn}3s1e?y z12Wah>Or=VUa4`)cW>Ry?-s`yz@vr}foB%SWIvg8O~=IOcEPS3o;568TDJCIiKkDr z130njYN1R%WM-^br|B`qt&=m2M~Gyhre(sSzUcyBomR9>^um>vGtOblN=sT=LDS}5 z-nyNqyh3AInMVRrPKif@B=|H+B3wp!g-ganF5I_7X1=f7gn0+>xn5D@zXQ=W2b5N$ff#}NiWkiAto-!rm7{nSXu-0(>MFGhv6L1 z>yfuqFNNrF*Xt51aZni2ta`(R?;PZ^=9TB#)i-JDFVI1`1A+O0B2=;Ce7-Z5^ z9P=tTBvf&#sNfV)!y<&sUg1@6NS$!ioMPh;t;A9BmA!TDAcvP+nh4^Jll<$Y4mn7D zyEq^Xe?ScrgE@+W6`xpCeHtqGL{+mYuw;=-6}y>(n9QLrqLdCahc$Gsl#8b{1Cuzl zKZUp@8Ln+k2MyUNX^^xGN_Y+llK$; zV%xfdA!`v&O-{g^8g;7aFy6@_s4H4_Th$2$m+5w5_RPXGdoj3eZ;>rETX!ma2u0hJ zwgoKYsC10#cBm&`D;Ix=Dwf^UG1v>)b-YGD2e*lO32l||5?=q#;dLrV+K*hPj|Ly~ zY(&8?jp0$1RQu^b@)BAs=AtZ3Q{F+?%14gwF;_G;?n8zygoQfkVW^x0TDdCnh-T}n zW=rp<|52a$X-nG? zzix3d^7vcH^+9U7yRj&ybFAmIq4E;^n(QN}a}sb5&xxR|6bIy*6g?%x+mNL#f5Du?wKiITRh$a1*h!2U4|sf#B-*wOYZgM1 z7>Cj_xbRRTDJJEh&-l>eR)Z6eM@|kTR{bEIfIPnzQKr`pj%~3>2Kg|G9NKA$#I+%T zMRYu;!bD#As$`NN7^hs}!X<1dae4pR$Se4%M<%vsJTLvsR^6bz);(`lVyrtweUq9GRqA zgQiu6`p)`5s@E)HPpEUvEvxe^a?gNy&uwxKba3ratHSHXA2djP@txrTcJ&_8fz`=o zl~>ObeKPOt@5vAYt`(OMgpQae9ZGNK-d3sCcaSQyo}K{f;M&~}>yW(*5Vr~j1aAu) z1gt^K4n>QO^hkRoEOytJZ0j_=Pw6Y{c2K6`7F49-7Fb)?sxdbfwDwmxLYe%B?8j~d z`W|VBzxT&0c38tK{m%wu^ zg}pX!njR3iwLhCDnzDJy{g2_f5?oLvOL=p_ga$v_XOV{F$CaWHU7 z4IT?=m`tOXsDbzw-B2mS5rW@KCSn&KrjI@#et5)AAa!`a4jOu~-wPQp?%*pO-gQb5 z3A*hDe-h)x;VzlafDa?iPq5~bpHAToz%68lemx0aAEvNp_zZ!?Gk_=jsQ3)R#WRv4 z^t;!Z+2Rhw3oeLbT(6Hvu0ECF@8Tt@%|6TpjZe0@Ci+?C2hIhRkB_h6c2VukpB3Hr zcU=SCBKw@ks?x{l59$k2p9fu|?&8`DrVC=92Yo}I;@Th875Vob_vok89>CRgfHQ25 zAlRgyn_ZJuOl%O-vsF+ZleF&e2bXkQTpHHxg-{87K7D*DpJ>o`hBIrrr7&szdI@|& zpCcombUFNY+okCEvKC4F5Ub3m`LY&K{2(jsr@{_nnx}h(I!7e3FO9l8BrGqgH4w;T zUUZuRl#*|yI>|cTZssqkGF#COcEMN6e}-wu_ETtGpM!SuKw z(KJv-BK}<2UFZ7~XA+u!ynN~dNHBcz14#G3T>?|$Qt9LBK+yFm3?Z}V6Y7EidNc-* zfZmWGrN^dIr8EK)=@T_-Uf!G_rC@j}BMExc zp!^i$YK3?AiF@=S0PYxf2}JWCjoF=x_I;!r1 zSL-gm6kfT0v1jw02hnr;tChgtOw-DL1YJL$T|Z+MiPGmtYN7B%Hl^j_6Vdw4)H3Mr zguR@+!FOuoIks-e(qi&cB~x>f{etcngujq{pPiMD|K>j_mHhesggNVF;Fo^)`~8sq zroBQh=cK*jL1FOM_Xh&I4=JhT4A&nb0ejHT_b2>BLSabKPw9DP_?CKcg*j0#vadn( zX2~Jn;^m(~cYZgqp*>L_zl3vii=MhhF<%|ObX&k@a@cl|KOmDG-0PiZ@qVQ($C=!&QNF^;A7qyqx z0Bzz+se`ugq1FEme`yRJTzDVU&DhWr#`mA{84;L-mw$H|v_~kqU?;(bxqG)yfE5IP zw|yTLu-WcRfEC7n_Zbf@ME@fEipYz5H-2XoG(3|(QwI&H<}cMpM}hky{hG)dD|T;L z(~$N!)1_sbLE7hbi8EY=*5$G51b8O*Y= zgvoJ*0`nY8v+8feTBPw%GwuMLwaQ|s!o+Yfv?UAG(Z~$3B9C4#bDKyDvMP;kFLN77 zGk=?C9Pyr93bD||GrwtZX&EIsq?d)_T` zL-1*b!x(FlQA=7oU|wrOs%&*ZO|X3ps^q%H^#GG$CDc{KoE_v<{#*{USL?Y^?730xxl!)9QSbRaK!<-K zZ#ZAw(c(NL-YX`7O^Yv5X(ToB)%XDgU|5p>pj00s>0TMu0VC_kfmIaw5{7&QV@?B0 z)Lm2YAI@gxl;f+V8Lf=P^6bW@Ex9Xn=?6!v6WZz3a@{%>{!+L3g?o->eOlUD+6CBCUL7-3 zSQ*YSGc{+SDPA=*H4CkO$QZ24XY2?k&6{WLu`+JJvbV(-Zo+5oF|=|Pxv-~TX>Ulg zK{2&@GP$s0iY@|XdKj8M>0Q~-B!Bd>WW`?b?5da%z@qjqjfJ7Jrq;blcJ+t;!N6Vx z9cA$j0AE@RkMighr-w}2ZwdTY0ovsx&#>*7=?91GK_x!;|^gsEa{gcrt#c0Fh}&g zn=$}}t=3>q^f9r>_mNQ>nu~yETK7Ro!7yzw5T@qrH>r)IVI4Hd4^{g${4(A$Fh$F> ziL-1t=pjLrz3#brC1{kUT%dzGqPZH69f=bM`j(D=D&k^+P5!jD4ooE9! zFfvJ!8BsUxIUa$%@uGb?OOA0FrtU$(FnN5Iuexk6wjR@46jl;uJ)N-M5)mh?HfBhD z-f<+C?M3Yx+pa8|SV3-?od{=MB>vT1@orRj6&DwesNYcWPMUUULP-LPlNu8n5A!x` z^(-s4hSR?wG^1@9uisE2<|$^T)`V!Q#xBD8uE)}d3%Vej>63`3&SXQFQSOT|$p)R(K8m#cC#5O3m>fw%D(vn&((>n+=) zc|B+5UOi{_;*a#|XM6?xM86ZtLm%XLQ298ApBfqWP2}7D>KFonNwKdUj=vgFav=iY z*=DP%PyYDYyg&Lxbs;CW;t7bjv$ZFd{x}yE*QvJV6A5}@NhwYd|2HYeiG})gS*KSg zBPAa5xzC8#nyn8yp5Y#z4@@&AjlcZ{wq z+PXz6wr$&XQn77272BzxV%xT{W81bc}#=LZ=PY%#hg54MY*^(gN3y@@TZtm1P0et$aUJwr3{#u9 z5}9TwuUKsH0y`R|J>v>MClOkijXVdV$2I<`ZeqJ?g>9_Bx|W+%2*!YG+y^eiW)mS< zFhs0m8~JlOG@-69TT@F*tbtHfNlYuIfIsW+YZN;#GpNZsoX4#HzNgycu(G59f_oux zHJ5aW2o}mHj?J1$QH4og&IxZD6GgfVyTxj#Q;hjh` z8j(jOiz%&3OD8aCGBk6!bcgzf)S~t#dB{1<{zQI!Esu|lGp@+uRKfAFIf-4*%2aXg z;>4UKPVV&qnN;8lg6`8}OW<+HbrMXtx?#gYW+x?aW(k*m-tcLXx+(#dyR37WG5+{C z$)VGl{?)ko$5JcO+f5c(+vnb|N@T1wK~2Moz*!LX!1x~>cv1arN{z6mw8pd|=ZNUW zK2pjt4kpe~qX&0T^XJDpv(dn%sC4hSuR=`zYJ_V8#R2pMrW;NXTEAWl1?z>{RxM_k zt3nWTTpG@S!e+rQXj*Q~Gu}B5G$!whd_*P08t@!{7UqT5v_ngIDppmOcP_F;yA^gqpvtEe5=6FUzL67M$hDh_fy+Tj7w9FE5ojFRULu%RNVFi~@Mx@h3{6 zvc$o=C|(gfZc&!$4km>K-&!07Jk?s+*j7mZ=7Eo0QpEYKdB7ob`-Ryr`C+AbMW-i= zFljW_!z!^H93D0fBS9ya1VwNB3%m<9>n?o{<}-6UPDj;oi2j=!*kN^-qXZ1?lr{E+ z{A_J!<6IFCDR<<(gO7AN&(xRHW3>{0owsdlt#t(7N`>G1@fCcGo*bVgUXO)_rjURQhc9T?KGamglMFC}$4z7lTeAFG-f;pzPOjpF_lq03HDLB!|=1HeS}rXnihVE&&33pn2Kb44E)x}S=lba5Ty5n zr3nvIFLGg<5|R^##ta)&4`GWRNdb)#9o0yqksXzN@_J12a{E4bY~_Kl1*UJ|VBO_n zbQ~~koaF~03$$+a_buF-D~J|1E{GLkQNam)0Vh~@NJqr#d-pS9K?osAk}xAucBO;j z?n6kqLpN>fnMkc~HMpw^cfHxeJ9w|1YR?ioGsHG45XsrmaRbZMW4FI`ciJ|Wm-x*l zP)%+e8fPu+<;vlea#otC4v$aDzv#H(dOB0DEG&abY?4%ow0)C^pH4Se#FT#7U$mNN z6WJf^TM=lDNS}qj(c1=@6R6=Ul^;Af3bcu>8a6wy;>sdeRqr70!%y4;G(dqL!P@&( z$)ksthb>6w_blf;km!WPjM1!$gCZ5X@Lya`zVnYrQhR2|%uDcCze<84A;^EA{E_^L z&4`t1RIzm8u*mZLx6&iC2H16mZCl#(0~O9PR<_x(*<`QAia?qEof75(QIp?IONQJz zj(<^mZKGt>PZeL~Vj`E)AL}!?g|t-M03$x^oxCt78>oXP$OYC`0lCtaBQg^dv~(uP zS*`;RtyiqsPhxx1aIBwNbnatuGIi3$7OG5} zkt5vzXARjY4HlwRZ#$cvf&i-yd{4u9=0WIyV0+fLwhVmrh)kZ~z)1}n7G~XL@4#w9 z(74{|7YW|e)<*~p5=*R4{6oN*3uc(L(5%nf>EHn#A>1P#Z4l_H} z!z)(TcMD@3?fHQ32A*uu9fPq#83rB4*PP~~Ua@4s6V3GtmS#b0#$8ebD!5odf4v5d zw2NEUn+n|wE<1e3(UHzkl6ywghzx-)iAjmE%U(ThbEQUv#-KGtid6-o5_9majWEBT zx-ur{w!n$7i3#?8oDEzY0Q4StR&VyM33-N>S6hr};YYM-8qusKPBMqgCYsZg2htAe z_S!TfISVo`QXqZU99~*^xa!+YH2}%RzTUZ7sXgrB|4E?;Da$^3MRRTULmL~~N}bC9 zPc%N)zL#ls*>8W?MVlxIjn+k$V8@(Y<%ShQ(xYuq+UGkyK{D#;GxM;OXAX!SN^)i` z_Zo*FMmuI*3TY-Cd`wj^sCK$EyQb_n`nts}{p} zc#hb8Em<1KT(HXKfe|VNupZ z375RW4e`+zV}^9tuSh6Yv=t35!_d30KxhdLdQRR)0*IQJ59w%0T2jx(D%5-R@jY}i z)08WJfxG1!LBg^*psioL==fLfo%GVK&{QU6d$y#7;#mHYk>vvxkCWsQuV*fvna^f# zE>VN#J9dEjn)(Z6>Ax7!gjUrYA7Yv(52sZoj{*Bo+sCLq%mRuoq}Wkw4C`{hdRNt# zdP0OIbnBmH$#wp>m?&h+B7#Cptm~s@qj_Ox(CWbgPEd7RGJO%N?UfN0xUEy|hmXM)}u`X4JI4;Rbv0V=0ga z^&t?8ma1z0)a=v!I<5V=U-78zk&%X&z}off@V$%%bUmJxff}od&;l0!ckw3 zVtX>QZPOT*j>WfL9s-3`Au3`U240RUl378x${62;rBOt98u#kwZZF95utE=1Wbb}^ zDkdji2s_VP2S$B`L^Yi3$?)bglM4?d0u(WE%99GT&7rh(DPK$H9L&VfWix15_E8_a zOkk#!>IJ?|(KZ~&Gno<<@UPHG7?N!pE?zQU)`wmp2nKK{-;@fvN&QEbOAC1v%4`&j z@wdRaw(NPWcCRZ}iPOhUl_Iu?eKt?XacgkC0zm?%oG`2CvIuJWMvhd~~UEi<1=Rya6wgi;nf zmV@y*p(ni(Ep!%4X@$mxU8qmRDY}=)`X(v^ki$ng^j#*Cc|M_8>D(Qc7VF-nv+CbJ0lb#K?eVg zbMjhsEgt&XWaLGhMHsR_g#*N4`JNbyUcyL{58t(xkTketCtocKGsi+-DIz?_j11Ly-c*}1eAPgY zUGP0g&|4U^>f$Te+Cigndpm6x?I1^W3%&Gf9C`TVpGN0=mC8GP#n5^P-^=gE(B{b( z_8G?8(IanzL=3V_Z&sLRe~`c|-+`TX0mRVDoC7T~=lh1Z0LwU!jy6&|ygZG1L1?bz zwSc*7{U8JNmAj*Yg{atDaY7BHX1m{K!5uyH>g#mz^Ge1gixykTDs-kzA|Q;<;-_$1 z68ls^_y>>jDxPLOMjsOX1OoA%aUL_vYx>YQ)sGx6kzAm@-lX2NPg9@95eFnt4tuXu z#A3+RT`Yb)Z?$6`kpybtq;t9MMh=smA4%Q6pMu{dn&O-Bl096xVUV)bouz~R<|nVy zCAmhh%`?Y__XsG4zc;DV4i7r%&pc&xSYw>croY?DB*uPMvob8kZu~@$6kR<)iof6> zkie`Q{oJav)!XTQaWxOC?ft4d)fzkE7S(|vzJwt|HKm7%04PCYie?NoI2W6~NqD}Y z@C-%zmY>v2_FbSA!`l&P-*^6>K(MZV=Q#LxbEaCUNH50$Nn(95`X`CSZas9${flm zmTPHT7n}n7d@8%H$bbr_!mCV|*_svc*W=T`c>>nzZz6|)tc{*~bZP(GE0!_!Ya;oG zn8uFt--pSI?xq+7WS>fbn^HMe*{g-}RGs`{{h62HX^d=Nu)}k4?N%)9>qHr?6$fGF zaRRY_$IownK&Le}dqt8Snda>FUeh(O{M9SW=*Hd4#Z&oBtnxctRnZ4p?K-y-Al;SZ znyg(K6{064S$p_SlZ=e{so72Hh$y4vU2%T7=nB)P$0)`Ob%}~Wd%u_8U9HKYE>^%k zX=X#`6lG5p)gAW;o-(&ff3Oz2MvLdwP9bq?5Vr_}PtwKTG*2;3i!MrBW~}C^XyF$? zYge!3_lp<{&pGNyarhT&i0sws-Ip@*sGa!j{4|hKK%4O20;R#^rX{%}>&gP`z8wv_ z65$T^+;D&vdwy^%UB)>&-Ta(+^Re2vS^FTlcGC*oBkj0a`(U0%(<&WRqwBb9J6YqD z?YFco98JE`p5dUG86kx?$rpT!{xrIfx@7dWn_9+Zi$E4- zh9jMxHGiCe28}s}5{_~9WTMzjfeb4eTeeX^YT2Y&0ugkC3_UG>GJl55bDI0t`jZs% zmTkqWl|qs^(~H5jMY;eX28_eRIMR3DxMLrmLDIcM_&tjc`wOscz+k9v z{>8x-O{Eg!gKjnB`!j*6((Q(2lVF=broR*2wAN>6TIkq3#yA^YZCj4Nb)N4=sVPbu zuDR|(7_2`j&JiF0XON+Z!E5QIp5N;H=v|%rYe%l#E`@J@GYa2O-a=v;-Q@d-MbYH< zy)IM!O8>3ptlk?0dSrgp%<8YFixMG{E=uP*00Z=~lW-|uYDm^vYeUzrc|TL==&uzW zch6sd0|z`a$2k;-wVsy}$2k+nvscLH_EVpp_N*a~=$F27!27s{*MwGJ44Hc3lTfgv zuygv2&~SumRjJ=S(A1`l)|P)2+ z&E#%HHaarWLG0G%GBWp!a#vP2H7Bo!+RQ<%7eY(MEe41ussfXrzdz^98p{F%6k8FO z@Bz27;l1WIUD7ys=9VY($dQ8XZ~lo&?fu@L(Min=O3NBFrbSN*v0b*REC>as{y&b^ zZD?jV#Qu!YM0NPF>Kk>2BLbIe_f@JR2`0sva`{t}jL(^r%y)ezbFagX4Fre;fAD~j z4CTxdM>Iz)GW0C(R5CbtRD?z%@-GK^0mK=}2y_Flu>r>yfIwpf6TcsdezxXq~x}kc9DFc zIIp|I7!gIs*S&|1VIW#ODSn60HINn20A~}1(haYLM>QtrLo!vcei^w%yZsst6mn>S zbTE6-z`M>K^n~7N)|6YfU=U=n%0?uijzFqdyd^1j>#VIqr_mPNGEn5btJAbF9dqN) zb@)pVdx0_q8=I+rG915$Nwdd$=1Wc1SUZ;Rv9vcigxCqiSBZG0$5wz2 zAEwn3R22bJe^X#xdykFyos%;}Bbt=U6{DjPTaLcvJrzD}oKe@GHMFoj>s!8Pnjjf@ zbZxFqal8toLIFugX7DYd^0qFDIo6u3d5~wJM zJnrcl>X|G6O})E@-SQYlo7nrvrb=DNY-PG$jq>^4@1051W(&L`NO3;UO1C(QSOzJxXA*88~7T|p+ z0!g4)>+s0~W|UY{ulTuaBn-^9gg?k)KunsmB11O8-t)3-Hx)hN7PdkCLDZ`HIj%d$ zA0~z6qhoxL6!mL^hIUWS=pgdf#zUo@JzPIj&>`WyBM;iod%G`1xkk$*<}ZU0YI(J| zwG}(Bj$2wa*~VYBXb+3*lNEK)74?Jh3dv0(eGO8?G@4aoYJQ5Msk`qmF#5nt#l*B~ zzVay+77Ig^+SrT5O^r-MO^-t7UmQZE==6nDO}2K~n4;Bm`F3>G249<#tg|B<)0P4+ zD-lsz9n@`;1$%cI9>Sit$eE=-j7gx9LyQBU>O`y_5#tN7EdEzf}|&Uuwa`?qX^jKvFSgKETJ>U3{2XI z6vWwsYa>jeV{Fw$G*;Ir-^G8e9#Gtk9MIM$mkoZp#92Ua=mZs8wS z$P8G~Wqwzcx@yW=l0^S&mIKv6J-I%=ob8N?WD>?h32eP{ATS!9|H;0x6$TX6x%|WH z3TSG2+oyik1i{VJD>v&4MNoHVn|kzxLZJ$kI?YnmI7xv@?*1imQK)FHXGpTU@?Lh! zv~l!rdA_0*AvW8W;JDGvyyL~z4FkZE*YU*aFgF?UP3C-Cc?%K^ZjGnbbWVoJzbm(V zf_zdvcoYmBXMqK3R_cKbzJ|@9(gk$D-vt|0@f=V~_eJXz=v(*tYqh-8j^wG8whtz6 z4n0Msa&Etk4;ebKjzM-O(@TPX!n8CX+$FqHeiga%9{x8Hm!u%s&TVYBr=qPRCq>=W9Q-AS<4}d!CsdWN^FxuT zM(6e@15;5*@}Wng#UQq9#Lqp74V|L%O&HaX%Vr9wUimq zAoVv`d{dZ2m)Cso59_>(Mq#^*{)jfZ;c1flJ++8k{yb5KG|DWu!F*1uYF~J^qAw}Y z`hYTX&-8|ld~)i%ui}yL*WupJAX%+L^_gRKiy*iCSOx>PloRAicOk7%d0vblR{21h zXme2{&zCJC`Jm@ML~odPO&K(z#cAVmawYGzv4Q|2{7wqq@`Kl-cyWk%>EB-Czv^Tg zea1E+gWvCt&zy@;BcCe@bJZUpuD4EWaz*(+wX}SX^eKa0e#P|hKH>ib9!KJT&Rw*o zTZ2H}ytnjVT}GvKr$qQoseMiEJ+W_B%VuW}mhbxq*;W(}-wcdKWet}$vovrcsZGT3 zhUXl9+YM~>!_=B+*Bc3yZ+Q#p^-GYT$6dmwh%#M{Me*J1FEf<}AwuB&0kVEa)lGhQ zcz72jPe{ecQLIRNk^U`J8@UY zcH9Sqs_S4yoyR6&eFXU>(Z`+`;vLTI`qr$DU}s{AY>vq7vea|49C6OOK@lm>S17M) zLm=;;KOGuv%~G#GRZ}PtA8Jl$T*&uq5(3*+4?Yzm>6;m@kJiqh-qM9X=*AIVlXr*7 z#r^1(jbL^qh%3KJpwF)IjXtow#o=F*r3F(4_@~f!C=ywxmyX>rysg^z!{>J23@Cl0 zNV!%QX2`LqS`)l2KJl2d`NN`Ml(kZYe>|{c@TLIM2X$?wu-A)bY45_%QRMieHlaHh zga>kv45>kYQ5je}fl}%K8S{K-z!UJ*LXcQ?^Yq6QP6-ok zHxk7MD|7`&NQ#&w>Dp$c%I0;z>l#a<3xx;88kA!6vhWzKL5J|mbE3)zqzd22AiIS+ zZ*D7u?a#1&@Vy?&?hbIIAbOc9s^N=XFjJXSx&=nTMTgeT@2E!UniFlRN8qol>SV4! zl5gNhwR=_RDiCK6ze4`Ke#(ntm!~V{#3Uc&ca|q=XMTkRdn@WdxcG!C+Z(?|PJpJrP{xDzqF2|jkAG?I~ZaMotDZ%0k!^M=q)yr>2 zP)t8u8OSYhx(fAxq?95ZUa9pJWU?Q$@rx`>IqPxhgJ+s+Zvp(^rWc7?P(wi6ke;+?TbmBu*={A;vS1M^{N zU^2s3L*>sS%y};Kd8$%%p&?swcZ1G+*fMpkf%Ic^D&+e{iR#anGt_;6vU3nh(uxOp zp17oWPoYC3E47Am3CUx{&dxk0fh4MMQhfNB_aD?JU1`ArW`JTXtc8mCXPorUYUp+f zD&(raf)aESA!uVg&?^-R;Z?^CgA-LucPs6XP##TJr&*5?ii-xn7Vk?Wa)?YWot z5}HL^UQ5hy^0iLx4sEisHp>qW$F0w=8`Hp6yo72nZ-`ltMdDJH+YoAK8z17?MExUa z&v47mao69490$G2lMVM+s1^6qQeUjECG!HXYmbx-89-lns6ZZgoAE}{85u$s$h-^m z%Auhwk6c2;>FLIK@FWbnk6-Pgn6lL2^v1_6|0D| z3&m1g1+0Qf{JaMVkS9uP7y}$(t&oGpK^dfL;dqH>*1~6>HCczAoyP7MCJ8GWxiY!y0GXPs7M z?k@MJwx~w^Vw-f*bt-zTvC76trzS~&PIr+>;KT19vyWX1PZDvcE_tn1JAZm3h zP1dCx#JCeFyQhzLWUXLFNBSu(6f!QTUg9fWPv6eH%cSUfuw5(SYPvT|*j@7~KG()U zIcAa)I1IZ==A;UZA7P?StzL8^&h2Y*9fA^Lu~9{J`v(7IzO$rqsfOidU}u_u4Ifgb&Dh~C?*m0+UuG_rp%M@`wR(W?DeUuwN@$Obw$~)=TujzUHf3(2!(yp zMr58dPfBA1MA92vcbNgJTnBt*%Goeek~JsmPV0dx_;Q5h!vqmOLTL|cdtsARpVM%3k`K8T8@=)nB6*x`CpV4+|({G62b5#8xp)Ruv47b z@q~MGnK*f|TmiBiH$OqWNe*JY5{1WvJC!e)!|N_j)c4HYzksgs^xI9hUrd zkPCEX#L81D205Txuc5}QOu=H~kP54o1{<9Yr&b_tvB)p)yp}Esj>4wLZEBP#qwa&a zIzXgSR!AH{uXdxVFwo*A&8$=f&=lm?$RAVL44{0;k94IwKW(o@I3Ih`zE0&7<8Q^2 zo?t`v-km~w7!|DosmR)GBwlm}G^*u`_7}Y1#<&P9 zQjka+*6~oG^zc@w9BS9G&W92L(QG6|VBUu$LTH8<(38ty@h zHF@vUYUu1zbx9YHh_bdYE;Q#vnxF!hD<0#y z(TI78wlG7yl4*U{a9u^~Jx+LJi>|uNrSYW<)v!em(no~4C`F@5)s=)}c9hpj zh+-W%tc+L+VLLqCrpy#G#YXpJ*184b7r@u(@?E!Q%nj=ue|`UxhYll3jVEsaZJoCYD$N zAS4}hjdy6XvnZ~nZ+Ef^nJ(ed+Oz)JtRk}^>L4iWM$s|GHNYfGY_(%dz^wLNeGn>W zzao*4=nk`!4A@ixHtyv(>sQRh4Xxk}Zqfz=T6%M?p3LV_IPn}E{*{p4G!gcqe}3Tg ziZM@w3K21WSo36@xuso0u6jfxhgQYop>@Th8BatTgR-p{sY`C5NRtp|PMtuNr3m6`gXb7xm>so=S;3?0S$&0t59Zh-q( zaEm1joZSNwVsiz}j%+Uu$559JX8lcW69v<>XsA(aTj$|vx z=lUSD*5`M}S0B2Hl_!bvfk!MbZ??K|;x-0~*pky+=N!QJCpL12dcH2z2NIkqd|kRe zI0aKjO`NEH20MvT3RRTsGQ3O#oYiBk<&HL~L1yM+%4_NY^IJ#HdJ6Pf$dn~mZIB86 z$yAAN(nQD`nN_-~S8%kT9k;3p;v{X+{D6{NElEuSCz;)dSQTfI&6*Ga_=n+AL^;kv zU%Kq#%dg5LYb!15h$4zLXr9}MC~mD0Nmok*5_&K=dob_b28Fd)#xzmzQpu44T}oo&lj43(&XzN%O{PFr?DyMt-%{i0ImH0PP+ z;13TN5~-~4Og!Ra5Nyfy02=U;=F_XJik}dPLGblPcDW@PTSBWBWr-BntJ=}eBeW8) ztj%#D<&knN1`mtsD12U@Ll*-$Ow;=F!XWKq#N8pglLnMuAajuQs~p(xGOh#a)P;wK zuUD}af!;>XVu0==`pM+Ki@Zrra_m|P zGp0}WwdDN=G9_f4B=?~wWT|L9Cb>}$oG1c{G_YcgGZ}0@^yvIO4Z{nrn$gi^ z9TcXeOTQA!8tZz`d6|C6QDO(Z>7m8&&Ie7rl12m2V4Jc&oVfbb}Q~mAJ z`OnM2oYk_mo@2A>O*Zzn&@XvbfUx&@D(dZwZu$%w^pB_87O>UD=&CG;z`oER6Pw(t zt_zY}tMk{^#=FmR;#fscC#5@dD?4>7aS(9#S$RsLxyMQK$ccblXx9%;RnN+7_q6rW z{@4KVRE?Tal?4<}Sew}6FxMk-;LFgEfEQ4|v#)#lao(KLS1}VJ2YS-R^@(HS)h&fB z4G=1^sjPa6nns#~P)yZ}}ch2`Gps#H<> ztJmw48*7acaRO3yeVP$J=@^n+>smzbq%XMIklizVcN+=H>CEIXe|+u*Hq(jlbV`!l zr1OLJbfMW|-nE{FyJhgjG;Bz=HZ!;-ccr34^)4?oG_99638H_r%lA+<<=PbDc>fFG z=DT#ZR~3`4)mFELP?hLb_kB5EfoIit?GF-@S2RK((*W&QU*DAAP2Uzo5{xd>_Ek+u zkjCuU$OZ#6Ygv(= zc=CxwYu!TY;KquZY99Q|LT7}#R8mw*$**4~D2 zXGU)A0%srUSoC~^CLl=r6YsAQuf%Eu+do8&VAQIZ4QTPO%5R@&EVN4_iH&H~X=4+t z^)(q8TBC`rI7G@pX|CsQ_#CR*5voBeK%)a%K`U2eXcbVkAPM1%K`UQLDwVRKA3n^r zu0)T%6liCNM!vBs*5Qa5IV#rWh;RKSM)QFy-E!3Ix}hH#g+?sjAELtWY|zIOrq{k3 zUKOOR%$n8mVqbV~{R&3pS))x3q3FDV*jSJ%hN|VAH+lL%;;+tE7e*ESsOCf)*)&qs#MYscqW9) zq8UXP8x1$ae0K(y|7)DO!emyvF-+wxVDvq0vG*GH+iOc%k_?KGhm6bVoBT#Yl!j#J z0|iP2bnVyoJoE#fIfX`eLg(G2Xc^=KKAAF=qM;w8Wg6et!i>} z#JaJIU9VIIz5e0-;&SEvLJoRGx-hQTS9W6SGIDzB zGO)yTMDx67A&Qp#7Gq{7$;t!q=8)!*d~*GX{Hq5&Yjb!$F!<^|A&0z8@6g9{@_lSs z&z5jU(S$*^1cYJLp+=CvQTP++H5{OKsOiTwKK+D6OcVQrl6`4SWWR&yijSZ?pKe~C z4sXs@VIdo(IjK%+m*`!@JU@O{LqXt8FI`u?4&AzJaZy`Y*5)0);rewgUK>N5!?<+~GA2wlSSn+5lC(puYH&y{@rl0vQn zZ$%M%?gpjfcZdxEMMw~)#WtSggPqCj06A_zR~55BVzPx!$WT)txWKNfmr#JwPS$i# zn`|YiPAialwL_GzD%H&JF72fj*pZHYOL^<$;A- zo&G1a&(D+mca9UI@L2)Eyx>l*JWAbI`~!&?TVlRg_Hb(~dS%(8LaM93jvX?@!QzYo zsCosh2&2xH*CT44A!(bG$+;&wF>0bj0>WO-G!;Zn*(Z|bTT!SNt=qr9A!zHOvN0nNt+JE9HPZqRI1Oxu1-Il zsOJks>leWHa|%rnI{LDf*7jBUOt>S;;QL5%4fU~tRwI|}vLrj$>KjK_14k>D$d8c8 z>oq~m21yTcQuZzcJ16jU)`am$DJ!PWAx5l{Tf4Vj#N`OtKF7B~Ma&cfNB!42^^663zua8E(! zA8)X1okX9;)ME|>)D2bL|gqJM;6f(;CHPFR0)Bgi((Ry5#t3ywGz}MLF;Fhc3t| z_TqTCt|A2>2^|=@Z>`Aadf5ODh|jrDwD)_H#KunO0M?_@oUW@NX zkdK9c`N2+<2GIC6plV#0HM zhCBMGL+rZxO$~`njNPHVy|k$3RVk^?-Pal0LBHRZC9ildP#b9>yqw6RQDwX}PALy$ z;x~CJ?>TjBRrU1>!qtB4?&;;(n9}*e8w;X27&shIt&RWrasEX2*=}OkD5GtwQPxQl z0^>*w;#8P#2~nk9$k)FF-*9}DWRzFXYe+OOxir@Bi#(_s?N)W3_lujWsRAJ8PRz2L zBYXC+Kd=-4$Beh&wJ^?S7%9OdR*YGo1Uz(%Y7C2S+{?*`FY^OM5GxgwJb=o8Iu$(l z_z%H`FceF12_}nDWZFiOruzi=fdPCf%;BFOr9YB#p(i-n0fd94a!i)Fs^hw4{l-KZ zpy&af08m!HA_FW9cs0KygQV5QMXI56RAah^r9TfFgK+NFpcy|&R$!a_stj;e;9dQ) z3@}#U9{oBE2v+Qp?Fk1tN62u2Ls|oKC50dZ1wnCvWOK0^{0g=)IpO8}61Gt}ZIbQD z2RX;dGC_`717XOaFnY=Cv2=jcd)e&qHhysQ(%Iu|0J-(@*>BMv2!?+b2XhCl8s3IV zxkZinwaw)Yr01``%@#VhUIw)1qyQZ3ZQC9#33!J`9E%u?i; zQ{){|;sGTR`apXtt?az^gNKyW%W2WY}y4&)8e~$u$x4CmdVv+-EPqz57I7cW30=++ z^Zuu6plR5+n!-7m(z%&t*_(FSn+DjMlB|GY_nHB~tl8S@=q6XP&CdOnfCYO+H!x%SgQG$7F`s`C~VgJi02Uzui6f%ADOUT(!P}A!Y z?z3Wj($~-(;EIlHcrVzAg&CxPKN5f z3wJeq!$oI}npZ7=&45k=;v1_5|I->^>piZ3{_|J4!`JHl>MQi>D2Nz1JM}ZMG(SOGP6cbA9&#VC~ zCd|;E8w21}$T4OaB?{AlLb<6>>8$u!?n-5rwGbEV`{DpB1M*Zbvj9g%hzYQ}pZ|bM z{X%)m!V6Rx%$h`%9C0JK=f4>RfJzH8-V0s7kc2f<4r|QTu;kZ>)xkex%(wD`-Cs5r z$r@a{M-2dH4Z+hR2Y@*T_v+CDAe=+|3u5XSh_Qy^U<;tMCUb+{b}8|PdmQZl%{{{D z?c;W#b->koRKschlRC~5Ah3q|VfT<4@B=6|DCBS4!LtJ}w#nSV^a3b5FGS!$$qXnl z!KHfb8G$1~3wq2M!6U);dfXX%E#moSEfTq+B<09mnDLc+R2bo8AXs{27-9aCJ|SQx zkjt~gqUK8vkWa5QAkqH}=Hk-=!5L6#fF}o_8IWm!sb%PD@3Ag%N4ab?KDb-|!09EQ zfn*P$GoV=kZwkOOpjZKO&Cp%nV}0a~^4@6taL4@t(Mx2H2`c#C+{x+j^+cKdz5M!d zip(L&5{K~i-`FuM+q9ezQvopB|3Vr8INRhcV737~+tr$Vd?gNB7QEZY_`5fdyx{UZ z%Ga>G5DY#4g$gp17l=f^JOfNhu>M|1M&wA~f?iBU^hl7s-ar|r&r#Mf<|xTD-mh&Y zFd4A<9%n|73DEi;YX$g2zg7c$NIz%=sPdlQfFCLW*a}w3YAF`96RhYC77;i9piK}B z=;S{K24EUcYJV0D;51;qLx%y#3MBg481hh*diyB zwEMXK5;r6mp(DYKdsb)@8XPo-o@tGN8_3O=EKJ>HfD?LA8PQ}w=6hioQ6_-v1-br7 zkY?&2nqs2KicnS5j5+NcKVmEuKr99E{<;5}9gU^KPEh7QqdZ_`}l|WX# z+yMOV-Jj%NNi=0O!^+5O1hYQiUkM&|-C2VNpqvA@_u>Omegpk)`OzK~m*s8SJ=uYH zoD-@_S?AS)_y5}lcFB)~^cO@ak0YcPOzy;md|=Z(PS?O4pfx>Kh-4T%gdBg68Bt+D ziVQe0fyDe34OlTjhWs52cvF9n``5uAW~)FLJ3<&sK^#_;x&JRV`1c?|R|*2tOANqV z2q4P^q6HhD;Mbh`UFZL1Qgl5^4bXZv`QNz!0Pp+pOJ$Fp3G}O%%N}3n$Ny0^5b!d-?HfYuDMIWqJ&mI9X8ZrE zYRFg}VfkmQ{7HogFXk6#pwcAScWiAe<0{j)Voh)OzXr*+)gr9bp$ThaHFhTpcKm-= zQ2!nf>dzilhvKaZ{YjvpO_ry@&iXYRdb+2f%+R1%f z(u-z~rUSCv3s;|*!8KHcyT{(}jeVUpKHkd!h;1Tw&~Mt`rutXXZ{0v@g-CQ}J<5(| zqUz@!DoS*fk-*xl?My_P!TDz-lo<|8tbOK#kK~X}o0&~rnWa1%(a8xMHenRyE%|Tl z_&ZFd-Lg<6bjtpJL&87x^D1p)QGr84fdj18aO(n!(5a+7bSC((9!-1rObCn~QG2L= zMnQWx9SDw|?+pD0xEm!-YSHGI`NowwuiujJ81y!id!P69e=F!joS%t0CxN^W{}(yX z^ZyWb&f%3k>7tI4j%_C$+qP}nw$rg~JL%X?cG9tJ+g8WCJ#*)rGrxOh=KQ;#XYG1w z)vE7%->=rHB4xmm0siydHv`@T5OXgL15R=frvY6$BuGApiW}23jw{oVyAhEV(502a zd_+h|i!w2b>+>%}8jvVKUJVfJ9;G^18c6CMsk;9Sn+*_FU>*ZLc7Qmb-~;*zE#PI# zR4w}K;bs4C_fa#^9aMsk{$UU*Su=k+zlE6DSTd81Qo#B5OppjQ`Cp*uY+>iZWOp~S z5%_595Egb+L&@@V^Ty`^gdZsW=Qe=kCi4DIHjq?0estpx)BA=A9U7p-fctkcWWbIF z{(p=rN46kiZ521uGG`kz^OPa;Z_|Q|U~1_4Zi*0 z`M2~KHS^y_QP0WpkQ4JgCkDcT_Hsg~9yadZq{!8av(%}fr74=&7D4Yniz;qT-#WO+ z_bl0c|6t?)8!G>WV8Q)|>>w*?4ZpZKHS}w0$j5?rhsZO0m_&;928^dTUU4k&zW}Ly z0i5zbf#RPhqdjWWTp56=+<`d{V9=KxHik(5W)`rTk2?YE+{=RqgqDx267XUGTn53K zkG>EPVt}Lpd)A9Q1C$#0Uy;#_pxTU2tKX+d?b#ydX4b0?_&>NM0=$3F4k8C^AR$;E zAxNx0*$k{!kD|lG#rVE%G>UF?(neb84aa}#x~H9F^5<4E*H#lJV>-DJ!+)J1fm93xG(d~_5DS4^ zb$^=^lS*eD*wO%OIGr&ip8ti9{t*%y2R3zMcy%S$^iHlo|H%~uFhzQhX23E9VK|^; z1NL{oIKIgR%wr z;Pw2|cF;U*Q*ke?U{8lg{kGt+%5jqTP~!|Y+wPTk!h(JYTEFz1L_5&#Vzo_-!Z7JyA&~eEsWGeuxwFApN z-!<)RC-`SA@`h^h&l&G0l1;U~NaIFCPByODg~51{uXQQ>;q|*w*a>t{(rcqVktDY`+~N|pTyXs>wvHU`LAbx;sV|a*gy5U zp~Dl+%H7z--59oM$nBU~;T98t%s(O@_TQ<&f5zf}piz&NGt`m``;r^`lH+#mu<>8S z=znDatfq7y=cW$Fst!k7E%LTFxjo0fv%o6b+25*2Ky-o9J4|gLHG$SUTy4O1f!aH4 zZJ<|~-?z{0Hm>dd1_N|2NceHRC^x7)pwqoDH^`kJwSs?GAbZgt21Ut zWcpVkfD$5vCEkIu0GZ4O#|0mrP>OwGkh^D)bD}H_PX4FB2Spb!z60F`T@!G=1JU-) zE?|8JzRfz-r8dm5JM7k9+Urlc`$ui$e;8mih3B7*@NQ4(#9wPIqo5f4ZJupB!P6$I zc(5VD*21cConjb1yV6vft>N~`Q0>p>MMT^wi%sq(Q8MC$cfrf5e zkDlqezG-f?HYv)Nk+0SOL?-$yyB$fR`h4$T>tB_y&-9Zpfcb)&y)@QO>8P){{cf8+ zQJnWWSv6%;R|IY>0C&#JVot38g`Bv68@*e2zmm2PU3sQl`lPew-Ii*o%Zaj$tVh8K z(hsB~yS?v|FV3Sz?!I|=bZQLB2%d|P;oc>57`!Q`@yhLW9rnqSzr<5IGr0?;bU5`) zwR29U9_g92>B2fRQ}~KH=1wXEj^`{EI2n#g)DP+SNHFp~^yv7kkN2F`Vh+5=a(0&H z0dwEtW{P^et0#gz*UxtvWV%XEtY~xgrYgN{uYTQk$k+eMac}S&kPDKP2$?J#X5!ZG zxv~q~iDjGBeI(t>ws17NYiW`gssoFc{kH zy>?}{-li07UFAW;O1+!g8=ha>o} z<;$OVsR_6V@3R#HdX0y;ChmE7@zv!Ot`#PaKzN&($WMbK+uIn|O!~p=Q6&N&3+D!W z$|un~$t$b5MR(90&w6!9TqIklT#sONDd@s`A;w&whj79nC{OcQGe}_BL_K6Qm6fh*!UEY}R;MZe41k=Q+l@WFM6Jys z{VAg3RLXFceMO;#H=!qaij}g*M%U`4{JovglEKIh;G$2fbIyToBpoqSJgH=0Z-zmxcBBm!YPRpu(;XMIe(dq9KZ5fEYzf;~b{! zlVY$b8tV)Z*b-)vcrjY_0H&xA#&AH_4?X`yh<7F6IVrUF+TeARDc=K)IT ze1~Y?Y8i7Ah|l#$AD?-ctZlhXWJXA&ZC%NQo;bFqh?h0!5s zruZ;>rJZ6~vl*wkDlseO#ND-(oJz4k?S7iUq1Tj7bjWC>LHdNCa^(W3sm0sOqjXKY z`;Df#INAMRz7VR%Y7wsK20cqe&xkGhm^bj7rrGqUdNDuET?O2#{GMLoJp|n9{7yU1 zi@^08~ zYygSVQGSSNopCHd=9hdP;{3_&_wGDF;8GP2epT;x{ySfqN^8#_ zRLIilsG6;}-T^^sB8Z9S#NmRG^;s*hk^Pej$3)b11pf-Bj4<2~PN$H0M*ryr{~d;X&_G z`S#Zu9I?5{@hiE~^asm0_IC?2p5Lx62uJ6+-~j16`WfCufb^sKc`o*5@vF6qeDzhs ztF^0q&ehz9E5N+qs^JaeELRjTzN`P!8~waS<;!SOxwd2O{Q+PeaW!^BKg$bu!v2Qi zGOe_ka;RV7oqAdW|7i*^Z@(J5rC;Wyc2O7qg5GRYa(8{KUEr;BQCI(>+HBNvcl}+v z#B1%M&i{qI*{J6J@Kn3Vd+xHl{C@5-?X~&f`sKCRDCqu?;1_hYTGr9EyItnZc3Q*# zXg|jr12~s|j{%rBUcHw1HD0Y2c61$X7kamx)$l*qFY>OtEa$zno`Q4VzvFQY&SAM3 zf#}BWtk(D$P_ETr?Ados+4mfz>7lF5f4Qd=7Q-{?7%*)N!y=HwR?3FHtz$1S^cRh8 zSFvKXcsmm@^W<(gATjeKY&gI%^W5|1Ary#6+D2swPVbD@JUef|Y7 zCop~X0hsffK7Xie>ZY7F9K&*1^c}jG`sD}z7(Zu@d=k$<_&q7m0^m5Q;(wPfbo`bE69^sE$QH?9qzmz27(`&ylG@r$gZOArvA zoGkf4-MJ4-8aAegd0d#tGNl9_wY$LXaP;t1C+u8xK3%gFqATbARh?e9eQ^+c^sSZY z1YRxgF)>(JRPLG((({}&=tEla)dFM@)vIdfIBQUH?S6fy)PU2m&KFJOAq5T(ksWWC zi+al!qG=ovbk2s5`XPRywmiT3y-X#W@b1%Z4xReSRHajWt*nkh8T0p=5@uiBoYUm( zNN`utO5@>owRy&FtN;d=713*-Z9&+lMtG{0$9crguI<5!2Z>DL*U=7*Z2BCsGN>m0?{5JQ- zL?2RdUrr7Bm?-L$Oj|HY^mg3&`VEN20hAR~B=& z3c-|5E0D-_tC9l1Cj6rbj(+6jz+NFPU(U!XAGlGCQvld$1t5o3I!}R}X0DtdR=%sz zfNpx?9g>sHsq{fEe9$SL=q0v5Lcyz|R;s?iU>}>8&V_{6J0n2`>)4l`{S-XL5qdZ- zKix=|A(@C|Xw(R$WEp>Oy@GyS@d=X*%Jy{O8(){vgf=Ck{L0S0Fvk%IO7?ve^k`;D zjZ=UW;N&Se7<36n=Dsk88&7IP-Rq}oZ~ufI%OF7;oB8TjNx&cB@PL5z|TJ=w#hCHuFwZ!~bEk*E9r`9?q+(fUj7DfAN(fp!Vc7H6`vz+6H z_&7@U(H8pRSZ;lA^ck=63AA>Vil=BkoJBIXdf7UGcS~HA{NZ z%Rfg_U?e;yyRAh@dRGXUBM}LULqAEnC>oJRfmfUCYNE$F2&&*=s?uIn0+@QLJ{0ht z6)d!A=211(tA82i1Y)0|E-01e%N2ZU4S|+zqgAr#vt=~hKb5M=s?#&Mv1|XNpH;M! zD*VVUpP}98H$9^207=$u-`U!U=VO%>NvC5qh7`G_Cey4uQKfF<@>Ng9FPR_f^a{r7 zJ6-&cPG81OUrwd!4Wtk6XoU0?ysI^|@>ORmU!HSiuto)hnn&z)6^?zH{%lk$ST72h zx?6b(rZ(bN8}hA+{LrWd^Qlbk6B~4wFh36L1G{FXe^H|jL30dRbOhx>j`b3B$>Onp z3}QhNdEyRvv$37(94x6dqh7FS@J>;qxX>80ar2oI8$`aZW;wSZ&OwXJ()DRfo-%1&s%NlOAdMuUz-}uHpiFfdR4UOJLK{yv;#8+Gk3`d{52RFc8k8#07(}Z~ zH)2&+uvn%rN<>YFOGk@rmNFW0k*lg^eI7hSmu8rwz3gBL1J=x$~c@GDV?pI#_FWFhhW-#sTb>N}}U&@JaIzAg$7f+Jz z=AaDCpz7-jf6Zav@(mfd7C!o%2x)8jsjZ`k-Utre@No&^si8l=`FcCL zA#s@_JHivuF-Nqa646mdw5bqXgPg-fFqoZk+O zsJtDvyEmZ?`((^r!%R?UuFY2CaIZz|*Uy!2W3+1~Ruw+JDg9{^(w&Q~AS zzc)o6!66xbo+pwm8~!Hi?T96`v78t_AKX?;jx^i|x>->F8;&+`={jyLN*ZaeD?tQT zp1{pr%-hoqsgoc5j#y9IKmlemjmXRo*XWTvWcssiHFpIZD??sOH(qi0wbQoL3!Uoz zBmF3{zrbG|&ZF6X`E09j1c__aEupZ;UJ@o|O+ThywV?fIZ~`J{GW);?)~+~L^K=B) z(v=p1U9T^iySMGK7;~eOh%Y@|EGVe>HueF1wti~zfn90XD+4*}QCHOMKxJjM^1x?h z^DCW2J!`1i8nx0+%cmg@qW{g30oNpGHy)WLyHLc!XpO+EXd2Y!-V^avAKJ5;mw(wp z<&r=6Pno5-?kke!-Yw|;3F^!6DOb>bKor!8G(k2c~4g^4a&}QT8uAowS}dJln<28IuYObI-W}c=iBCS;-rLttUyS$F4=Ri0&J}Vw6YUfO0z{40@#9+!iFKZbUcwqQ*3T z+%YZQ6Y~kpp81UyddB18+!dBXhgJN{RBBl*KkX()C{@Iyz`|i2sg@9qcc`>Icu-MZ za?d!<3Yq15j+4!z4`2E?#XF;+xxiBf7SL42UfMAmOfZ@f8 z?nR;axqLG{{yrEx9sN4s@?$Hhwu~^`ya*^?y?Yv=s-k?jrE<=)tUnMu>IWC$asTAg=4tM=u5X0!ax8{0)R$IUF8PVF zU+{BzwB=<(5^Ar?jVkYj^XrfIi|$29=EQLFK3_652aJdJmfq@+PH^Uf)HaRL?p3Jm zy|D9Mg7uRONaB9e(3OSly>xC_1(*KjeU9D^N>d>N)Fb3lQoU$q^=mv6GM{0MWR$el z;h065)l#Q?nw9OMc}o>SE?V%qT}~1wub^9u>NVc(Y`AehI+tYu0ETKm-P}kQLgmi{ zP;Zpx-8#H@@kKzSHtah34tX^-p6T&FY!>8+Wso5uQDyepK<-R$awH@UoG_p~x!DYu8;ErXl=|5J);ML2qbLykfkSS%NEYz_yL^`#VRGI0Vk83}i}9S1qu| zHMGh`HL0Ta zvCgs`-z^M+_q2(9d?pc82kf?6%>VvrELr6FMyvP(idgqasOd4pB0TVSxS)Rj(aFpj zM$wU^LeImq7}D=|X6-z|o>QB7X$Y~F)5D$w;$gJFr}fNnBByBRh-4YcA{wtvGs^}2 zJHa=}#g~oMrUHg|bW7npa^G%<6wTVTkCT)aJU4zNzblak@_4*_^99<#UG^_8l-&c> zEQ&j|fb~Xg8y;*maghx)oZyr|Sj#-pUuKnXb(y)GEz~i)6!^Bo!<(S#suJTswhOi( zDH*&XE9*b*=qT9q^UqQPXd0>_p}Td17391aYiZ=)vL7?X1_tAIzbifV8mRv{zc%=Z z<6#}Qc2&}*KPZ>}7|nZKVMcPb&6WTrIFUVI%AJXZRkm0U9*KpYbW4cwXE}GaN7ex< z>>5lBK6(9i=oo>3)+lOIuzf))gZ52#%x5+Dm)^a5LTn3M^BA1!=^8sG^Ps`w+F_ug z=1?on&q7_#ZMvJwskbfQZEm)J6|kvgEY3XeW4#LMRES1?=?j>l`2K)>-jhhXAL!U# zS9P?tKTEYU3GdTR)+|g998~y()&aIWs`gvuYjH7`^_NOzotxT2Le2Vm&nNJ)6mMDJ zlhtlT=k}y5{FcYl@q3;M+4=iDO^}y8t|KRZ!ZE_ioz~ijor{s|y-tYV7!z((A-kRs zT8TQhK|mnj(qmL+Bq2ilTYgsguYNNPWs+5s@&TI5pgIrI^WnY`2Ou#B*ny<1xtcam z0!KD;@*>RD4zCcZH?&RTysc-U!LQHm;yuCdH_uE`E^4)F*OWCrReNk_o$8TX*Mx3c zm7>653_GmE?(sO-T>g4`=FQhkJfHQGM?P&F2jh;UoZ>g+S|_u)h48%NSIVEAB0Ch4 zSz(1YScx7HnG+5kBC{|&$|FH=_Xw4Vp#_dT zk4ZZx_6|qSk|d%`n|aj1I-LqaeCw9qvC{tR8Oz6VTI7gPPLBU@IKY9A05zMmM;bki zK^R%d?O(P^rxeou>1lOQX<&nwcXGooa_;)UzkVg29OW| zDB_H>Sxt{$wDs^iui?6K*Kllj&BH;@2GtbwkOB4(GHFZ6Yb^|jFt+o>IIlH5u9}(f z@sHhzB@{gTYd2*UUJKQ>&ea2q*^yj2YM{4p`S%`~CKO~6SbiHHzuK9R5zMXm8&IPeTS3HD^4p=YUcb=OwI;f%m2JT zZ|;4gaH)e)XgtsOqZqSY32c`Z^^+d+3c-l%T@LySEm%0ICM@FD?BEXfMw>Tq_$d=A z4u0W0fH@E`6a$9A#0=5M(0*vfbYD`(vg0ZQ;n?ZX-nsp@eWO$7$}l-3c2#!Of<9JS zd1r|Np8CMzEHXCJ0)=4r24RzV81~VFW<{`1LpFF8H`LeSESBv-#9F`Cmo*Aa>ze%+ zcF2#qgKKfaHoH|@ZvXDm$eX9_$hL|v^>At-i5&cL;Yk2?2%=M*FKwiSwHI9I4nUQG zN_F^UM756&+FW(0{|q0UgaF+a9YT>I&Ms;XtqD@JmXR^Lq*rVQkq`J4{7La?M@T6e z+5*kGm3OU)WbNLoso)w_hhE`WFs|6TMBG|U?IU^D9qmVLqs+1E^20RCWwcr5R`^mE z7Thk@mBI;LX6p3QE}P9U@JVZ7bB;^P>!x$D2e(9h+D|!Wv~jyNv>8Zz)Z+9{U)Y0H zJli!CTQ5k2ge!jOjSk-pVC*|SLtS~xLio)= z{iTqFs>=w{I>BM`mmJYczYW`J2#&{kBOL8MJk%RP9qqi0I<@(GZ85J~NXgnBP;_5H z21HtY6Jew=Em>t;to4u7j1sl^SHTfXEM#Te&P&@6*{5uS@|OlsTYW3E-I--fJt*lJ z7pwEc^Z-R^=|@Z3AmzohA+Y1ftKyGIABO^(l=P{&oiciK&F_Wp74Lxe)c2bAT-@(| z#C~+*<2VQQ2ixN~hy3IGo+#<;F`lgq_UONl7_RM(Xic_^j%bayppSURI$bB$dpgb2 zTO&L*k1kEN)+g2{JAX}ZPjo(IJbn8;q`8GU-Z6PsqP;~k-Z6f6q^;L+SY6ktn%X*a zx1_a&J}#}j#Wucf=xMLD#hvQDau=($MV^}0*eTiKo^?6d^sB?h{?8tK>swCi-Xo7{ zt6Tdmt-X)AjlVE#uT9s-*S6%=y@wwk*SEr2+&3Qinrm@#+6Jq;(Qb(8E$rI{L-=>{ zQcHqnLn5>lStEV*>^39Pu4~%C(!`9G(U>bd`P`Rw;NiNZrCNZj$h?7{tv+k{3 z#Ona>c5}A@@%41S;J={;7#rX4;p&xI1)JQ4^B?)*__kjg*HiBkGW8l{sV>%k}4YIB{2FFaq}IHbMlt;V2p5 zQ@NZ0<3PgjfvZC0rHnai1>0Toq&g`p`_qYm%_TP_NW>qUPKMAU!iaudjRC@YjzSfbZn9dI}aZwKp|*dRS51 zNeZ|XtNu*yB9PlO4;_*-RHWrB!Feb! zk8-Ar&0~#K&URhmh}v7x+{YXs!X9=64u1t9HiRB{s&^E?0?vv4Ol?7JNy(Pw`0O2F z>%G*`;?9}J$=SaJtA4iXLI5Z?xAvy1rC_yB5C~waa;DgY8`s3`arh0vDwz8)R}nk zV7vs2));X56Za7TNg09)Vc*>h^l?6avt0)}DJHjXUSo7tS#cnJKymp+xU*%-0T(i$ z26cD{HztOoOju&OD^%9t1?I*GV_)!1$I_Dz(SkJs={cKD781yVX?XF&Boi20w^Js@ z{f4PWL@eisEi|e$c7^*00KG*q!#T77GEBKM0GyVIth^YbXd&_NI+Vy!T0nKDaE1A;xstGID&uv18axOGmO&shI99| zrwqdQ278yIm1ZxFsM-v+SCDcFGUdogQ-f2;TpvOsbV85$JoxJ=#SfS<1GLki;|-oy zTr&h63=WeO2MXzDCx-#Hqh?38_7UPKDsh`N+&T%T``}nYd1VLiN@%VOy0%qEs$uwH ziP~UD@%8x=ljUZK(+`c*g?--8D=(wtU7*l?AucRf6os6N&|DgYt#tv<^fLQ+iBRZ- zH$xp^ic?Y*LS@1UOBE^Fl*G^pv=?NpD5({Ttl3*ye~?Ksfoj4=KB&nn@GG;}@w3tl z`B?u}QDd(ib{O71!=oJrM^hjW;fKW}z$LNFOTMn|^$ zKtJ$DI^t}G?QkVCj=z)P7~Vb0#5Z`s)3h)l4GPj;oDs}e9Il=wjVj90G9s)C{Z7U{ z$5yLaOIeO^)@JtKqM9q1Nak=p`$Axi{#n1 zwxisc?_!A_yDmfE3A%{7;lv7MAH`dqVD?zRcnCpUFQ|v04>_L{+F-LH2J;_e+hdN_ zx3=RG6oiH|XaSU9z~#7WA;boxQ`70CHx={=au2V$R-I5Gcu{TQe_jlCS`HX=8&no3 z=Ifjz*oW`Tz!8*C_|@}?k*`>kH!Nmc$sl|I3B`ELj0l5rsvn#_B@AnoA{dR1uqXh8 zm`wevhj0}VLfAQbf9HaW!c>7h`On@V$Eebv0{$249B0Gon6E2VESYd`#qwJL)yQ(V1~&gf`SZx!ofK=2{uYAq^zRT7c@0g@n(oR?S{{_DJW~2 zCtm2l>fzESm_{X)8QMwjfwv5DSf&rO0c&q+V8Sk^vULny~~g8RZA2ca~0gCo!fsQ-ea@geC5Ftuy4Gg!sB#Oe9qHdF8-fn;ZIz^|yZ zH{h$WM@1we?2DrhX-CgiQ~fG)96_vE=`~e1JT9JLtq5tnRzie+fQmX0pkZ?0+6n%| zcQ#&83EH@V<1j2OE3*`gSg}i{(H`Ew3?STT=}L?o0>j*k21IFVNN?L05wsS6u)wPg z$F;`b2q!GqN59%p(MF2KwAX`We#J6lVI+kNH9CkTlCiMjztdDTQx)M5 zzD4TKgarK@tB1ZJnER;tv4AV&A7e)%m*x{Pw^owKmK#QxF_|3(Bsr8FM)(TjgzV*i z=^tNexLG8cAGC)nBM_;Fs>HaXKiu!t&8*blHEKZ9Hr5?+mvJVB0Z|!+4KJ#w$Ag=5 z8Wj~*L_s%(NC$IzLKOKk#csg07K;0W?Z8QcS^XI=xVa~1MdOdLwH2Zd`v0;wNZZD= zE__Yf35A@1Hm(D4Jrq5S_ZVX|&TG-zrF{u+Abr0N0rr4(lHe1x$J8K*z`VRRt|WLV zER6w5&%Y-R<(pdK1dfy(0rHYvHM$v24VPSh6i-`q!<>JyX@3m_+vXNbY*AeZ8n~@ zt?53DH&~RoF5}m$G$Uh`p^pcApGx5dk7&WYX(hv z3sT)pk=HOqcpgmE#_^-f%tJe_%CTd?1-;nq_T2`*#Hr(^ZW#q1JDxqT`lKgmai_%~ zO1kW7x})=1gA+9JEGbYfGeSgF_Jj!D~8H~ zU`MRw$Tgx*V$vQp_QeXenvD+NL6+Sz1ky-=osX1F&x5qWTn)f<6!Nfk+8L4Nl=EIO zQO?p3>O~y3kpnqPZtf%mDS9(z?TZPt6U^|mQKt++7lfL^AICp#-(jbYk0fHdEcthd zp7gdm1PkK0s@aLyNbngpQ5ZTBcb8%nsH^Q-A*Gg-O-+*zD=#^hYj@^rLx{-GM@OWf zJx%Akah8E9Z9nKs;aR6K%J9ZD0b>-0206g_$)ZSE2m_H`uGKb#G&>WAkG_E2;dr8^ zLtsB6s5~)8^4OHyFV-o@oCG+{D?~V;Bld2HJFO|<3sHyj{mN$=XsbviIn1v?8ZrK6 zZVaer!rJI=D=)n=V~<)#S~&$4jOf{2U}*9aFke!nM+u7H4Lm00@}S5NTO=Dnr$vH@ zx|g&ds*h))lW~RU3XWovksl=@QhBwD)t=?;GRbC|6QmM$Jr~q=)U3dmy!!Yf-ZM4CX`J8D| zq~9C00pdUVZU-J>8@x!0x0)B*4C&k3q`_zby-=l|+a{nY-M>>(Zbx+}{qxBIa!J!3 z9GOyXQ@3|EFPd~OU=(NsBrU}jSqw?(Inw1JlukvpPGO0{CG1%@Zal~aa$=@z%2kJa zhTnwyDl29wS0H$(D6t8-fI}yFo)|HehVp{tG}U)e!8+4}1jdGlF`@QQ7PNam5fmns z?#v^Hkyit?+(VU!TEL1{Wbav`H11VISmpE}J@-DehC`e-+7A_PqtvOr;d@LIk3Zj; zcy*?frHqvEEIhcMO4famGVlUo_{nmI=K>j`38b!Ba{JR9hjQd_$`9^*#Ppv%zhMF_ zW{FK%+AQaGT5@v>uTLr0JTSR#!wZzwbhwP0FJ=UHq!GKD8phta&}fF|0`|3F2Hj)D zRj2?_sl(f1x10CGTbR9Qmm%h$s2HA5O{80?!u$LhR99?eNv@awf z2T?Gix6nSvtBJKVr20am{pmm8m&{IA=W6MGb|b{pM%bet;q#1295*xjR|-^fLt=E} zRLkVQpB1m>1i^golowXsnh{bg+Xy+dqv9nAx|4?;CClkO1WWC_nEu{f8UG_!@)L;l z!Y>1K-VDCSM*@*?0@#YD3D@G!o-ct4tt7SU^y83sv7JiWQ;=GC;I@1a#94~7wUT_F9C0 z@{KlEhr=>TmdDD?h49a`&crczD9pH;1Zb8V=q8bj;^y=G4MyF7&^y@^(veRF*DqC; z{=_bL@jgJ>}eaWGz5tkY|S`+yk*3-A<#3He8_`R2GHy#8Djwj@#LK542r zhYjMhpngzfyGyY(Dvu@(7gt0;1BZ(#BH-*xV>$J`d#3sOm;T9G;#UWt<)GcwwxH`c zM>Z)`8}w%c_q9%Dn;xKLQN-;d3Nm=2%p5MRno~QwJLrC8b?=JJF%BgK?gULeqybKh z&(ZAXDxhzqSws&5gZRT9L109w@WNGsSpE*`=NmC}fLIhvp#lKhw?Y}*Hhon`L%*qg zr_~eB9hWZG|Amsw=doBDr74uXm5dN{Gn3F3V`~E_Iwwu^igs;NYQ9C;*ujVh>QPk@ zD?SwvQO}pO&GGu%RAzsX7fZ&40e~N8U`QN}q~NXx_G9L<38^9Xtwc@v6i|hkfkxz~ zZ3h|$^ku^|Ukmm4U4cB6@j_Utv)m;GtfcSv-EVw49gdve#9V^dI8a62OUGy`Tf-H< z%NZ#mJP^#JxVUtK{W1Zf;3M^XpynV~zn~P|yyOUfFDb^)Pq*MfKfYO$#0mK|3k5OZ z02)JCC2xU4yR<+79ee- zgNzadC;1SJ3UKB0a5@FbHSHlt_#wZ5?%BByFm}R1B3%$+>@2H1-dbjRIV0KJ#u<&6 zMQk2Pq@vi}aPXK4kwfPU%KTFtNLR`u_8tz}gsn}5%1wL>H|{+f8AG+`wK#^1+cbt$ zK`Y92qzoR3jZfIecwBlRm!~mBh6Oe}?mLes%U};vU~Ml@fUdI&yC`lKliL!Wo?z3I z5*h-;Vx%2+>@UN`xpAb8_35BPV-sj1w5yO1S8$~Fzne)S)3Zt{$cG^NAve+oc*JIz zSd_5qQE5Wr6AZ&OC?x{DcEfz&iDPvUjNE)|_=*YKnm&1qh7Kiq5Ongx(9(Q=ab6fe zW~-`~r@@Dw#e!%eLb;CO08!Tu7sn_JaUfjnvGwrix4?q7bS!0|ix1rMNzmCd4aW$K zr-w>G%DdL>!}?SE<;5TOr)mXLg-GVnh#4-SrxwGReEvw?{q=1pXI<_P@y{_C9+^cs zxr3>*%^c>rj{=I-EqFN;!-qxlAtx?~n_j~#GVQ?P`$1r)Ab|n@BiemF zY6uK+0g;-vHz5dL|NMb=O&m|5>OGK#w~hMX8rB(Lkv!URxXddl1H7+C5)%+4aNp0x zGk%*9=xLFc4e#kvdZHX7{V~qe%`QFcQupcp9`XX!UF7xtX>;&PtmYj__M**5zKtUT zrvsXx!{X;+Q(WC$WqesZbYtaP!RlR`aIO>aBWfZ`>kKaN>A-%QH4W6uM*=H@B|q09 zBB_HzFw?I{NB1MAdK`)SKEWylqqa7YRs|ytGz8@cJFY8a0o=R37v#+!5iekL`0@Q< zj&cdijxIZViynO6iB^-vC9w!XArxdCA@dd%(g?p5g)-(rvkDr5;>{yPWYA&Nq0{Q2 z|4N+Y-xgK)-mQ{$7UniUG;=_GiL!vD6()V(rXBu!-oEf&fFz3}7n)ZvQP{oC?a#T6 zjR9)5h+X9I%dF^me>Owy#4c}LA3+u5iKK1w!sY^5k|%Q>5c0klyeZ|x{9idy7QZ&g|0d_c{QhOcxBWi-! zZUqXHifU_!1gC^f*-=)1UHEa49VIOrc44H9TN;g5cg}W*Gw5EpPuNSUR0kr9>W?tENEN;$|AU7$_#?|XFrytj1IoU!`R zl1|;k!?DO$K9FtI-NeL*xs9|Nk|jvL(?@dmeZ%V#TaUfn-mtqL2CJx&Y2Dio69Ohk2D-fV|Xsc_=j)c6TtD43898k_V(OK({xXj zXFO@o1E~d%{!!lD8a^0he)AQ}ZnD6^C{&w&zS@&MJN!)1nkp_ojsW$(3H7AnXgwwfs2P;7sO5^4$r`_bJgHfzD|8g}an%soz`L&aE=H`H| z-3L#rS!Y)I@zS)rH5nk{#qI;7)vO^)V`pjma!JM&#l7RBZU|iK zu)-{6c~7Sk?J3oQrrgR`BfqLcjpec2YKsW_O^r1SXFQa3D3tZFGIK$L&*X>kSH+Pu z4y3>pG?=R%wyhvBziL7#@ZoY4x3>(%#IqhVb>+~k@@IL{*!?gl(WaYJ6Brl}moRJs zsC$3@mpIHc1kz-9Jwk3^2%plH8sUFkN(5o> z`T<3r*s%RZqNX`-`@Q?R6%i9@?1eizAljdV3i<|YdtG`AEqoQGU?7AJ))MW-I`O!; zKIRr0@QxXgwro6nFq!R|^THakBXcCdNr&i7L2aw1u~M5&NTy%4=YAWoHlEb2tOBHL?sjOB(2Dg0X^0SNFgYZ)Iph_41s6J5!o!=;5 zW5X|j>8x;}L=o<eRe(bJ5$?U%Auv8Pjy>&@cLHN?&`Y0}Z8wKvF1%njhoOw!lZAFC8TXCXNe|;at zeGid8tl>?0yDZm%`QUY=dRi;rh*fUK{eT-i6e^&z3$y*i(JvfqjReeFNPu(ybY-a zs5OM`%hqo)kU}o^H<}znRLAYK>r>z@6KFas@U=|XRBMzGg9e})r5Si2`f!U?R!Ubg zEKx!95xQ>z=62l_r?cg()=_m0WjEaes3 z#*+eMV<=9_OVR4YR!JP$oV@}K*cOc0+ZvS5(Ww=C#cwgt!zEbXnU-?h0hiJsPl;ls z1)$T)Y4k(sf|o<;01e-w`2#H@p=`ndIH8e_;F`%;FO&oli!>zm=GxF%BA9w(kP|if z6SvZXOFowu*IaZ4XMJ+TUhQNl{h4pP1JZDi7(SGtTb+h!R|A`FbhDl zGci;drbnl>(G#1?*Qz`S=_KAVhUi@4HMxa4n*8cZ(5fv7bA3^Lz z)%Tj6?n_M*A93#;YhS*?zJk7rdX(LywTb$v`U(0e`k-4UIp}7z0Q1q}_*htJbukl_ z)DfjW3#2KgOk{7PPOJLx@o0BEoC@%X$4_H^0i6nZq|}E7p7m9U!Ux~)n3bgD;Zyn# zitm^-Y3b8gl}4wfXv4;*N2w2vVcNAn!w_%2lKce7Il1nCn_cgIR=+#=h4F7JtB|$q zJAAp#GFf84oyix~N13fs%(0$VTFC=X0_qq(L4J`jS>fuL58w17v6WX|={Dfs*sa*5 z_E=azBia&ez%+XH-%yfVyRL+{VeTG}W4>+N3#dtd!(L_Dbsg8`uc{dwjka;rXmM+| zhFpy-Bp0|3o-h1p%e3en?Pj082ldu|3UONDexg r;h2*W2!DG5cP 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 literal 0 HcmV?d00001 diff --git a/websocket/data/__init__.py b/websocket/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/websocket/data/flashsocket.js b/websocket/data/flashsocket.js new file mode 100644 index 0000000..c7dd442 --- /dev/null +++ b/websocket/data/flashsocket.js @@ -0,0 +1,998 @@ +WEB_SOCKET_SWF_LOCATION = '/websocket/WebSocketMain.swf'; +/* SWFObject v2.2 + is released under the MIT License +*/ +var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab 0) { + for (var i = 0; i < ol; i++) { + if (typeof objects[i].SetVariable != "undefined") { + activeObjects[activeObjects.length] = objects[i]; + } + } + } + var embeds = document.getElementsByTagName("embed"); + var el = embeds.length; + var activeEmbeds = []; + if (el > 0) { + for (var j = 0; j < el; j++) { + if (typeof embeds[j].SetVariable != "undefined") { + activeEmbeds[activeEmbeds.length] = embeds[j]; + } + } + } + var aol = activeObjects.length; + var ael = activeEmbeds.length; + var searchStr = "bridgeName="+ bridgeName; + if ((aol == 1 && !ael) || (aol == 1 && ael == 1)) { + FABridge.attachBridge(activeObjects[0], bridgeName); + } + else if (ael == 1 && !aol) { + FABridge.attachBridge(activeEmbeds[0], bridgeName); + } + else { + var flash_found = false; + if (aol > 1) { + for (var k = 0; k < aol; k++) { + var params = activeObjects[k].childNodes; + for (var l = 0; l < params.length; l++) { + var param = params[l]; + if (param.nodeType == 1 && param.tagName.toLowerCase() == "param" && param["name"].toLowerCase() == "flashvars" && param["value"].indexOf(searchStr) >= 0) { + FABridge.attachBridge(activeObjects[k], bridgeName); + flash_found = true; + break; + } + } + if (flash_found) { + break; + } + } + } + if (!flash_found && ael > 1) { + for (var m = 0; m < ael; m++) { + var flashVars = activeEmbeds[m].attributes.getNamedItem("flashVars").nodeValue; + if (flashVars.indexOf(searchStr) >= 0) { + FABridge.attachBridge(activeEmbeds[m], bridgeName); + break; + } + } + } + } + return true; +} + +// used to track multiple bridge instances, since callbacks from AS are global across the page. + +FABridge.nextBridgeID = 0; +FABridge.instances = {}; +FABridge.idMap = {}; +FABridge.refCount = 0; + +FABridge.extractBridgeFromID = function(id) +{ + var bridgeID = (id >> 16); + return FABridge.idMap[bridgeID]; +} + +FABridge.attachBridge = function(instance, bridgeName) +{ + var newBridgeInstance = new FABridge(instance, bridgeName); + + FABridge[bridgeName] = newBridgeInstance; + +/* FABridge[bridgeName] = function() { + return newBridgeInstance.root(); + } +*/ + var callbacks = FABridge.initCallbacks[bridgeName]; + if (callbacks == null) + { + return; + } + for (var i = 0; i < callbacks.length; i++) + { + callbacks[i].call(newBridgeInstance); + } + delete FABridge.initCallbacks[bridgeName] +} + +// some methods can't be proxied. You can use the explicit get,set, and call methods if necessary. + +FABridge.blockedMethods = +{ + toString: true, + get: true, + set: true, + call: true +}; + +FABridge.prototype = +{ + + +// bootstrapping + + root: function() + { + return this.deserialize(this.target.getRoot()); + }, +//clears all of the AS objects in the cache maps + releaseASObjects: function() + { + return this.target.releaseASObjects(); + }, +//clears a specific object in AS from the type maps + releaseNamedASObject: function(value) + { + if(typeof(value) != "object") + { + return false; + } + else + { + var ret = this.target.releaseNamedASObject(value.fb_instance_id); + return ret; + } + }, +//create a new AS Object + create: function(className) + { + return this.deserialize(this.target.create(className)); + }, + + + // utilities + + makeID: function(token) + { + return (this.bridgeID << 16) + token; + }, + + + // low level access to the flash object + +//get a named property from an AS object + getPropertyFromAS: function(objRef, propName) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + retVal = this.target.getPropFromAS(objRef, propName); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, +//set a named property on an AS object + setPropertyInAS: function(objRef,propName, value) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + retVal = this.target.setPropInAS(objRef,propName, this.serialize(value)); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, + +//call an AS function + callASFunction: function(funcID, args) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + retVal = this.target.invokeASFunction(funcID, this.serialize(args)); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, +//call a method on an AS object + callASMethod: function(objID, funcName, args) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + args = this.serialize(args); + retVal = this.target.invokeASMethod(objID, funcName, args); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, + + // responders to remote calls from flash + + //callback from flash that executes a local JS function + //used mostly when setting js functions as callbacks on events + invokeLocalFunction: function(funcID, args) + { + var result; + var func = this.localFunctionCache[funcID]; + + if(func != undefined) + { + result = this.serialize(func.apply(null, this.deserialize(args))); + } + + return result; + }, + + // Object Types and Proxies + + // accepts an object reference, returns a type object matching the obj reference. + getTypeFromName: function(objTypeName) + { + return this.remoteTypeCache[objTypeName]; + }, + //create an AS proxy for the given object ID and type + createProxy: function(objID, typeName) + { + var objType = this.getTypeFromName(typeName); + instanceFactory.prototype = objType; + var instance = new instanceFactory(objID); + this.remoteInstanceCache[objID] = instance; + return instance; + }, + //return the proxy associated with the given object ID + getProxy: function(objID) + { + return this.remoteInstanceCache[objID]; + }, + + // accepts a type structure, returns a constructed type + addTypeDataToCache: function(typeData) + { + var newType = new ASProxy(this, typeData.name); + var accessors = typeData.accessors; + for (var i = 0; i < accessors.length; i++) + { + this.addPropertyToType(newType, accessors[i]); + } + + var methods = typeData.methods; + for (var i = 0; i < methods.length; i++) + { + if (FABridge.blockedMethods[methods[i]] == undefined) + { + this.addMethodToType(newType, methods[i]); + } + } + + + this.remoteTypeCache[newType.typeName] = newType; + return newType; + }, + + //add a property to a typename; used to define the properties that can be called on an AS proxied object + addPropertyToType: function(ty, propName) + { + var c = propName.charAt(0); + var setterName; + var getterName; + if(c >= "a" && c <= "z") + { + getterName = "get" + c.toUpperCase() + propName.substr(1); + setterName = "set" + c.toUpperCase() + propName.substr(1); + } + else + { + getterName = "get" + propName; + setterName = "set" + propName; + } + ty[setterName] = function(val) + { + this.bridge.setPropertyInAS(this.fb_instance_id, propName, val); + } + ty[getterName] = function() + { + return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); + } + }, + + //add a method to a typename; used to define the methods that can be callefd on an AS proxied object + addMethodToType: function(ty, methodName) + { + ty[methodName] = function() + { + return this.bridge.deserialize(this.bridge.callASMethod(this.fb_instance_id, methodName, FABridge.argsToArray(arguments))); + } + }, + + // Function Proxies + + //returns the AS proxy for the specified function ID + getFunctionProxy: function(funcID) + { + var bridge = this; + if (this.remoteFunctionCache[funcID] == null) + { + this.remoteFunctionCache[funcID] = function() + { + bridge.callASFunction(funcID, FABridge.argsToArray(arguments)); + } + } + return this.remoteFunctionCache[funcID]; + }, + + //reutrns the ID of the given function; if it doesnt exist it is created and added to the local cache + getFunctionID: function(func) + { + if (func.__bridge_id__ == undefined) + { + func.__bridge_id__ = this.makeID(this.nextLocalFuncID++); + this.localFunctionCache[func.__bridge_id__] = func; + } + return func.__bridge_id__; + }, + + // serialization / deserialization + + serialize: function(value) + { + var result = {}; + + var t = typeof(value); + //primitives are kept as such + if (t == "number" || t == "string" || t == "boolean" || t == null || t == undefined) + { + result = value; + } + else if (value instanceof Array) + { + //arrays are serializesd recursively + result = []; + for (var i = 0; i < value.length; i++) + { + result[i] = this.serialize(value[i]); + } + } + else if (t == "function") + { + //js functions are assigned an ID and stored in the local cache + result.type = FABridge.TYPE_JSFUNCTION; + result.value = this.getFunctionID(value); + } + else if (value instanceof ASProxy) + { + result.type = FABridge.TYPE_ASINSTANCE; + result.value = value.fb_instance_id; + } + else + { + result.type = FABridge.TYPE_ANONYMOUS; + result.value = value; + } + + return result; + }, + + //on deserialization we always check the return for the specific error code that is used to marshall NPE's into JS errors + // the unpacking is done by returning the value on each pachet for objects/arrays + deserialize: function(packedValue) + { + + var result; + + var t = typeof(packedValue); + if (t == "number" || t == "string" || t == "boolean" || packedValue == null || packedValue == undefined) + { + result = this.handleError(packedValue); + } + else if (packedValue instanceof Array) + { + result = []; + for (var i = 0; i < packedValue.length; i++) + { + result[i] = this.deserialize(packedValue[i]); + } + } + else if (t == "object") + { + for(var i = 0; i < packedValue.newTypes.length; i++) + { + this.addTypeDataToCache(packedValue.newTypes[i]); + } + for (var aRefID in packedValue.newRefs) + { + this.createProxy(aRefID, packedValue.newRefs[aRefID]); + } + if (packedValue.type == FABridge.TYPE_PRIMITIVE) + { + result = packedValue.value; + } + else if (packedValue.type == FABridge.TYPE_ASFUNCTION) + { + result = this.getFunctionProxy(packedValue.value); + } + else if (packedValue.type == FABridge.TYPE_ASINSTANCE) + { + result = this.getProxy(packedValue.value); + } + else if (packedValue.type == FABridge.TYPE_ANONYMOUS) + { + result = packedValue.value; + } + } + return result; + }, + //increases the reference count for the given object + addRef: function(obj) + { + this.target.incRef(obj.fb_instance_id); + }, + //decrease the reference count for the given object and release it if needed + release:function(obj) + { + this.target.releaseRef(obj.fb_instance_id); + }, + + // check the given value for the components of the hard-coded error code : __FLASHERROR + // used to marshall NPE's into flash + + handleError: function(value) + { + if (typeof(value)=="string" && value.indexOf("__FLASHERROR")==0) + { + var myErrorMessage = value.split("||"); + if(FABridge.refCount > 0 ) + { + FABridge.refCount--; + } + throw new Error(myErrorMessage[1]); + return value; + } + else + { + return value; + } + } +}; + +// The root ASProxy class that facades a flash object + +ASProxy = function(bridge, typeName) +{ + this.bridge = bridge; + this.typeName = typeName; + return this; +}; +//methods available on each ASProxy object +ASProxy.prototype = +{ + get: function(propName) + { + return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); + }, + + set: function(propName, value) + { + this.bridge.setPropertyInAS(this.fb_instance_id, propName, value); + }, + + call: function(funcName, args) + { + this.bridge.callASMethod(this.fb_instance_id, funcName, args); + }, + + addRef: function() { + this.bridge.addRef(this); + }, + + release: function() { + this.bridge.release(this); + } +}; + +// Copyright: Hiroshi Ichikawa +// License: New BSD License +// Reference: http://dev.w3.org/html5/websockets/ +// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol + +(function() { + + if (window.WebSocket) return; + + var console = window.console; + if (!console) console = {log: function(){ }, error: function(){ }}; + + if (!swfobject.hasFlashPlayerVersion("9.0.0")) { + console.error("Flash Player is not installed."); + return; + } + if (location.protocol == "file:") { + console.error( + "WARNING: web-socket-js doesn't work in file:///... URL " + + "unless you set Flash Security Settings properly. " + + "Open the page via Web server i.e. http://..."); + } + + WebSocket = function(url, protocol, proxyHost, proxyPort, headers) { + var self = this; + self.readyState = WebSocket.CONNECTING; + self.bufferedAmount = 0; + // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. + // Otherwise, when onopen fires immediately, onopen is called before it is set. + setTimeout(function() { + WebSocket.__addTask(function() { + self.__createFlash(url, protocol, proxyHost, proxyPort, headers); + }); + }, 1); + } + + WebSocket.prototype.__createFlash = function(url, protocol, proxyHost, proxyPort, headers) { + var self = this; + self.__flash = + WebSocket.__flash.create(url, protocol, proxyHost || null, proxyPort || 0, headers || null); + + self.__flash.addEventListener("open", function(fe) { + try { + self.readyState = self.__flash.getReadyState(); + if (self.__timer) clearInterval(self.__timer); + if (window.opera) { + // Workaround for weird behavior of Opera which sometimes drops events. + self.__timer = setInterval(function () { + self.__handleMessages(); + }, 500); + } + if (self.onopen) self.onopen(); + } catch (e) { + console.error(e.toString()); + } + }); + + self.__flash.addEventListener("close", function(fe) { + try { + self.readyState = self.__flash.getReadyState(); + if (self.__timer) clearInterval(self.__timer); + if (self.onclose) self.onclose(); + } catch (e) { + console.error(e.toString()); + } + }); + + self.__flash.addEventListener("message", function() { + try { + self.__handleMessages(); + } catch (e) { + console.error(e.toString()); + } + }); + + self.__flash.addEventListener("error", function(fe) { + try { + if (self.__timer) clearInterval(self.__timer); + if (self.onerror) self.onerror(); + } catch (e) { + console.error(e.toString()); + } + }); + + self.__flash.addEventListener("stateChange", function(fe) { + try { + self.readyState = self.__flash.getReadyState(); + self.bufferedAmount = fe.getBufferedAmount(); + } catch (e) { + console.error(e.toString()); + } + }); + + //console.log("[WebSocket] Flash object is ready"); + }; + + WebSocket.prototype.send = function(data) { + if (this.__flash) { + this.readyState = this.__flash.getReadyState(); + } + if (!this.__flash || this.readyState == WebSocket.CONNECTING) { + throw "INVALID_STATE_ERR: Web Socket connection has not been established"; + } + // We use encodeURIComponent() here, because FABridge doesn't work if + // the argument includes some characters. We don't use escape() here + // because of this: + // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions + // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't + // preserve all Unicode characters either e.g. "\uffff" in Firefox. + var result = this.__flash.send(encodeURIComponent(data)); + if (result < 0) { // success + return true; + } else { + this.bufferedAmount = result; + return false; + } + }; + + WebSocket.prototype.close = function() { + var self = this; + if (!self.__flash) return; + self.readyState = self.__flash.getReadyState(); + if (self.readyState == WebSocket.CLOSED || self.readyState == WebSocket.CLOSING) return; + self.__flash.close(); + // Sets/calls them manually here because Flash WebSocketConnection.close cannot fire events + // which causes weird error: + // > You are trying to call recursively into the Flash Player which is not allowed. + self.readyState = WebSocket.CLOSED; + if (self.__timer) clearInterval(self.__timer); + if (self.onclose) { + // Make it asynchronous so that it looks more like an actual + // close event + setTimeout(self.onclose, 1); + } + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture !NB Not implemented yet + * @return void + */ + WebSocket.prototype.addEventListener = function(type, listener, useCapture) { + if (!('__events' in this)) { + this.__events = {}; + } + if (!(type in this.__events)) { + this.__events[type] = []; + if ('function' == typeof this['on' + type]) { + this.__events[type].defaultHandler = this['on' + type]; + this['on' + type] = this.__createEventHandler(this, type); + } + } + this.__events[type].push(listener); + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture NB! Not implemented yet + * @return void + */ + WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { + if (!('__events' in this)) { + this.__events = {}; + } + if (!(type in this.__events)) return; + for (var i = this.__events.length; i > -1; --i) { + if (listener === this.__events[type][i]) { + this.__events[type].splice(i, 1); + break; + } + } + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {WebSocketEvent} event + * @return void + */ + WebSocket.prototype.dispatchEvent = function(event) { + if (!('__events' in this)) throw 'UNSPECIFIED_EVENT_TYPE_ERR'; + if (!(event.type in this.__events)) throw 'UNSPECIFIED_EVENT_TYPE_ERR'; + + for (var i = 0, l = this.__events[event.type].length; i < l; ++ i) { + this.__events[event.type][i](event); + if (event.cancelBubble) break; + } + + if (false !== event.returnValue && + 'function' == typeof this.__events[event.type].defaultHandler) + { + this.__events[event.type].defaultHandler(event); + } + }; + + WebSocket.prototype.__handleMessages = function() { + // Gets data using readSocketData() instead of getting it from event object + // of Flash event. This is to make sure to keep message order. + // It seems sometimes Flash events don't arrive in the same order as they are sent. + var arr = this.__flash.readSocketData(); + for (var i = 0; i < arr.length; i++) { + var data = decodeURIComponent(arr[i]); + try { + if (this.onmessage) { + var e; + if (window.MessageEvent && !window.opera) { + e = document.createEvent("MessageEvent"); + e.initMessageEvent("message", false, false, data, null, null, window, null); + } else { // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes + e = {data: data}; + } + this.onmessage(e); + } + } catch (e) { + console.error(e.toString()); + } + } + }; + + /** + * @param {object} object + * @param {string} type + */ + WebSocket.prototype.__createEventHandler = function(object, type) { + return function(data) { + var event = new WebSocketEvent(); + event.initEvent(type, true, true); + event.target = event.currentTarget = object; + for (var key in data) { + event[key] = data[key]; + } + object.dispatchEvent(event, arguments); + }; + } + + /** + * Basic implementation of {@link DOM 2 EventInterface} + * + * @class + * @constructor + */ + function WebSocketEvent(){} + + /** + * + * @type boolean + */ + WebSocketEvent.prototype.cancelable = true; + + /** + * + * @type boolean + */ + WebSocketEvent.prototype.cancelBubble = false; + + /** + * + * @return void + */ + WebSocketEvent.prototype.preventDefault = function() { + if (this.cancelable) { + this.returnValue = false; + } + }; + + /** + * + * @return void + */ + WebSocketEvent.prototype.stopPropagation = function() { + this.cancelBubble = true; + }; + + /** + * + * @param {string} eventTypeArg + * @param {boolean} canBubbleArg + * @param {boolean} cancelableArg + * @return void + */ + WebSocketEvent.prototype.initEvent = function(eventTypeArg, canBubbleArg, cancelableArg) { + this.type = eventTypeArg; + this.cancelable = cancelableArg; + this.timeStamp = new Date(); + }; + + + WebSocket.CONNECTING = 0; + WebSocket.OPEN = 1; + WebSocket.CLOSING = 2; + WebSocket.CLOSED = 3; + + WebSocket.__tasks = []; + + WebSocket.__initialize = function() { + if (WebSocket.__swfLocation) { + // For backword compatibility. + window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; + } + if (!window.WEB_SOCKET_SWF_LOCATION) { + console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); + return; + } + var container = document.createElement("div"); + container.id = "webSocketContainer"; + // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents + // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). + // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash + // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is + // the best we can do as far as we know now. + container.style.position = "absolute"; + if (WebSocket.__isFlashLite()) { + container.style.left = "0px"; + container.style.top = "0px"; + } else { + container.style.left = "-100px"; + container.style.top = "-100px"; + } + var holder = document.createElement("div"); + holder.id = "webSocketFlash"; + container.appendChild(holder); + document.body.appendChild(container); + // See this article for hasPriority: + // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html + swfobject.embedSWF( + WEB_SOCKET_SWF_LOCATION, "webSocketFlash", + "1" /* width */, "1" /* height */, "9.0.0" /* SWF version */, + null, {bridgeName: "webSocket"}, {hasPriority: true, allowScriptAccess: "always"}, null, + function(e) { + if (!e.success) console.error("[WebSocket] swfobject.embedSWF failed"); + } + ); + FABridge.addInitializationCallback("webSocket", function() { + try { + //console.log("[WebSocket] FABridge initializad"); + WebSocket.__flash = FABridge.webSocket.root(); + WebSocket.__flash.setCallerUrl(location.href); + WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); + for (var i = 0; i < WebSocket.__tasks.length; ++i) { + WebSocket.__tasks[i](); + } + WebSocket.__tasks = []; + } catch (e) { + console.error("[WebSocket] " + e.toString()); + } + }); + }; + + WebSocket.__addTask = function(task) { + if (WebSocket.__flash) { + task(); + } else { + WebSocket.__tasks.push(task); + } + }; + + WebSocket.__isFlashLite = function() { + if (!window.navigator || !window.navigator.mimeTypes) return false; + var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; + if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) return false; + return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; + }; + + // called from Flash + window.webSocketLog = function(message) { + console.log(decodeURIComponent(message)); + }; + + // called from Flash + window.webSocketError = function(message) { + console.error(decodeURIComponent(message)); + }; + + if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { + if (window.addEventListener) { + window.addEventListener("load", WebSocket.__initialize, false); + } else { + window.attachEvent("onload", WebSocket.__initialize); + } + } + +})(); diff --git a/websocket/policyserver.py b/websocket/policyserver.py new file mode 100644 index 0000000..6da9e0b --- /dev/null +++ b/websocket/policyserver.py @@ -0,0 +1,22 @@ +from gevent.server import StreamServer + +__all__ = ['FlashPolicyServer'] + + +class FlashPolicyServer(StreamServer): + policy = """ +""" + + noisy = False + + def __init__(self, listener=None, backlog=None, noisy=None): + if listener is None: + listener = ('0.0.0.0', 843) + if noisy is not None: + self.noisy = noisy + StreamServer.__init__(self, listener=listener, backlog=backlog) + + def handle(self, socket, address): + if self.noisy: + print 'Accepted connection from %s:%s' % address + socket.sendall(self.policy) diff --git a/websocket/server.py b/websocket/server.py new file mode 100644 index 0000000..1c42249 --- /dev/null +++ b/websocket/server.py @@ -0,0 +1,134 @@ +import sys +import traceback +from os.path import abspath, dirname, join, basename +from socket import error +from hashlib import md5 +from datetime import datetime +from gevent.pywsgi import WSGIHandler, WSGIServer + +from websocket.policyserver import FlashPolicyServer +from websocket import WebSocket + +import gevent +assert gevent.version_info >= (0, 13, 2), 'Newer version of gevent is required to run websocket.server' + +__all__ = ['WebsocketHandler', 'WebsocketServer'] + + +class WebsocketHandler(WSGIHandler): + + def run_application(self): + path = self.environ.get('PATH_INFO') + content_type = self.server.data_handlers.get(path) + if content_type is not None: + self.serve_file(basename(path), content_type) + return + + websocket_mode = False + + if WebSocket.is_socket(self.environ): + self.status = 'websocket' + self.log_request() + self.environ['websocket'] = WebSocket(self.environ, self.socket, self.rfile) + websocket_mode = True + try: + self.result = self.application(self.environ, self.start_response) + if self.result is not None: + self.process_result() + except: + websocket = self.environ.get('websocket') + if websocket is not None: + websocket.close() + raise + finally: + if websocket_mode: + # we own the socket now, make sure pywsgi does not try to read from it: + self.socket = None + + def serve_file(self, filename, content_type): + from websocket import data + path = join(dirname(abspath(data.__file__)), filename) + if self.server.etags.get(path) == (self.environ.get('HTTP_IF_NONE_MATCH') or 'x'): + self.start_response('304 Not Modifed', []) + self.write('') + return + try: + body = open(path).read() + except IOError, ex: + sys.stderr.write('Cannot open %s: %s\n' % (path, ex)) + self.start_response('404 Not Found', []) + self.write('') + return + etag = md5(body).hexdigest() + self.server.etags[path] = etag + self.start_response('200 OK', [('Content-Type', content_type), + ('Content-Length', str(len(body))), + ('Etag', etag)]) + self.write(body) + + +class WebsocketServer(WSGIServer): + + handler_class = WebsocketHandler + data_handlers = { + '/websocket/WebSocketMain.swf': 'application/x-shockwave-flash', + '/websocket/flashsocket.js': 'text/javascript' + } + etags = {} + + def __init__(self, listener, application=None, policy_server=True, backlog=None, + spawn='default', log='default', handler_class=None, environ=None, **ssl_args): + if policy_server is True: + self.policy_server = FlashPolicyServer() + elif isinstance(policy_server, tuple): + self.policy_server = FlashPolicyServer(policy_server) + elif policy_server: + raise TypeError('Expected tuple or boolean: %r' % (policy_server, )) + else: + self.policy_server = None + super(WebsocketServer, self).__init__(listener, application, backlog=backlog, spawn=spawn, log=log, + handler_class=handler_class, environ=environ, **ssl_args) + + def start_accepting(self): + self._start_policy_server() + super(WebsocketServer, self).start_accepting() + self.log_message('%s accepting connections on %s', self.__class__.__name__, _format_address(self)) + + def _start_policy_server(self): + server = self.policy_server + if server is not None: + try: + server.start() + self.log_message('%s accepting connections on %s', server.__class__.__name__, _format_address(server)) + except error, ex: + sys.stderr.write('FAILED to start %s on %s: %s\n' % (server.__class__.__name__, _format_address(server), ex)) + except Exception: + traceback.print_exc() + sys.stderr.write('FAILED to start %s on %s\n' % (server.__class__.__name__, _format_address(server))) + + def kill(self): + if self.policy_server is not None: + self.policy_server.kill() + super(WebsocketServer, self).kill() + + def log_message(self, message, *args): + log = self.log + if log is not None: + try: + message = message % args + except Exception: + traceback.print_exc() + try: + message = '%r %r' % (message, args) + except Exception: + traceback.print_exc() + log.write('%s %s\n' % (datetime.now().replace(microsecond=0), message)) + + +def _format_address(server): + try: + if server.server_host == '0.0.0.0': + return ':%s' % server.server_port + return '%s:%s' % (server.server_host, server.server_port) + except Exception: + traceback.print_exc() diff --git a/websocket/tests/__init__.py b/websocket/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/websocket/tests/data/header01.txt b/websocket/tests/data/header01.txt new file mode 100644 index 0000000..d44d24c --- /dev/null +++ b/websocket/tests/data/header01.txt @@ -0,0 +1,6 @@ +HTTP/1.1 101 WebSocket Protocol Handshake +Connection: Upgrade +Upgrade: WebSocket +Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0= +some_header: something + diff --git a/websocket/tests/data/header02.txt b/websocket/tests/data/header02.txt new file mode 100644 index 0000000..f481de9 --- /dev/null +++ b/websocket/tests/data/header02.txt @@ -0,0 +1,6 @@ +HTTP/1.1 101 WebSocket Protocol Handshake +Connection: Upgrade +Upgrade WebSocket +Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0= +some_header: something + diff --git a/websocket/tests/data/header03.txt b/websocket/tests/data/header03.txt new file mode 100644 index 0000000..1a81dc7 --- /dev/null +++ b/websocket/tests/data/header03.txt @@ -0,0 +1,8 @@ +HTTP/1.1 101 WebSocket Protocol Handshake +Connection: Upgrade, Keep-Alive +Upgrade: WebSocket +Sec-WebSocket-Accept: Kxep+hNu9n51529fGidYu7a3wO0= +Set-Cookie: Token=ABCDE +Set-Cookie: Token=FGHIJ +some_header: something + diff --git a/websocket/tests/echo-server.py b/websocket/tests/echo-server.py new file mode 100644 index 0000000..08d108a --- /dev/null +++ b/websocket/tests/echo-server.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +# From https://github.com/aaugustin/websockets/blob/main/example/echo.py + +import asyncio +import websockets +import os + +LOCAL_WS_SERVER_PORT = os.environ.get('LOCAL_WS_SERVER_PORT', '8765') + + +async def echo(websocket, path): + async for message in websocket: + await websocket.send(message) + + +async def main(): + async with websockets.serve(echo, "localhost", LOCAL_WS_SERVER_PORT): + await asyncio.Future() # run forever + +asyncio.run(main()) diff --git a/websocket/tests/test_abnf.py b/websocket/tests/test_abnf.py new file mode 100644 index 0000000..7c9d89d --- /dev/null +++ b/websocket/tests/test_abnf.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# +import websocket as ws +from websocket._abnf import * +import unittest + +""" +test_abnf.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + + +class ABNFTest(unittest.TestCase): + + def testInit(self): + a = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING) + self.assertEqual(a.fin, 0) + self.assertEqual(a.rsv1, 0) + self.assertEqual(a.rsv2, 0) + self.assertEqual(a.rsv3, 0) + self.assertEqual(a.opcode, 9) + self.assertEqual(a.data, '') + a_bad = ABNF(0,1,0,0, opcode=77) + self.assertEqual(a_bad.rsv1, 1) + self.assertEqual(a_bad.opcode, 77) + + def testValidate(self): + a_invalid_ping = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING) + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_invalid_ping.validate, skip_utf8_validation=False) + a_bad_rsv_value = ABNF(0,1,0,0, opcode=ABNF.OPCODE_TEXT) + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_bad_rsv_value.validate, skip_utf8_validation=False) + a_bad_opcode = ABNF(0,0,0,0, opcode=77) + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_bad_opcode.validate, skip_utf8_validation=False) + a_bad_close_frame = ABNF(0,0,0,0, opcode=ABNF.OPCODE_CLOSE, data=b'\x01') + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_bad_close_frame.validate, skip_utf8_validation=False) + a_bad_close_frame_2 = ABNF(0,0,0,0, opcode=ABNF.OPCODE_CLOSE, data=b'\x01\x8a\xaa\xff\xdd') + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_bad_close_frame_2.validate, skip_utf8_validation=False) + a_bad_close_frame_3 = ABNF(0,0,0,0, opcode=ABNF.OPCODE_CLOSE, data=b'\x03\xe7') + self.assertRaises(ws._exceptions.WebSocketProtocolException, a_bad_close_frame_3.validate, skip_utf8_validation=True) + + def testMask(self): + abnf_none_data = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING, mask=1, data=None) + bytes_val = b"aaaa" + self.assertEqual(abnf_none_data._get_masked(bytes_val), bytes_val) + abnf_str_data = ABNF(0,0,0,0, opcode=ABNF.OPCODE_PING, mask=1, data="a") + self.assertEqual(abnf_str_data._get_masked(bytes_val), b'aaaa\x00') + + def testFormat(self): + abnf_bad_rsv_bits = ABNF(2,0,0,0, opcode=ABNF.OPCODE_TEXT) + self.assertRaises(ValueError, abnf_bad_rsv_bits.format) + abnf_bad_opcode = ABNF(0,0,0,0, opcode=5) + self.assertRaises(ValueError, abnf_bad_opcode.format) + abnf_length_10 = ABNF(0,0,0,0, opcode=ABNF.OPCODE_TEXT, data="abcdefghij") + self.assertEqual(b'\x01', abnf_length_10.format()[0].to_bytes(1, 'big')) + self.assertEqual(b'\x8a', abnf_length_10.format()[1].to_bytes(1, 'big')) + self.assertEqual("fin=0 opcode=1 data=abcdefghij", abnf_length_10.__str__()) + abnf_length_20 = ABNF(0,0,0,0, opcode=ABNF.OPCODE_BINARY, data="abcdefghijabcdefghij") + self.assertEqual(b'\x02', abnf_length_20.format()[0].to_bytes(1, 'big')) + self.assertEqual(b'\x94', abnf_length_20.format()[1].to_bytes(1, 'big')) + abnf_no_mask = ABNF(0,0,0,0, opcode=ABNF.OPCODE_TEXT, mask=0, data=b'\x01\x8a\xcc') + self.assertEqual(b'\x01\x03\x01\x8a\xcc', abnf_no_mask.format()) + + def testFrameBuffer(self): + fb = frame_buffer(0, True) + self.assertEqual(fb.recv, 0) + self.assertEqual(fb.skip_utf8_validation, True) + fb.clear + self.assertEqual(fb.header, None) + self.assertEqual(fb.length, None) + self.assertEqual(fb.mask, None) + self.assertEqual(fb.has_mask(), False) + + +if __name__ == "__main__": + unittest.main() diff --git a/websocket/tests/test_app.py b/websocket/tests/test_app.py new file mode 100644 index 0000000..ac2a7dd --- /dev/null +++ b/websocket/tests/test_app.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +# +import os +import os.path +import threading +import websocket as ws +import ssl +import unittest + +""" +test_app.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + +# Skip test to access the internet unless TEST_WITH_INTERNET == 1 +TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1' +# Skip tests relying on local websockets server unless LOCAL_WS_SERVER_PORT != -1 +LOCAL_WS_SERVER_PORT = os.environ.get('LOCAL_WS_SERVER_PORT', '-1') +TEST_WITH_LOCAL_SERVER = LOCAL_WS_SERVER_PORT != '-1' +TRACEABLE = True + + +class WebSocketAppTest(unittest.TestCase): + + class NotSetYet: + """ A marker class for signalling that a value hasn't been set yet. + """ + + def setUp(self): + ws.enableTrace(TRACEABLE) + + WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet() + WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet() + WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet() + WebSocketAppTest.on_error_data = WebSocketAppTest.NotSetYet() + + def tearDown(self): + WebSocketAppTest.keep_running_open = WebSocketAppTest.NotSetYet() + WebSocketAppTest.keep_running_close = WebSocketAppTest.NotSetYet() + WebSocketAppTest.get_mask_key_id = WebSocketAppTest.NotSetYet() + WebSocketAppTest.on_error_data = WebSocketAppTest.NotSetYet() + + @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled") + def testKeepRunning(self): + """ A WebSocketApp should keep running as long as its self.keep_running + is not False (in the boolean context). + """ + + def on_open(self, *args, **kwargs): + """ Set the keep_running flag for later inspection and immediately + close the connection. + """ + self.send("hello!") + WebSocketAppTest.keep_running_open = self.keep_running + self.keep_running = False + + def on_message(wsapp, message): + print(message) + self.close() + + def on_close(self, *args, **kwargs): + """ Set the keep_running flag for the test to use. + """ + WebSocketAppTest.keep_running_close = self.keep_running + + app = ws.WebSocketApp('ws://127.0.0.1:' + LOCAL_WS_SERVER_PORT, on_open=on_open, on_close=on_close, on_message=on_message) + app.run_forever() + + @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled") + def testRunForeverDispatcher(self): + """ A WebSocketApp should keep running as long as its self.keep_running + is not False (in the boolean context). + """ + + def on_open(self, *args, **kwargs): + """ Send a message, receive, and send one more + """ + self.send("hello!") + self.recv() + self.send("goodbye!") + + def on_message(wsapp, message): + print(message) + self.close() + + app = ws.WebSocketApp('ws://127.0.0.1:' + LOCAL_WS_SERVER_PORT, on_open=on_open, on_message=on_message) + app.run_forever(dispatcher="Dispatcher") + + @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled") + def testRunForeverTeardownCleanExit(self): + """ The WebSocketApp.run_forever() method should return `False` when the application ends gracefully. + """ + app = ws.WebSocketApp('ws://127.0.0.1:' + LOCAL_WS_SERVER_PORT) + threading.Timer(interval=0.2, function=app.close).start() + teardown = app.run_forever() + self.assertEqual(teardown, False) + + @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled") + def testRunForeverTeardownExceptionalExit(self): + """ The WebSocketApp.run_forever() method should return `True` when the application ends with an exception. + It should also invoke the `on_error` callback before exiting. + """ + + def break_it(): + # Deliberately break the WebSocketApp by closing the inner socket. + app.sock.close() + + def on_error(_, err): + WebSocketAppTest.on_error_data = str(err) + + app = ws.WebSocketApp('ws://127.0.0.1:' + LOCAL_WS_SERVER_PORT, on_error=on_error) + threading.Timer(interval=0.2, function=break_it).start() + teardown = app.run_forever(ping_timeout=0.1) + self.assertEqual(teardown, True) + self.assertTrue(len(WebSocketAppTest.on_error_data) > 0) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testSockMaskKey(self): + """ A WebSocketApp should forward the received mask_key function down + to the actual socket. + """ + + def my_mask_key_func(): + return "\x00\x00\x00\x00" + + app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1', get_mask_key=my_mask_key_func) + + # if numpy is installed, this assertion fail + # Note: We can't use 'is' for comparing the functions directly, need to use 'id'. + self.assertEqual(id(app.get_mask_key), id(my_mask_key_func)) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testInvalidPingIntervalPingTimeout(self): + """ Test exception handling if ping_interval < ping_timeout + """ + + def on_ping(app, msg): + print("Got a ping!") + app.close() + + def on_pong(app, msg): + print("Got a pong! No need to respond") + app.close() + + app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1', on_ping=on_ping, on_pong=on_pong) + self.assertRaises(ws.WebSocketException, app.run_forever, ping_interval=1, ping_timeout=2, sslopt={"cert_reqs": ssl.CERT_NONE}) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testPingInterval(self): + """ Test WebSocketApp proper ping functionality + """ + + def on_ping(app, msg): + print("Got a ping!") + app.close() + + def on_pong(app, msg): + print("Got a pong! No need to respond") + app.close() + + app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1', on_ping=on_ping, on_pong=on_pong) + app.run_forever(ping_interval=2, ping_timeout=1, sslopt={"cert_reqs": ssl.CERT_NONE}) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testOpcodeClose(self): + """ Test WebSocketApp close opcode + """ + + app = ws.WebSocketApp('wss://tsock.us1.twilio.com/v3/wsconnect') + app.run_forever(ping_interval=2, ping_timeout=1, ping_payload="Ping payload") + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testOpcodeBinary(self): + """ Test WebSocketApp binary opcode + """ + # The lack of wss:// in the URL below is on purpose + app = ws.WebSocketApp('streaming.vn.teslamotors.com/streaming/') + app.run_forever(ping_interval=2, ping_timeout=1, ping_payload="Ping payload") + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testBadPingInterval(self): + """ A WebSocketApp handling of negative ping_interval + """ + app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1') + self.assertRaises(ws.WebSocketException, app.run_forever, ping_interval=-5, sslopt={"cert_reqs": ssl.CERT_NONE}) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testBadPingTimeout(self): + """ A WebSocketApp handling of negative ping_timeout + """ + app = ws.WebSocketApp('wss://api-pub.bitfinex.com/ws/1') + self.assertRaises(ws.WebSocketException, app.run_forever, ping_timeout=-3, sslopt={"cert_reqs": ssl.CERT_NONE}) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testCloseStatusCode(self): + """ Test extraction of close frame status code and close reason in WebSocketApp + """ + def on_close(wsapp, close_status_code, close_msg): + print("on_close reached") + + app = ws.WebSocketApp('wss://tsock.us1.twilio.com/v3/wsconnect', on_close=on_close) + closeframe = ws.ABNF(opcode=ws.ABNF.OPCODE_CLOSE, data=b'\x03\xe8no-init-from-client') + self.assertEqual([1000, 'no-init-from-client'], app._get_close_args(closeframe)) + + closeframe = ws.ABNF(opcode=ws.ABNF.OPCODE_CLOSE, data=b'') + self.assertEqual([None, None], app._get_close_args(closeframe)) + + app2 = ws.WebSocketApp('wss://tsock.us1.twilio.com/v3/wsconnect') + closeframe = ws.ABNF(opcode=ws.ABNF.OPCODE_CLOSE, data=b'') + self.assertEqual([None, None], app2._get_close_args(closeframe)) + + self.assertRaises(ws.WebSocketConnectionClosedException, app.send, data="test if connection is closed") + + +if __name__ == "__main__": + unittest.main() diff --git a/websocket/tests/test_cookiejar.py b/websocket/tests/test_cookiejar.py new file mode 100644 index 0000000..559b2e0 --- /dev/null +++ b/websocket/tests/test_cookiejar.py @@ -0,0 +1,116 @@ +import unittest +from websocket._cookiejar import SimpleCookieJar + +""" +test_cookiejar.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + + +class CookieJarTest(unittest.TestCase): + def testAdd(self): + cookie_jar = SimpleCookieJar() + cookie_jar.add("") + self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar") + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b") + self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar") + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; domain=.abc") + self.assertTrue(".abc" in cookie_jar.jar) + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; domain=abc") + self.assertTrue(".abc" in cookie_jar.jar) + self.assertTrue("abc" not in cookie_jar.jar) + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; c=d; domain=abc") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d") + self.assertEqual(cookie_jar.get(None), "") + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; c=d; domain=abc") + cookie_jar.add("e=f; domain=abc") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d; e=f") + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; c=d; domain=abc") + cookie_jar.add("e=f; domain=.abc") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d; e=f") + + cookie_jar = SimpleCookieJar() + cookie_jar.add("a=b; c=d; domain=abc") + cookie_jar.add("e=f; domain=xyz") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d") + self.assertEqual(cookie_jar.get("xyz"), "e=f") + self.assertEqual(cookie_jar.get("something"), "") + + def testSet(self): + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b") + self.assertFalse(cookie_jar.jar, "Cookie with no domain should not be added to the jar") + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; domain=.abc") + self.assertTrue(".abc" in cookie_jar.jar) + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; domain=abc") + self.assertTrue(".abc" in cookie_jar.jar) + self.assertTrue("abc" not in cookie_jar.jar) + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; c=d; domain=abc") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d") + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; c=d; domain=abc") + cookie_jar.set("e=f; domain=abc") + self.assertEqual(cookie_jar.get("abc"), "e=f") + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; c=d; domain=abc") + cookie_jar.set("e=f; domain=.abc") + self.assertEqual(cookie_jar.get("abc"), "e=f") + + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; c=d; domain=abc") + cookie_jar.set("e=f; domain=xyz") + self.assertEqual(cookie_jar.get("abc"), "a=b; c=d") + self.assertEqual(cookie_jar.get("xyz"), "e=f") + self.assertEqual(cookie_jar.get("something"), "") + + def testGet(self): + cookie_jar = SimpleCookieJar() + cookie_jar.set("a=b; c=d; domain=abc.com") + self.assertEqual(cookie_jar.get("abc.com"), "a=b; c=d") + self.assertEqual(cookie_jar.get("x.abc.com"), "a=b; c=d") + self.assertEqual(cookie_jar.get("abc.com.es"), "") + self.assertEqual(cookie_jar.get("xabc.com"), "") + + cookie_jar.set("a=b; c=d; domain=.abc.com") + self.assertEqual(cookie_jar.get("abc.com"), "a=b; c=d") + self.assertEqual(cookie_jar.get("x.abc.com"), "a=b; c=d") + self.assertEqual(cookie_jar.get("abc.com.es"), "") + self.assertEqual(cookie_jar.get("xabc.com"), "") + + +if __name__ == "__main__": + unittest.main() diff --git a/websocket/tests/test_http.py b/websocket/tests/test_http.py new file mode 100644 index 0000000..649e0fe --- /dev/null +++ b/websocket/tests/test_http.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# +import os +import os.path +import websocket as ws +from websocket._http import proxy_info, read_headers, _start_proxied_socket, _tunnel, _get_addrinfo_list, connect +import unittest +import ssl +import websocket +import socket + +""" +test_http.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + +try: + from python_socks._errors import ProxyError, ProxyTimeoutError, ProxyConnectionError +except: + from websocket._http import ProxyError, ProxyTimeoutError, ProxyConnectionError + +# Skip test to access the internet unless TEST_WITH_INTERNET == 1 +TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1' +TEST_WITH_PROXY = os.environ.get('TEST_WITH_PROXY', '0') == '1' +# Skip tests relying on local websockets server unless LOCAL_WS_SERVER_PORT != -1 +LOCAL_WS_SERVER_PORT = os.environ.get('LOCAL_WS_SERVER_PORT', '-1') +TEST_WITH_LOCAL_SERVER = LOCAL_WS_SERVER_PORT != '-1' + + +class SockMock: + def __init__(self): + self.data = [] + self.sent = [] + + def add_packet(self, data): + self.data.append(data) + + def gettimeout(self): + return None + + def recv(self, bufsize): + if self.data: + e = self.data.pop(0) + if isinstance(e, Exception): + raise e + if len(e) > bufsize: + self.data.insert(0, e[bufsize:]) + return e[:bufsize] + + def send(self, data): + self.sent.append(data) + return len(data) + + def close(self): + pass + + +class HeaderSockMock(SockMock): + + def __init__(self, fname): + SockMock.__init__(self) + path = os.path.join(os.path.dirname(__file__), fname) + with open(path, "rb") as f: + self.add_packet(f.read()) + + +class OptsList(): + + def __init__(self): + self.timeout = 1 + self.sockopt = [] + self.sslopt = {"cert_reqs": ssl.CERT_NONE} + + +class HttpTest(unittest.TestCase): + + def testReadHeader(self): + status, header, status_message = read_headers(HeaderSockMock("data/header01.txt")) + self.assertEqual(status, 101) + self.assertEqual(header["connection"], "Upgrade") + # header02.txt is intentionally malformed + self.assertRaises(ws.WebSocketException, read_headers, HeaderSockMock("data/header02.txt")) + + def testTunnel(self): + self.assertRaises(ws.WebSocketProxyException, _tunnel, HeaderSockMock("data/header01.txt"), "example.com", 80, ("username", "password")) + self.assertRaises(ws.WebSocketProxyException, _tunnel, HeaderSockMock("data/header02.txt"), "example.com", 80, ("username", "password")) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testConnect(self): + # Not currently testing an actual proxy connection, so just check whether proxy errors are raised. This requires internet for a DNS lookup + if ws._http.HAVE_PYTHON_SOCKS: + # Need this check, otherwise case where python_socks is not installed triggers + # websocket._exceptions.WebSocketException: Python Socks is needed for SOCKS proxying but is not available + self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks4", timeout=1)) + self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks4a", timeout=1)) + self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks5", timeout=1)) + self.assertRaises((ProxyTimeoutError, OSError), _start_proxied_socket, "wss://example.com", OptsList(), proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="socks5h", timeout=1)) + self.assertRaises(ProxyConnectionError, connect, "wss://example.com", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port=9999, proxy_type="socks4", timeout=1), None) + + self.assertRaises(TypeError, _get_addrinfo_list, None, 80, True, proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="9999", proxy_type="http")) + self.assertRaises(TypeError, _get_addrinfo_list, None, 80, True, proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="9999", proxy_type="http")) + self.assertRaises(socket.timeout, connect, "wss://google.com", OptsList(), proxy_info(http_proxy_host="8.8.8.8", http_proxy_port=9999, proxy_type="http", timeout=1), None) + self.assertEqual( + connect("wss://google.com", OptsList(), proxy_info(http_proxy_host="8.8.8.8", http_proxy_port=8080, proxy_type="http"), True), + (True, ("google.com", 443, "/"))) + # The following test fails on Mac OS with a gaierror, not an OverflowError + # self.assertRaises(OverflowError, connect, "wss://example.com", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port=99999, proxy_type="socks4", timeout=2), False) + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + @unittest.skipUnless(TEST_WITH_PROXY, "This test requires a HTTP proxy to be running on port 8899") + @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled") + def testProxyConnect(self): + ws = websocket.WebSocket() + ws.connect("ws://127.0.0.1:" + LOCAL_WS_SERVER_PORT, http_proxy_host="127.0.0.1", http_proxy_port="8899", proxy_type="http") + ws.send("Hello, Server") + server_response = ws.recv() + self.assertEqual(server_response, "Hello, Server") + # self.assertEqual(_start_proxied_socket("wss://api.bitfinex.com/ws/2", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8899", proxy_type="http"))[1], ("api.bitfinex.com", 443, '/ws/2')) + self.assertEqual(_get_addrinfo_list("api.bitfinex.com", 443, True, proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8899", proxy_type="http")), + (socket.getaddrinfo("127.0.0.1", 8899, 0, socket.SOCK_STREAM, socket.SOL_TCP), True, None)) + self.assertEqual(connect("wss://api.bitfinex.com/ws/2", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port=8899, proxy_type="http"), None)[1], ("api.bitfinex.com", 443, '/ws/2')) + # TODO: Test SOCKS4 and SOCK5 proxies with unit tests + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testSSLopt(self): + ssloptions = { + "check_hostname": False, + "server_hostname": "ServerName", + "ssl_version": ssl.PROTOCOL_TLS_CLIENT, + "ciphers": "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:\ + TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:\ + ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:\ + ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:\ + DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:\ + ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256:\ + ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:\ + DHE-RSA-AES256-SHA256:ECDHE-ECDSA-AES128-SHA256:\ + ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:\ + ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA", + "ecdh_curve": "prime256v1" + } + ws_ssl1 = websocket.WebSocket(sslopt=ssloptions) + ws_ssl1.connect("wss://api.bitfinex.com/ws/2") + ws_ssl1.send("Hello") + ws_ssl1.close() + + ws_ssl2 = websocket.WebSocket(sslopt={"check_hostname": True}) + ws_ssl2.connect("wss://api.bitfinex.com/ws/2") + ws_ssl2.close + + def testProxyInfo(self): + self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http").proxy_protocol, "http") + self.assertRaises(ProxyError, proxy_info, http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="badval") + self.assertEqual(proxy_info(http_proxy_host="example.com", http_proxy_port="8080", proxy_type="http").proxy_host, "example.com") + self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http").proxy_port, "8080") + self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http").auth, None) + self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http", http_proxy_auth=("my_username123", "my_pass321")).auth[0], "my_username123") + self.assertEqual(proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http", http_proxy_auth=("my_username123", "my_pass321")).auth[1], "my_pass321") + + +if __name__ == "__main__": + unittest.main() diff --git a/websocket/tests/test_url.py b/websocket/tests/test_url.py new file mode 100644 index 0000000..7e155fd --- /dev/null +++ b/websocket/tests/test_url.py @@ -0,0 +1,301 @@ +# -*- coding: utf-8 -*- +# +import os +import unittest +from websocket._url import get_proxy_info, parse_url, _is_address_in_network, _is_no_proxy_host + +""" +test_url.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + + +class UrlTest(unittest.TestCase): + + def test_address_in_network(self): + self.assertTrue(_is_address_in_network('127.0.0.1', '127.0.0.0/8')) + self.assertTrue(_is_address_in_network('127.1.0.1', '127.0.0.0/8')) + self.assertFalse(_is_address_in_network('127.1.0.1', '127.0.0.0/24')) + + def testParseUrl(self): + p = parse_url("ws://www.example.com/r") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com/r/") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/r/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com/") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com:8080/r") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com:8080/") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("ws://www.example.com:8080") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/") + self.assertEqual(p[3], False) + + p = parse_url("wss://www.example.com:8080/r") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], True) + + p = parse_url("wss://www.example.com:8080/r?key=value") + self.assertEqual(p[0], "www.example.com") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r?key=value") + self.assertEqual(p[3], True) + + self.assertRaises(ValueError, parse_url, "http://www.example.com/r") + + p = parse_url("ws://[2a03:4000:123:83::3]/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 80) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("ws://[2a03:4000:123:83::3]:8080/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], False) + + p = parse_url("wss://[2a03:4000:123:83::3]/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 443) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], True) + + p = parse_url("wss://[2a03:4000:123:83::3]:8080/r") + self.assertEqual(p[0], "2a03:4000:123:83::3") + self.assertEqual(p[1], 8080) + self.assertEqual(p[2], "/r") + self.assertEqual(p[3], True) + + +class IsNoProxyHostTest(unittest.TestCase): + def setUp(self): + self.no_proxy = os.environ.get("no_proxy", None) + if "no_proxy" in os.environ: + del os.environ["no_proxy"] + + def tearDown(self): + if self.no_proxy: + os.environ["no_proxy"] = self.no_proxy + elif "no_proxy" in os.environ: + del os.environ["no_proxy"] + + def testMatchAll(self): + self.assertTrue(_is_no_proxy_host("any.websocket.org", ['*'])) + self.assertTrue(_is_no_proxy_host("192.168.0.1", ['*'])) + self.assertTrue(_is_no_proxy_host("any.websocket.org", ['other.websocket.org', '*'])) + os.environ['no_proxy'] = '*' + self.assertTrue(_is_no_proxy_host("any.websocket.org", None)) + self.assertTrue(_is_no_proxy_host("192.168.0.1", None)) + os.environ['no_proxy'] = 'other.websocket.org, *' + self.assertTrue(_is_no_proxy_host("any.websocket.org", None)) + + def testIpAddress(self): + self.assertTrue(_is_no_proxy_host("127.0.0.1", ['127.0.0.1'])) + self.assertFalse(_is_no_proxy_host("127.0.0.2", ['127.0.0.1'])) + self.assertTrue(_is_no_proxy_host("127.0.0.1", ['other.websocket.org', '127.0.0.1'])) + self.assertFalse(_is_no_proxy_host("127.0.0.2", ['other.websocket.org', '127.0.0.1'])) + os.environ['no_proxy'] = '127.0.0.1' + self.assertTrue(_is_no_proxy_host("127.0.0.1", None)) + self.assertFalse(_is_no_proxy_host("127.0.0.2", None)) + os.environ['no_proxy'] = 'other.websocket.org, 127.0.0.1' + self.assertTrue(_is_no_proxy_host("127.0.0.1", None)) + self.assertFalse(_is_no_proxy_host("127.0.0.2", None)) + + def testIpAddressInRange(self): + self.assertTrue(_is_no_proxy_host("127.0.0.1", ['127.0.0.0/8'])) + self.assertTrue(_is_no_proxy_host("127.0.0.2", ['127.0.0.0/8'])) + self.assertFalse(_is_no_proxy_host("127.1.0.1", ['127.0.0.0/24'])) + os.environ['no_proxy'] = '127.0.0.0/8' + self.assertTrue(_is_no_proxy_host("127.0.0.1", None)) + self.assertTrue(_is_no_proxy_host("127.0.0.2", None)) + os.environ['no_proxy'] = '127.0.0.0/24' + self.assertFalse(_is_no_proxy_host("127.1.0.1", None)) + + def testHostnameMatch(self): + self.assertTrue(_is_no_proxy_host("my.websocket.org", ['my.websocket.org'])) + self.assertTrue(_is_no_proxy_host("my.websocket.org", ['other.websocket.org', 'my.websocket.org'])) + self.assertFalse(_is_no_proxy_host("my.websocket.org", ['other.websocket.org'])) + os.environ['no_proxy'] = 'my.websocket.org' + self.assertTrue(_is_no_proxy_host("my.websocket.org", None)) + self.assertFalse(_is_no_proxy_host("other.websocket.org", None)) + os.environ['no_proxy'] = 'other.websocket.org, my.websocket.org' + self.assertTrue(_is_no_proxy_host("my.websocket.org", None)) + + def testHostnameMatchDomain(self): + self.assertTrue(_is_no_proxy_host("any.websocket.org", ['.websocket.org'])) + self.assertTrue(_is_no_proxy_host("my.other.websocket.org", ['.websocket.org'])) + self.assertTrue(_is_no_proxy_host("any.websocket.org", ['my.websocket.org', '.websocket.org'])) + self.assertFalse(_is_no_proxy_host("any.websocket.com", ['.websocket.org'])) + os.environ['no_proxy'] = '.websocket.org' + self.assertTrue(_is_no_proxy_host("any.websocket.org", None)) + self.assertTrue(_is_no_proxy_host("my.other.websocket.org", None)) + self.assertFalse(_is_no_proxy_host("any.websocket.com", None)) + os.environ['no_proxy'] = 'my.websocket.org, .websocket.org' + self.assertTrue(_is_no_proxy_host("any.websocket.org", None)) + + +class ProxyInfoTest(unittest.TestCase): + def setUp(self): + self.http_proxy = os.environ.get("http_proxy", None) + self.https_proxy = os.environ.get("https_proxy", None) + self.no_proxy = os.environ.get("no_proxy", None) + if "http_proxy" in os.environ: + del os.environ["http_proxy"] + if "https_proxy" in os.environ: + del os.environ["https_proxy"] + if "no_proxy" in os.environ: + del os.environ["no_proxy"] + + def tearDown(self): + if self.http_proxy: + os.environ["http_proxy"] = self.http_proxy + elif "http_proxy" in os.environ: + del os.environ["http_proxy"] + + if self.https_proxy: + os.environ["https_proxy"] = self.https_proxy + elif "https_proxy" in os.environ: + del os.environ["https_proxy"] + + if self.no_proxy: + os.environ["no_proxy"] = self.no_proxy + elif "no_proxy" in os.environ: + del os.environ["no_proxy"] + + def testProxyFromArgs(self): + self.assertEqual(get_proxy_info("echo.websocket.events", False, proxy_host="localhost"), ("localhost", 0, None)) + self.assertEqual(get_proxy_info("echo.websocket.events", False, proxy_host="localhost", proxy_port=3128), + ("localhost", 3128, None)) + self.assertEqual(get_proxy_info("echo.websocket.events", True, proxy_host="localhost"), ("localhost", 0, None)) + self.assertEqual(get_proxy_info("echo.websocket.events", True, proxy_host="localhost", proxy_port=3128), + ("localhost", 3128, None)) + + self.assertEqual(get_proxy_info("echo.websocket.events", False, proxy_host="localhost", proxy_auth=("a", "b")), + ("localhost", 0, ("a", "b"))) + self.assertEqual( + get_proxy_info("echo.websocket.events", False, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")), + ("localhost", 3128, ("a", "b"))) + self.assertEqual(get_proxy_info("echo.websocket.events", True, proxy_host="localhost", proxy_auth=("a", "b")), + ("localhost", 0, ("a", "b"))) + self.assertEqual( + get_proxy_info("echo.websocket.events", True, proxy_host="localhost", proxy_port=3128, proxy_auth=("a", "b")), + ("localhost", 3128, ("a", "b"))) + + self.assertEqual(get_proxy_info("echo.websocket.events", True, proxy_host="localhost", proxy_port=3128, + no_proxy=["example.com"], proxy_auth=("a", "b")), + ("localhost", 3128, ("a", "b"))) + self.assertEqual(get_proxy_info("echo.websocket.events", True, proxy_host="localhost", proxy_port=3128, + no_proxy=["echo.websocket.events"], proxy_auth=("a", "b")), + (None, 0, None)) + + def testProxyFromEnv(self): + os.environ["http_proxy"] = "http://localhost/" + self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, None)) + os.environ["http_proxy"] = "http://localhost:3128/" + self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", 3128, None)) + + os.environ["http_proxy"] = "http://localhost/" + os.environ["https_proxy"] = "http://localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, None)) + os.environ["http_proxy"] = "http://localhost:3128/" + os.environ["https_proxy"] = "http://localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", 3128, None)) + + os.environ["http_proxy"] = "http://localhost/" + os.environ["https_proxy"] = "http://localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", None, None)) + os.environ["http_proxy"] = "http://localhost:3128/" + os.environ["https_proxy"] = "http://localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", 3128, None)) + + os.environ["http_proxy"] = "http://a:b@localhost/" + self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", 3128, ("a", "b"))) + + os.environ["http_proxy"] = "http://a:b@localhost/" + os.environ["https_proxy"] = "http://a:b@localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.events", False), ("localhost", 3128, ("a", "b"))) + + os.environ["http_proxy"] = "http://a:b@localhost/" + os.environ["https_proxy"] = "http://a:b@localhost2/" + self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", 3128, ("a", "b"))) + + os.environ["http_proxy"] = "http://john%40example.com:P%40SSWORD@localhost:3128/" + os.environ["https_proxy"] = "http://john%40example.com:P%40SSWORD@localhost2:3128/" + self.assertEqual(get_proxy_info("echo.websocket.events", True), ("localhost2", 3128, ("john@example.com", "P@SSWORD"))) + + os.environ["http_proxy"] = "http://a:b@localhost/" + os.environ["https_proxy"] = "http://a:b@localhost2/" + os.environ["no_proxy"] = "example1.com,example2.com" + self.assertEqual(get_proxy_info("example.1.com", True), ("localhost2", None, ("a", "b"))) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + os.environ["no_proxy"] = "example1.com,example2.com, echo.websocket.events" + self.assertEqual(get_proxy_info("echo.websocket.events", True), (None, 0, None)) + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + os.environ["no_proxy"] = "example1.com,example2.com, .websocket.events" + self.assertEqual(get_proxy_info("echo.websocket.events", True), (None, 0, None)) + + os.environ["http_proxy"] = "http://a:b@localhost:3128/" + os.environ["https_proxy"] = "http://a:b@localhost2:3128/" + os.environ["no_proxy"] = "127.0.0.0/8, 192.168.0.0/16" + self.assertEqual(get_proxy_info("127.0.0.1", False), (None, 0, None)) + self.assertEqual(get_proxy_info("192.168.1.1", False), (None, 0, None)) + + +if __name__ == "__main__": + unittest.main() diff --git a/websocket/tests/test_websocket.py b/websocket/tests/test_websocket.py new file mode 100644 index 0000000..ae42ab5 --- /dev/null +++ b/websocket/tests/test_websocket.py @@ -0,0 +1,455 @@ +# -*- coding: utf-8 -*- +# +import os +import os.path +import socket +import websocket as ws +import unittest +from websocket._handshake import _create_sec_websocket_key, \ + _validate as _validate_header +from websocket._http import read_headers +from websocket._utils import validate_utf8 +from base64 import decodebytes as base64decode + +""" +test_websocket.py +websocket - WebSocket client library for Python + +Copyright 2022 engn33r + +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. +""" + +try: + import ssl + from ssl import SSLError +except ImportError: + # dummy class of SSLError for ssl none-support environment. + class SSLError(Exception): + pass + +# Skip test to access the internet unless TEST_WITH_INTERNET == 1 +TEST_WITH_INTERNET = os.environ.get('TEST_WITH_INTERNET', '0') == '1' +# Skip tests relying on local websockets server unless LOCAL_WS_SERVER_PORT != -1 +LOCAL_WS_SERVER_PORT = os.environ.get('LOCAL_WS_SERVER_PORT', '-1') +TEST_WITH_LOCAL_SERVER = LOCAL_WS_SERVER_PORT != '-1' +TRACEABLE = True + + +def create_mask_key(_): + return "abcd" + + +class SockMock: + def __init__(self): + self.data = [] + self.sent = [] + + def add_packet(self, data): + self.data.append(data) + + def gettimeout(self): + return None + + def recv(self, bufsize): + if self.data: + e = self.data.pop(0) + if isinstance(e, Exception): + raise e + if len(e) > bufsize: + self.data.insert(0, e[bufsize:]) + return e[:bufsize] + + def send(self, data): + self.sent.append(data) + return len(data) + + def close(self): + pass + + +class HeaderSockMock(SockMock): + + def __init__(self, fname): + SockMock.__init__(self) + path = os.path.join(os.path.dirname(__file__), fname) + with open(path, "rb") as f: + self.add_packet(f.read()) + + +class WebSocketTest(unittest.TestCase): + def setUp(self): + ws.enableTrace(TRACEABLE) + + def tearDown(self): + pass + + def testDefaultTimeout(self): + self.assertEqual(ws.getdefaulttimeout(), None) + ws.setdefaulttimeout(10) + self.assertEqual(ws.getdefaulttimeout(), 10) + ws.setdefaulttimeout(None) + + def testWSKey(self): + key = _create_sec_websocket_key() + self.assertTrue(key != 24) + self.assertTrue(str("¥n") not in key) + + def testNonce(self): + """ WebSocket key should be a random 16-byte nonce. + """ + key = _create_sec_websocket_key() + nonce = base64decode(key.encode("utf-8")) + self.assertEqual(16, len(nonce)) + + def testWsUtils(self): + key = "c6b8hTg4EeGb2gQMztV1/g==" + required_header = { + "upgrade": "websocket", + "connection": "upgrade", + "sec-websocket-accept": "Kxep+hNu9n51529fGidYu7a3wO0="} + self.assertEqual(_validate_header(required_header, key, None), (True, None)) + + header = required_header.copy() + header["upgrade"] = "http" + self.assertEqual(_validate_header(header, key, None), (False, None)) + del header["upgrade"] + self.assertEqual(_validate_header(header, key, None), (False, None)) + + header = required_header.copy() + header["connection"] = "something" + self.assertEqual(_validate_header(header, key, None), (False, None)) + del header["connection"] + self.assertEqual(_validate_header(header, key, None), (False, None)) + + header = required_header.copy() + header["sec-websocket-accept"] = "something" + self.assertEqual(_validate_header(header, key, None), (False, None)) + del header["sec-websocket-accept"] + self.assertEqual(_validate_header(header, key, None), (False, None)) + + header = required_header.copy() + header["sec-websocket-protocol"] = "sub1" + self.assertEqual(_validate_header(header, key, ["sub1", "sub2"]), (True, "sub1")) + # This case will print out a logging error using the error() function, but that is expected + self.assertEqual(_validate_header(header, key, ["sub2", "sub3"]), (False, None)) + + header = required_header.copy() + header["sec-websocket-protocol"] = "sUb1" + self.assertEqual(_validate_header(header, key, ["Sub1", "suB2"]), (True, "sub1")) + + header = required_header.copy() + # This case will print out a logging error using the error() function, but that is expected + self.assertEqual(_validate_header(header, key, ["Sub1", "suB2"]), (False, None)) + + def testReadHeader(self): + status, header, status_message = read_headers(HeaderSockMock("data/header01.txt")) + self.assertEqual(status, 101) + self.assertEqual(header["connection"], "Upgrade") + + status, header, status_message = read_headers(HeaderSockMock("data/header03.txt")) + self.assertEqual(status, 101) + self.assertEqual(header["connection"], "Upgrade, Keep-Alive") + + HeaderSockMock("data/header02.txt") + self.assertRaises(ws.WebSocketException, read_headers, HeaderSockMock("data/header02.txt")) + + def testSend(self): + # TODO: add longer frame data + sock = ws.WebSocket() + sock.set_mask_key(create_mask_key) + s = sock.sock = HeaderSockMock("data/header01.txt") + sock.send("Hello") + self.assertEqual(s.sent[0], b'\x81\x85abcd)\x07\x0f\x08\x0e') + + sock.send("こんにちは") + self.assertEqual(s.sent[1], b'\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc') + +# sock.send("x" * 5000) +# self.assertEqual(s.sent[1], b'\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc") + + self.assertEqual(sock.send_binary(b'1111111111101'), 19) + + def testRecv(self): + # TODO: add longer frame data + sock = ws.WebSocket() + s = sock.sock = SockMock() + something = b'\x81\x8fabcd\x82\xe3\xf0\x87\xe3\xf1\x80\xe5\xca\x81\xe2\xc5\x82\xe3\xcc' + s.add_packet(something) + data = sock.recv() + self.assertEqual(data, "こんにちは") + + s.add_packet(b'\x81\x85abcd)\x07\x0f\x08\x0e') + data = sock.recv() + self.assertEqual(data, "Hello") + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testIter(self): + count = 2 + s = ws.create_connection('wss://api.bitfinex.com/ws/2') + s.send('{"event": "subscribe", "channel": "ticker"}') + for _ in s: + count -= 1 + if count == 0: + break + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testNext(self): + sock = ws.create_connection('wss://api.bitfinex.com/ws/2') + self.assertEqual(str, type(next(sock))) + + def testInternalRecvStrict(self): + sock = ws.WebSocket() + s = sock.sock = SockMock() + s.add_packet(b'foo') + s.add_packet(socket.timeout()) + s.add_packet(b'bar') + # s.add_packet(SSLError("The read operation timed out")) + s.add_packet(b'baz') + with self.assertRaises(ws.WebSocketTimeoutException): + sock.frame_buffer.recv_strict(9) + # with self.assertRaises(SSLError): + # data = sock._recv_strict(9) + data = sock.frame_buffer.recv_strict(9) + self.assertEqual(data, b'foobarbaz') + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.frame_buffer.recv_strict(1) + + def testRecvTimeout(self): + sock = ws.WebSocket() + s = sock.sock = SockMock() + s.add_packet(b'\x81') + s.add_packet(socket.timeout()) + s.add_packet(b'\x8dabcd\x29\x07\x0f\x08\x0e') + s.add_packet(socket.timeout()) + s.add_packet(b'\x4e\x43\x33\x0e\x10\x0f\x00\x40') + with self.assertRaises(ws.WebSocketTimeoutException): + sock.recv() + with self.assertRaises(ws.WebSocketTimeoutException): + sock.recv() + data = sock.recv() + self.assertEqual(data, "Hello, World!") + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.recv() + + def testRecvWithSimpleFragmentation(self): + sock = ws.WebSocket() + s = sock.sock = SockMock() + # OPCODE=TEXT, FIN=0, MSG="Brevity is " + s.add_packet(b'\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C') + # OPCODE=CONT, FIN=1, MSG="the soul of wit" + s.add_packet(b'\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17') + data = sock.recv() + self.assertEqual(data, "Brevity is the soul of wit") + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.recv() + + def testRecvWithFireEventOfFragmentation(self): + sock = ws.WebSocket(fire_cont_frame=True) + s = sock.sock = SockMock() + # OPCODE=TEXT, FIN=0, MSG="Brevity is " + s.add_packet(b'\x01\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C') + # OPCODE=CONT, FIN=0, MSG="Brevity is " + s.add_packet(b'\x00\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C') + # OPCODE=CONT, FIN=1, MSG="the soul of wit" + s.add_packet(b'\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17') + + _, data = sock.recv_data() + self.assertEqual(data, b'Brevity is ') + _, data = sock.recv_data() + self.assertEqual(data, b'Brevity is ') + _, data = sock.recv_data() + self.assertEqual(data, b'the soul of wit') + + # OPCODE=CONT, FIN=0, MSG="Brevity is " + s.add_packet(b'\x80\x8babcd#\x10\x06\x12\x08\x16\x1aD\x08\x11C') + + with self.assertRaises(ws.WebSocketException): + sock.recv_data() + + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.recv() + + def testClose(self): + sock = ws.WebSocket() + sock.connected = True + sock.close + + sock = ws.WebSocket() + s = sock.sock = SockMock() + sock.connected = True + s.add_packet(b'\x88\x80\x17\x98p\x84') + sock.recv() + self.assertEqual(sock.connected, False) + + def testRecvContFragmentation(self): + sock = ws.WebSocket() + s = sock.sock = SockMock() + # OPCODE=CONT, FIN=1, MSG="the soul of wit" + s.add_packet(b'\x80\x8fabcd\x15\n\x06D\x12\r\x16\x08A\r\x05D\x16\x0b\x17') + self.assertRaises(ws.WebSocketException, sock.recv) + + def testRecvWithProlongedFragmentation(self): + sock = ws.WebSocket() + s = sock.sock = SockMock() + # OPCODE=TEXT, FIN=0, MSG="Once more unto the breach, " + s.add_packet(b'\x01\x9babcd.\x0c\x00\x01A\x0f\x0c\x16\x04B\x16\n\x15\rC\x10\t\x07C\x06\x13\x07\x02\x07\tNC') + # OPCODE=CONT, FIN=0, MSG="dear friends, " + s.add_packet(b'\x00\x8eabcd\x05\x07\x02\x16A\x04\x11\r\x04\x0c\x07\x17MB') + # OPCODE=CONT, FIN=1, MSG="once more" + s.add_packet(b'\x80\x89abcd\x0e\x0c\x00\x01A\x0f\x0c\x16\x04') + data = sock.recv() + self.assertEqual( + data, + "Once more unto the breach, dear friends, once more") + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.recv() + + def testRecvWithFragmentationAndControlFrame(self): + sock = ws.WebSocket() + sock.set_mask_key(create_mask_key) + s = sock.sock = SockMock() + # OPCODE=TEXT, FIN=0, MSG="Too much " + s.add_packet(b'\x01\x89abcd5\r\x0cD\x0c\x17\x00\x0cA') + # OPCODE=PING, FIN=1, MSG="Please PONG this" + s.add_packet(b'\x89\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17') + # OPCODE=CONT, FIN=1, MSG="of a good thing" + s.add_packet(b'\x80\x8fabcd\x0e\x04C\x05A\x05\x0c\x0b\x05B\x17\x0c\x08\x0c\x04') + data = sock.recv() + self.assertEqual(data, "Too much of a good thing") + with self.assertRaises(ws.WebSocketConnectionClosedException): + sock.recv() + self.assertEqual( + s.sent[0], + b'\x8a\x90abcd1\x0e\x06\x05\x12\x07C4.,$D\x15\n\n\x17') + + @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled") + def testWebSocket(self): + s = ws.create_connection("ws://127.0.0.1:" + LOCAL_WS_SERVER_PORT) + self.assertNotEqual(s, None) + s.send("Hello, World") + result = s.next() + s.fileno() + self.assertEqual(result, "Hello, World") + + s.send("こにゃにゃちは、世界") + result = s.recv() + self.assertEqual(result, "こにゃにゃちは、世界") + self.assertRaises(ValueError, s.send_close, -1, "") + s.close() + + @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled") + def testPingPong(self): + s = ws.create_connection("ws://127.0.0.1:" + LOCAL_WS_SERVER_PORT) + self.assertNotEqual(s, None) + s.ping("Hello") + s.pong("Hi") + s.close() + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testSupportRedirect(self): + s = ws.WebSocket() + self.assertRaises(ws._exceptions.WebSocketBadStatusException, s.connect, "ws://google.com/") + # Need to find a URL that has a redirect code leading to a websocket + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testSecureWebSocket(self): + import ssl + s = ws.create_connection("wss://api.bitfinex.com/ws/2") + self.assertNotEqual(s, None) + self.assertTrue(isinstance(s.sock, ssl.SSLSocket)) + self.assertEqual(s.getstatus(), 101) + self.assertNotEqual(s.getheaders(), None) + s.settimeout(10) + self.assertEqual(s.gettimeout(), 10) + self.assertEqual(s.getsubprotocol(), None) + s.abort() + + @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled") + def testWebSocketWithCustomHeader(self): + s = ws.create_connection("ws://127.0.0.1:" + LOCAL_WS_SERVER_PORT, + headers={"User-Agent": "PythonWebsocketClient"}) + self.assertNotEqual(s, None) + self.assertEqual(s.getsubprotocol(), None) + s.send("Hello, World") + result = s.recv() + self.assertEqual(result, "Hello, World") + self.assertRaises(ValueError, s.close, -1, "") + s.close() + + @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled") + def testAfterClose(self): + s = ws.create_connection("ws://127.0.0.1:" + LOCAL_WS_SERVER_PORT) + self.assertNotEqual(s, None) + s.close() + self.assertRaises(ws.WebSocketConnectionClosedException, s.send, "Hello") + self.assertRaises(ws.WebSocketConnectionClosedException, s.recv) + + +class SockOptTest(unittest.TestCase): + @unittest.skipUnless(TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled") + def testSockOpt(self): + sockopt = ((socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),) + s = ws.create_connection("ws://127.0.0.1:" + LOCAL_WS_SERVER_PORT, sockopt=sockopt) + self.assertNotEqual(s.sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY), 0) + s.close() + + +class UtilsTest(unittest.TestCase): + def testUtf8Validator(self): + state = validate_utf8(b'\xf0\x90\x80\x80') + self.assertEqual(state, True) + state = validate_utf8(b'\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5\xed\xa0\x80edited') + self.assertEqual(state, False) + state = validate_utf8(b'') + self.assertEqual(state, True) + + +class HandshakeTest(unittest.TestCase): + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def test_http_SSL(self): + websock1 = ws.WebSocket(sslopt={"cert_chain": ssl.get_default_verify_paths().capath}, enable_multithread=False) + self.assertRaises(ValueError, + websock1.connect, "wss://api.bitfinex.com/ws/2") + websock2 = ws.WebSocket(sslopt={"certfile": "myNonexistentCertFile"}) + self.assertRaises(FileNotFoundError, + websock2.connect, "wss://api.bitfinex.com/ws/2") + + @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled") + def testManualHeaders(self): + websock3 = ws.WebSocket(sslopt={"ca_certs": ssl.get_default_verify_paths().cafile, + "ca_cert_path": ssl.get_default_verify_paths().capath}) + self.assertRaises(ws._exceptions.WebSocketBadStatusException, + websock3.connect, "wss://api.bitfinex.com/ws/2", cookie="chocolate", + origin="testing_websockets.com", + host="echo.websocket.org/websocket-client-test", + subprotocols=["testproto"], + connection="Upgrade", + header={"CustomHeader1":"123", + "Cookie":"TestValue", + "Sec-WebSocket-Key":"k9kFAUWNAMmf5OEMfTlOEA==", + "Sec-WebSocket-Protocol":"newprotocol"}) + + def testIPv6(self): + websock2 = ws.WebSocket() + self.assertRaises(ValueError, websock2.connect, "2001:4860:4860::8888") + + def testBadURLs(self): + websock3 = ws.WebSocket() + self.assertRaises(ValueError, websock3.connect, "ws//example.com") + self.assertRaises(ws.WebSocketAddressException, websock3.connect, "ws://example") + self.assertRaises(ValueError, websock3.connect, "example.com") + + +if __name__ == "__main__": + unittest.main() diff --git a/websockets/__init__.py b/websockets/__init__.py new file mode 100644 index 0000000..dcf3d81 --- /dev/null +++ b/websockets/__init__.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +from .imports import lazy_import +from .version import version as __version__ # noqa: F401 + + +__all__ = [ + "AbortHandshake", + "basic_auth_protocol_factory", + "BasicAuthWebSocketServerProtocol", + "broadcast", + "ClientProtocol", + "connect", + "ConnectionClosed", + "ConnectionClosedError", + "ConnectionClosedOK", + "Data", + "DuplicateParameter", + "ExtensionName", + "ExtensionParameter", + "InvalidHandshake", + "InvalidHeader", + "InvalidHeaderFormat", + "InvalidHeaderValue", + "InvalidMessage", + "InvalidOrigin", + "InvalidParameterName", + "InvalidParameterValue", + "InvalidState", + "InvalidStatus", + "InvalidStatusCode", + "InvalidUpgrade", + "InvalidURI", + "LoggerLike", + "NegotiationError", + "Origin", + "parse_uri", + "PayloadTooBig", + "ProtocolError", + "RedirectHandshake", + "SecurityError", + "serve", + "ServerProtocol", + "Subprotocol", + "unix_connect", + "unix_serve", + "WebSocketClientProtocol", + "WebSocketCommonProtocol", + "WebSocketException", + "WebSocketProtocolError", + "WebSocketServer", + "WebSocketServerProtocol", + "WebSocketURI", +] + +lazy_import( + globals(), + aliases={ + "auth": ".legacy", + "basic_auth_protocol_factory": ".legacy.auth", + "BasicAuthWebSocketServerProtocol": ".legacy.auth", + "broadcast": ".legacy.protocol", + "ClientProtocol": ".client", + "connect": ".legacy.client", + "unix_connect": ".legacy.client", + "WebSocketClientProtocol": ".legacy.client", + "Headers": ".datastructures", + "MultipleValuesError": ".datastructures", + "WebSocketException": ".exceptions", + "ConnectionClosed": ".exceptions", + "ConnectionClosedError": ".exceptions", + "ConnectionClosedOK": ".exceptions", + "InvalidHandshake": ".exceptions", + "SecurityError": ".exceptions", + "InvalidMessage": ".exceptions", + "InvalidHeader": ".exceptions", + "InvalidHeaderFormat": ".exceptions", + "InvalidHeaderValue": ".exceptions", + "InvalidOrigin": ".exceptions", + "InvalidUpgrade": ".exceptions", + "InvalidStatus": ".exceptions", + "InvalidStatusCode": ".exceptions", + "NegotiationError": ".exceptions", + "DuplicateParameter": ".exceptions", + "InvalidParameterName": ".exceptions", + "InvalidParameterValue": ".exceptions", + "AbortHandshake": ".exceptions", + "RedirectHandshake": ".exceptions", + "InvalidState": ".exceptions", + "InvalidURI": ".exceptions", + "PayloadTooBig": ".exceptions", + "ProtocolError": ".exceptions", + "WebSocketProtocolError": ".exceptions", + "protocol": ".legacy", + "WebSocketCommonProtocol": ".legacy.protocol", + "ServerProtocol": ".server", + "serve": ".legacy.server", + "unix_serve": ".legacy.server", + "WebSocketServerProtocol": ".legacy.server", + "WebSocketServer": ".legacy.server", + "Data": ".typing", + "LoggerLike": ".typing", + "Origin": ".typing", + "ExtensionHeader": ".typing", + "ExtensionParameter": ".typing", + "Subprotocol": ".typing", + }, + deprecated_aliases={ + "framing": ".legacy", + "handshake": ".legacy", + "parse_uri": ".uri", + "WebSocketURI": ".uri", + }, +) diff --git a/websockets/__main__.py b/websockets/__main__.py new file mode 100644 index 0000000..f2ea5cf --- /dev/null +++ b/websockets/__main__.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import argparse +import os +import signal +import sys +import threading + + +try: + import readline # noqa: F401 +except ImportError: # Windows has no `readline` normally + pass + +from .sync.client import ClientConnection, connect +from .version import version as websockets_version + + +if sys.platform == "win32": + + def win_enable_vt100() -> None: + """ + Enable VT-100 for console output on Windows. + + See also https://bugs.python.org/issue29059. + + """ + import ctypes + + STD_OUTPUT_HANDLE = ctypes.c_uint(-11) + INVALID_HANDLE_VALUE = ctypes.c_uint(-1) + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x004 + + handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) + if handle == INVALID_HANDLE_VALUE: + raise RuntimeError("unable to obtain stdout handle") + + cur_mode = ctypes.c_uint() + if ctypes.windll.kernel32.GetConsoleMode(handle, ctypes.byref(cur_mode)) == 0: + raise RuntimeError("unable to query current console mode") + + # ctypes ints lack support for the required bit-OR operation. + # Temporarily convert to Py int, do the OR and convert back. + py_int_mode = int.from_bytes(cur_mode, sys.byteorder) + new_mode = ctypes.c_uint(py_int_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + + if ctypes.windll.kernel32.SetConsoleMode(handle, new_mode) == 0: + raise RuntimeError("unable to set console mode") + + +def print_during_input(string: str) -> None: + sys.stdout.write( + # Save cursor position + "\N{ESC}7" + # Add a new line + "\N{LINE FEED}" + # Move cursor up + "\N{ESC}[A" + # Insert blank line, scroll last line down + "\N{ESC}[L" + # Print string in the inserted blank line + f"{string}\N{LINE FEED}" + # Restore cursor position + "\N{ESC}8" + # Move cursor down + "\N{ESC}[B" + ) + sys.stdout.flush() + + +def print_over_input(string: str) -> None: + sys.stdout.write( + # Move cursor to beginning of line + "\N{CARRIAGE RETURN}" + # Delete current line + "\N{ESC}[K" + # Print string + f"{string}\N{LINE FEED}" + ) + sys.stdout.flush() + + +def print_incoming_messages(websocket: ClientConnection, stop: threading.Event) -> None: + for message in websocket: + if isinstance(message, str): + print_during_input("< " + message) + else: + print_during_input("< (binary) " + message.hex()) + if not stop.is_set(): + # When the server closes the connection, raise KeyboardInterrupt + # in the main thread to exit the program. + if sys.platform == "win32": + ctrl_c = signal.CTRL_C_EVENT + else: + ctrl_c = signal.SIGINT + os.kill(os.getpid(), ctrl_c) + + +def main() -> None: + # Parse command line arguments. + parser = argparse.ArgumentParser( + prog="python -m websockets", + description="Interactive WebSocket client.", + add_help=False, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument("--version", action="store_true") + group.add_argument("uri", metavar="", nargs="?") + args = parser.parse_args() + + if args.version: + print(f"websockets {websockets_version}") + return + + if args.uri is None: + parser.error("the following arguments are required: ") + + # If we're on Windows, enable VT100 terminal support. + if sys.platform == "win32": + try: + win_enable_vt100() + except RuntimeError as exc: + sys.stderr.write( + f"Unable to set terminal to VT100 mode. This is only " + f"supported since Win10 anniversary update. Expect " + f"weird symbols on the terminal.\nError: {exc}\n" + ) + sys.stderr.flush() + + try: + websocket = connect(args.uri) + except Exception as exc: + print(f"Failed to connect to {args.uri}: {exc}.") + sys.exit(1) + else: + print(f"Connected to {args.uri}.") + + stop = threading.Event() + + # Start the thread that reads messages from the connection. + thread = threading.Thread(target=print_incoming_messages, args=(websocket, stop)) + thread.start() + + # Read from stdin in the main thread in order to receive signals. + try: + while True: + # Since there's no size limit, put_nowait is identical to put. + message = input("> ") + websocket.send(message) + except (KeyboardInterrupt, EOFError): # ^C, ^D + stop.set() + websocket.close() + print_over_input("Connection closed.") + + thread.join() + + +if __name__ == "__main__": + main() diff --git a/websockets/auth.py b/websockets/auth.py new file mode 100644 index 0000000..5292e4f --- /dev/null +++ b/websockets/auth.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +# See #940 for why lazy_import isn't used here for backwards compatibility. +from .legacy.auth import * diff --git a/websockets/client.py b/websockets/client.py new file mode 100644 index 0000000..bf8427c --- /dev/null +++ b/websockets/client.py @@ -0,0 +1,358 @@ +from __future__ import annotations + +import warnings +from typing import Any, Generator, List, Optional, Sequence + +from .datastructures import Headers, MultipleValuesError +from .exceptions import ( + InvalidHandshake, + InvalidHeader, + InvalidHeaderValue, + InvalidStatus, + InvalidUpgrade, + NegotiationError, +) +from .extensions import ClientExtensionFactory, Extension +from .headers import ( + build_authorization_basic, + build_extension, + build_host, + build_subprotocol, + parse_connection, + parse_extension, + parse_subprotocol, + parse_upgrade, +) +from .http11 import Request, Response +from .protocol import CLIENT, CONNECTING, OPEN, Protocol, State +from .typing import ( + ConnectionOption, + ExtensionHeader, + LoggerLike, + Origin, + Subprotocol, + UpgradeProtocol, +) +from .uri import WebSocketURI +from .utils import accept_key, generate_key + + +# See #940 for why lazy_import isn't used here for backwards compatibility. +from .legacy.client import * # isort:skip # noqa: I001 + + +__all__ = ["ClientProtocol"] + + +class ClientProtocol(Protocol): + """ + Sans-I/O implementation of a WebSocket client connection. + + Args: + wsuri: URI of the WebSocket server, parsed + with :func:`~websockets.uri.parse_uri`. + origin: value of the ``Origin`` header. This is useful when connecting + to a server that validates the ``Origin`` header to defend against + Cross-Site WebSocket Hijacking attacks. + extensions: list of supported extensions, in order in which they + should be tried. + subprotocols: list of supported subprotocols, in order of decreasing + preference. + state: initial state of the WebSocket connection. + max_size: maximum size of incoming messages in bytes; + :obj:`None` disables the limit. + logger: logger for this connection; + defaults to ``logging.getLogger("websockets.client")``; + see the :doc:`logging guide <../../topics/logging>` for details. + + """ + + def __init__( + self, + wsuri: WebSocketURI, + *, + origin: Optional[Origin] = None, + extensions: Optional[Sequence[ClientExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + state: State = CONNECTING, + max_size: Optional[int] = 2**20, + logger: Optional[LoggerLike] = None, + ): + super().__init__( + side=CLIENT, + state=state, + max_size=max_size, + logger=logger, + ) + self.wsuri = wsuri + self.origin = origin + self.available_extensions = extensions + self.available_subprotocols = subprotocols + self.key = generate_key() + + def connect(self) -> Request: + """ + Create a handshake request to open a connection. + + You must send the handshake request with :meth:`send_request`. + + You can modify it before sending it, for example to add HTTP headers. + + Returns: + Request: WebSocket handshake request event to send to the server. + + """ + headers = Headers() + + headers["Host"] = build_host( + self.wsuri.host, self.wsuri.port, self.wsuri.secure + ) + + if self.wsuri.user_info: + headers["Authorization"] = build_authorization_basic(*self.wsuri.user_info) + + if self.origin is not None: + headers["Origin"] = self.origin + + headers["Upgrade"] = "websocket" + headers["Connection"] = "Upgrade" + headers["Sec-WebSocket-Key"] = self.key + headers["Sec-WebSocket-Version"] = "13" + + if self.available_extensions is not None: + extensions_header = build_extension( + [ + (extension_factory.name, extension_factory.get_request_params()) + for extension_factory in self.available_extensions + ] + ) + headers["Sec-WebSocket-Extensions"] = extensions_header + + if self.available_subprotocols is not None: + protocol_header = build_subprotocol(self.available_subprotocols) + headers["Sec-WebSocket-Protocol"] = protocol_header + + return Request(self.wsuri.resource_name, headers) + + def process_response(self, response: Response) -> None: + """ + Check a handshake response. + + Args: + request: WebSocket handshake response received from the server. + + Raises: + InvalidHandshake: if the handshake response is invalid. + + """ + + if response.status_code != 101: + raise InvalidStatus(response) + + headers = response.headers + + connection: List[ConnectionOption] = sum( + [parse_connection(value) for value in headers.get_all("Connection")], [] + ) + + if not any(value.lower() == "upgrade" for value in connection): + raise InvalidUpgrade( + "Connection", ", ".join(connection) if connection else None + ) + + upgrade: List[UpgradeProtocol] = sum( + [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] + ) + + # For compatibility with non-strict implementations, ignore case when + # checking the Upgrade header. It's supposed to be 'WebSocket'. + if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): + raise InvalidUpgrade("Upgrade", ", ".join(upgrade) if upgrade else None) + + try: + s_w_accept = headers["Sec-WebSocket-Accept"] + except KeyError as exc: + raise InvalidHeader("Sec-WebSocket-Accept") from exc + except MultipleValuesError as exc: + raise InvalidHeader( + "Sec-WebSocket-Accept", + "more than one Sec-WebSocket-Accept header found", + ) from exc + + if s_w_accept != accept_key(self.key): + raise InvalidHeaderValue("Sec-WebSocket-Accept", s_w_accept) + + self.extensions = self.process_extensions(headers) + + self.subprotocol = self.process_subprotocol(headers) + + def process_extensions(self, headers: Headers) -> List[Extension]: + """ + Handle the Sec-WebSocket-Extensions HTTP response header. + + Check that each extension is supported, as well as its parameters. + + :rfc:`6455` leaves the rules up to the specification of each + extension. + + To provide this level of flexibility, for each extension accepted by + the server, we check for a match with each extension available in the + client configuration. If no match is found, an exception is raised. + + If several variants of the same extension are accepted by the server, + it may be configured several times, which won't make sense in general. + Extensions must implement their own requirements. For this purpose, + the list of previously accepted extensions is provided. + + Other requirements, for example related to mandatory extensions or the + order of extensions, may be implemented by overriding this method. + + Args: + headers: WebSocket handshake response headers. + + Returns: + List[Extension]: List of accepted extensions. + + Raises: + InvalidHandshake: to abort the handshake. + + """ + accepted_extensions: List[Extension] = [] + + extensions = headers.get_all("Sec-WebSocket-Extensions") + + if extensions: + if self.available_extensions is None: + raise InvalidHandshake("no extensions supported") + + parsed_extensions: List[ExtensionHeader] = sum( + [parse_extension(header_value) for header_value in extensions], [] + ) + + for name, response_params in parsed_extensions: + for extension_factory in self.available_extensions: + # Skip non-matching extensions based on their name. + if extension_factory.name != name: + continue + + # Skip non-matching extensions based on their params. + try: + extension = extension_factory.process_response_params( + response_params, accepted_extensions + ) + except NegotiationError: + continue + + # Add matching extension to the final list. + accepted_extensions.append(extension) + + # Break out of the loop once we have a match. + break + + # If we didn't break from the loop, no extension in our list + # matched what the server sent. Fail the connection. + else: + raise NegotiationError( + f"Unsupported extension: " + f"name = {name}, params = {response_params}" + ) + + return accepted_extensions + + def process_subprotocol(self, headers: Headers) -> Optional[Subprotocol]: + """ + Handle the Sec-WebSocket-Protocol HTTP response header. + + If provided, check that it contains exactly one supported subprotocol. + + Args: + headers: WebSocket handshake response headers. + + Returns: + Optional[Subprotocol]: Subprotocol, if one was selected. + + """ + subprotocol: Optional[Subprotocol] = None + + subprotocols = headers.get_all("Sec-WebSocket-Protocol") + + if subprotocols: + if self.available_subprotocols is None: + raise InvalidHandshake("no subprotocols supported") + + parsed_subprotocols: Sequence[Subprotocol] = sum( + [parse_subprotocol(header_value) for header_value in subprotocols], [] + ) + + if len(parsed_subprotocols) > 1: + subprotocols_display = ", ".join(parsed_subprotocols) + raise InvalidHandshake(f"multiple subprotocols: {subprotocols_display}") + + subprotocol = parsed_subprotocols[0] + + if subprotocol not in self.available_subprotocols: + raise NegotiationError(f"unsupported subprotocol: {subprotocol}") + + return subprotocol + + def send_request(self, request: Request) -> None: + """ + Send a handshake request to the server. + + Args: + request: WebSocket handshake request event. + + """ + if self.debug: + self.logger.debug("> GET %s HTTP/1.1", request.path) + for key, value in request.headers.raw_items(): + self.logger.debug("> %s: %s", key, value) + + self.writes.append(request.serialize()) + + def parse(self) -> Generator[None, None, None]: + if self.state is CONNECTING: + try: + response = yield from Response.parse( + self.reader.read_line, + self.reader.read_exact, + self.reader.read_to_eof, + ) + except Exception as exc: + self.handshake_exc = exc + self.parser = self.discard() + next(self.parser) # start coroutine + yield + + if self.debug: + code, phrase = response.status_code, response.reason_phrase + self.logger.debug("< HTTP/1.1 %d %s", code, phrase) + for key, value in response.headers.raw_items(): + self.logger.debug("< %s: %s", key, value) + if response.body is not None: + self.logger.debug("< [body] (%d bytes)", len(response.body)) + + try: + self.process_response(response) + except InvalidHandshake as exc: + response._exception = exc + self.events.append(response) + self.handshake_exc = exc + self.parser = self.discard() + next(self.parser) # start coroutine + yield + + assert self.state is CONNECTING + self.state = OPEN + self.events.append(response) + + yield from super().parse() + + +class ClientConnection(ClientProtocol): + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "ClientConnection was renamed to ClientProtocol", + DeprecationWarning, + ) + super().__init__(*args, **kwargs) diff --git a/websockets/connection.py b/websockets/connection.py new file mode 100644 index 0000000..88bcda1 --- /dev/null +++ b/websockets/connection.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +import warnings + +# lazy_import doesn't support this use case. +from .protocol import SEND_EOF, Protocol as Connection, Side, State # noqa: F401 + + +warnings.warn( + "websockets.connection was renamed to websockets.protocol " + "and Connection was renamed to Protocol", + DeprecationWarning, +) diff --git a/websockets/datastructures.py b/websockets/datastructures.py new file mode 100644 index 0000000..a0a6484 --- /dev/null +++ b/websockets/datastructures.py @@ -0,0 +1,194 @@ +from __future__ import annotations + +from typing import ( + Any, + Dict, + Iterable, + Iterator, + List, + Mapping, + MutableMapping, + Protocol, + Tuple, + Union, +) + + +__all__ = ["Headers", "HeadersLike", "MultipleValuesError"] + + +class MultipleValuesError(LookupError): + """ + Exception raised when :class:`Headers` has more than one value for a key. + + """ + + def __str__(self) -> str: + # Implement the same logic as KeyError_str in Objects/exceptions.c. + if len(self.args) == 1: + return repr(self.args[0]) + return super().__str__() + + +class Headers(MutableMapping[str, str]): + """ + Efficient data structure for manipulating HTTP headers. + + A :class:`list` of ``(name, values)`` is inefficient for lookups. + + A :class:`dict` doesn't suffice because header names are case-insensitive + and multiple occurrences of headers with the same name are possible. + + :class:`Headers` stores HTTP headers in a hybrid data structure to provide + efficient insertions and lookups while preserving the original data. + + In order to account for multiple values with minimal hassle, + :class:`Headers` follows this logic: + + - When getting a header with ``headers[name]``: + - if there's no value, :exc:`KeyError` is raised; + - if there's exactly one value, it's returned; + - if there's more than one value, :exc:`MultipleValuesError` is raised. + + - When setting a header with ``headers[name] = value``, the value is + appended to the list of values for that header. + + - When deleting a header with ``del headers[name]``, all values for that + header are removed (this is slow). + + Other methods for manipulating headers are consistent with this logic. + + As long as no header occurs multiple times, :class:`Headers` behaves like + :class:`dict`, except keys are lower-cased to provide case-insensitivity. + + Two methods support manipulating multiple values explicitly: + + - :meth:`get_all` returns a list of all values for a header; + - :meth:`raw_items` returns an iterator of ``(name, values)`` pairs. + + """ + + __slots__ = ["_dict", "_list"] + + # Like dict, Headers accepts an optional "mapping or iterable" argument. + def __init__(self, *args: HeadersLike, **kwargs: str) -> None: + self._dict: Dict[str, List[str]] = {} + self._list: List[Tuple[str, str]] = [] + self.update(*args, **kwargs) + + def __str__(self) -> str: + return "".join(f"{key}: {value}\r\n" for key, value in self._list) + "\r\n" + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self._list!r})" + + def copy(self) -> Headers: + copy = self.__class__() + copy._dict = self._dict.copy() + copy._list = self._list.copy() + return copy + + def serialize(self) -> bytes: + # Since headers only contain ASCII characters, we can keep this simple. + return str(self).encode() + + # Collection methods + + def __contains__(self, key: object) -> bool: + return isinstance(key, str) and key.lower() in self._dict + + def __iter__(self) -> Iterator[str]: + return iter(self._dict) + + def __len__(self) -> int: + return len(self._dict) + + # MutableMapping methods + + def __getitem__(self, key: str) -> str: + value = self._dict[key.lower()] + if len(value) == 1: + return value[0] + else: + raise MultipleValuesError(key) + + def __setitem__(self, key: str, value: str) -> None: + self._dict.setdefault(key.lower(), []).append(value) + self._list.append((key, value)) + + def __delitem__(self, key: str) -> None: + key_lower = key.lower() + self._dict.__delitem__(key_lower) + # This is inefficient. Fortunately deleting HTTP headers is uncommon. + self._list = [(k, v) for k, v in self._list if k.lower() != key_lower] + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Headers): + return NotImplemented + return self._dict == other._dict + + def clear(self) -> None: + """ + Remove all headers. + + """ + self._dict = {} + self._list = [] + + def update(self, *args: HeadersLike, **kwargs: str) -> None: + """ + Update from a :class:`Headers` instance and/or keyword arguments. + + """ + args = tuple( + arg.raw_items() if isinstance(arg, Headers) else arg for arg in args + ) + super().update(*args, **kwargs) + + # Methods for handling multiple values + + def get_all(self, key: str) -> List[str]: + """ + Return the (possibly empty) list of all values for a header. + + Args: + key: header name. + + """ + return self._dict.get(key.lower(), []) + + def raw_items(self) -> Iterator[Tuple[str, str]]: + """ + Return an iterator of all values as ``(name, value)`` pairs. + + """ + return iter(self._list) + + +# copy of _typeshed.SupportsKeysAndGetItem. +class SupportsKeysAndGetItem(Protocol): # pragma: no cover + """ + Dict-like types with ``keys() -> str`` and ``__getitem__(key: str) -> str`` methods. + + """ + + def keys(self) -> Iterable[str]: + ... + + def __getitem__(self, key: str) -> str: + ... + + +HeadersLike = Union[ + Headers, + Mapping[str, str], + Iterable[Tuple[str, str]], + SupportsKeysAndGetItem, +] +""" +Types accepted where :class:`Headers` is expected. + +In addition to :class:`Headers` itself, this includes dict-like types where both +keys and values are :class:`str`. + +""" diff --git a/websockets/exceptions.py b/websockets/exceptions.py new file mode 100644 index 0000000..0f06868 --- /dev/null +++ b/websockets/exceptions.py @@ -0,0 +1,404 @@ +""" +:mod:`websockets.exceptions` defines the following exception hierarchy: + +* :exc:`WebSocketException` + * :exc:`ConnectionClosed` + * :exc:`ConnectionClosedError` + * :exc:`ConnectionClosedOK` + * :exc:`InvalidHandshake` + * :exc:`SecurityError` + * :exc:`InvalidMessage` + * :exc:`InvalidHeader` + * :exc:`InvalidHeaderFormat` + * :exc:`InvalidHeaderValue` + * :exc:`InvalidOrigin` + * :exc:`InvalidUpgrade` + * :exc:`InvalidStatus` + * :exc:`InvalidStatusCode` (legacy) + * :exc:`NegotiationError` + * :exc:`DuplicateParameter` + * :exc:`InvalidParameterName` + * :exc:`InvalidParameterValue` + * :exc:`AbortHandshake` + * :exc:`RedirectHandshake` + * :exc:`InvalidState` + * :exc:`InvalidURI` + * :exc:`PayloadTooBig` + * :exc:`ProtocolError` + +""" + +from __future__ import annotations + +import http +from typing import Optional + +from . import datastructures, frames, http11 + + +__all__ = [ + "WebSocketException", + "ConnectionClosed", + "ConnectionClosedError", + "ConnectionClosedOK", + "InvalidHandshake", + "SecurityError", + "InvalidMessage", + "InvalidHeader", + "InvalidHeaderFormat", + "InvalidHeaderValue", + "InvalidOrigin", + "InvalidUpgrade", + "InvalidStatus", + "InvalidStatusCode", + "NegotiationError", + "DuplicateParameter", + "InvalidParameterName", + "InvalidParameterValue", + "AbortHandshake", + "RedirectHandshake", + "InvalidState", + "InvalidURI", + "PayloadTooBig", + "ProtocolError", + "WebSocketProtocolError", +] + + +class WebSocketException(Exception): + """ + Base class for all exceptions defined by websockets. + + """ + + +class ConnectionClosed(WebSocketException): + """ + Raised when trying to interact with a closed connection. + + Attributes: + rcvd (Optional[Close]): if a close frame was received, its code and + reason are available in ``rcvd.code`` and ``rcvd.reason``. + sent (Optional[Close]): if a close frame was sent, its code and reason + are available in ``sent.code`` and ``sent.reason``. + rcvd_then_sent (Optional[bool]): if close frames were received and + sent, this attribute tells in which order this happened, from the + perspective of this side of the connection. + + """ + + def __init__( + self, + rcvd: Optional[frames.Close], + sent: Optional[frames.Close], + rcvd_then_sent: Optional[bool] = None, + ) -> None: + self.rcvd = rcvd + self.sent = sent + self.rcvd_then_sent = rcvd_then_sent + + def __str__(self) -> str: + if self.rcvd is None: + if self.sent is None: + assert self.rcvd_then_sent is None + return "no close frame received or sent" + else: + assert self.rcvd_then_sent is None + return f"sent {self.sent}; no close frame received" + else: + if self.sent is None: + assert self.rcvd_then_sent is None + return f"received {self.rcvd}; no close frame sent" + else: + assert self.rcvd_then_sent is not None + if self.rcvd_then_sent: + return f"received {self.rcvd}; then sent {self.sent}" + else: + return f"sent {self.sent}; then received {self.rcvd}" + + # code and reason attributes are provided for backwards-compatibility + + @property + def code(self) -> int: + if self.rcvd is None: + return frames.CloseCode.ABNORMAL_CLOSURE + return self.rcvd.code + + @property + def reason(self) -> str: + if self.rcvd is None: + return "" + return self.rcvd.reason + + +class ConnectionClosedError(ConnectionClosed): + """ + Like :exc:`ConnectionClosed`, when the connection terminated with an error. + + A close frame with a code other than 1000 (OK) or 1001 (going away) was + received or sent, or the closing handshake didn't complete properly. + + """ + + +class ConnectionClosedOK(ConnectionClosed): + """ + Like :exc:`ConnectionClosed`, when the connection terminated properly. + + A close code with code 1000 (OK) or 1001 (going away) or without a code was + received and sent. + + """ + + +class InvalidHandshake(WebSocketException): + """ + Raised during the handshake when the WebSocket connection fails. + + """ + + +class SecurityError(InvalidHandshake): + """ + Raised when a handshake request or response breaks a security rule. + + Security limits are hard coded. + + """ + + +class InvalidMessage(InvalidHandshake): + """ + Raised when a handshake request or response is malformed. + + """ + + +class InvalidHeader(InvalidHandshake): + """ + Raised when an HTTP header doesn't have a valid format or value. + + """ + + def __init__(self, name: str, value: Optional[str] = None) -> None: + self.name = name + self.value = value + + def __str__(self) -> str: + if self.value is None: + return f"missing {self.name} header" + elif self.value == "": + return f"empty {self.name} header" + else: + return f"invalid {self.name} header: {self.value}" + + +class InvalidHeaderFormat(InvalidHeader): + """ + Raised when an HTTP header cannot be parsed. + + The format of the header doesn't match the grammar for that header. + + """ + + def __init__(self, name: str, error: str, header: str, pos: int) -> None: + super().__init__(name, f"{error} at {pos} in {header}") + + +class InvalidHeaderValue(InvalidHeader): + """ + Raised when an HTTP header has a wrong value. + + The format of the header is correct but a value isn't acceptable. + + """ + + +class InvalidOrigin(InvalidHeader): + """ + Raised when the Origin header in a request isn't allowed. + + """ + + def __init__(self, origin: Optional[str]) -> None: + super().__init__("Origin", origin) + + +class InvalidUpgrade(InvalidHeader): + """ + Raised when the Upgrade or Connection header isn't correct. + + """ + + +class InvalidStatus(InvalidHandshake): + """ + Raised when a handshake response rejects the WebSocket upgrade. + + """ + + def __init__(self, response: http11.Response) -> None: + self.response = response + + def __str__(self) -> str: + return ( + "server rejected WebSocket connection: " + f"HTTP {self.response.status_code:d}" + ) + + +class InvalidStatusCode(InvalidHandshake): + """ + Raised when a handshake response status code is invalid. + + """ + + def __init__(self, status_code: int, headers: datastructures.Headers) -> None: + self.status_code = status_code + self.headers = headers + + def __str__(self) -> str: + return f"server rejected WebSocket connection: HTTP {self.status_code}" + + +class NegotiationError(InvalidHandshake): + """ + Raised when negotiating an extension fails. + + """ + + +class DuplicateParameter(NegotiationError): + """ + Raised when a parameter name is repeated in an extension header. + + """ + + def __init__(self, name: str) -> None: + self.name = name + + def __str__(self) -> str: + return f"duplicate parameter: {self.name}" + + +class InvalidParameterName(NegotiationError): + """ + Raised when a parameter name in an extension header is invalid. + + """ + + def __init__(self, name: str) -> None: + self.name = name + + def __str__(self) -> str: + return f"invalid parameter name: {self.name}" + + +class InvalidParameterValue(NegotiationError): + """ + Raised when a parameter value in an extension header is invalid. + + """ + + def __init__(self, name: str, value: Optional[str]) -> None: + self.name = name + self.value = value + + def __str__(self) -> str: + if self.value is None: + return f"missing value for parameter {self.name}" + elif self.value == "": + return f"empty value for parameter {self.name}" + else: + return f"invalid value for parameter {self.name}: {self.value}" + + +class AbortHandshake(InvalidHandshake): + """ + Raised to abort the handshake on purpose and return an HTTP response. + + This exception is an implementation detail. + + The public API + is :meth:`~websockets.server.WebSocketServerProtocol.process_request`. + + Attributes: + status (~http.HTTPStatus): HTTP status code. + headers (Headers): HTTP response headers. + body (bytes): HTTP response body. + """ + + def __init__( + self, + status: http.HTTPStatus, + headers: datastructures.HeadersLike, + body: bytes = b"", + ) -> None: + # If a user passes an int instead of a HTTPStatus, fix it automatically. + self.status = http.HTTPStatus(status) + self.headers = datastructures.Headers(headers) + self.body = body + + def __str__(self) -> str: + return ( + f"HTTP {self.status:d}, " + f"{len(self.headers)} headers, " + f"{len(self.body)} bytes" + ) + + +class RedirectHandshake(InvalidHandshake): + """ + Raised when a handshake gets redirected. + + This exception is an implementation detail. + + """ + + def __init__(self, uri: str) -> None: + self.uri = uri + + def __str__(self) -> str: + return f"redirect to {self.uri}" + + +class InvalidState(WebSocketException, AssertionError): + """ + Raised when an operation is forbidden in the current state. + + This exception is an implementation detail. + + It should never be raised in normal circumstances. + + """ + + +class InvalidURI(WebSocketException): + """ + Raised when connecting to a URI that isn't a valid WebSocket URI. + + """ + + def __init__(self, uri: str, msg: str) -> None: + self.uri = uri + self.msg = msg + + def __str__(self) -> str: + return f"{self.uri} isn't a valid URI: {self.msg}" + + +class PayloadTooBig(WebSocketException): + """ + Raised when receiving a frame with a payload exceeding the maximum size. + + """ + + +class ProtocolError(WebSocketException): + """ + Raised when a frame breaks the protocol. + + """ + + +WebSocketProtocolError = ProtocolError # for backwards compatibility diff --git a/websockets/extensions/__init__.py b/websockets/extensions/__init__.py new file mode 100644 index 0000000..02838b9 --- /dev/null +++ b/websockets/extensions/__init__.py @@ -0,0 +1,4 @@ +from .base import * + + +__all__ = ["Extension", "ClientExtensionFactory", "ServerExtensionFactory"] diff --git a/websockets/extensions/base.py b/websockets/extensions/base.py new file mode 100644 index 0000000..6c481a4 --- /dev/null +++ b/websockets/extensions/base.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +from typing import List, Optional, Sequence, Tuple + +from .. import frames +from ..typing import ExtensionName, ExtensionParameter + + +__all__ = ["Extension", "ClientExtensionFactory", "ServerExtensionFactory"] + + +class Extension: + """ + Base class for extensions. + + """ + + name: ExtensionName + """Extension identifier.""" + + def decode( + self, + frame: frames.Frame, + *, + max_size: Optional[int] = None, + ) -> frames.Frame: + """ + Decode an incoming frame. + + Args: + frame (Frame): incoming frame. + max_size: maximum payload size in bytes. + + Returns: + Frame: Decoded frame. + + Raises: + PayloadTooBig: if decoding the payload exceeds ``max_size``. + + """ + raise NotImplementedError + + def encode(self, frame: frames.Frame) -> frames.Frame: + """ + Encode an outgoing frame. + + Args: + frame (Frame): outgoing frame. + + Returns: + Frame: Encoded frame. + + """ + raise NotImplementedError + + +class ClientExtensionFactory: + """ + Base class for client-side extension factories. + + """ + + name: ExtensionName + """Extension identifier.""" + + def get_request_params(self) -> List[ExtensionParameter]: + """ + Build parameters to send to the server for this extension. + + Returns: + List[ExtensionParameter]: Parameters to send to the server. + + """ + raise NotImplementedError + + def process_response_params( + self, + params: Sequence[ExtensionParameter], + accepted_extensions: Sequence[Extension], + ) -> Extension: + """ + Process parameters received from the server. + + Args: + params (Sequence[ExtensionParameter]): parameters received from + the server for this extension. + accepted_extensions (Sequence[Extension]): list of previously + accepted extensions. + + Returns: + Extension: An extension instance. + + Raises: + NegotiationError: if parameters aren't acceptable. + + """ + raise NotImplementedError + + +class ServerExtensionFactory: + """ + Base class for server-side extension factories. + + """ + + name: ExtensionName + """Extension identifier.""" + + def process_request_params( + self, + params: Sequence[ExtensionParameter], + accepted_extensions: Sequence[Extension], + ) -> Tuple[List[ExtensionParameter], Extension]: + """ + Process parameters received from the client. + + Args: + params (Sequence[ExtensionParameter]): parameters received from + the client for this extension. + accepted_extensions (Sequence[Extension]): list of previously + accepted extensions. + + Returns: + Tuple[List[ExtensionParameter], Extension]: To accept the offer, + parameters to send to the client for this extension and an + extension instance. + + Raises: + NegotiationError: to reject the offer, if parameters received from + the client aren't acceptable. + + """ + raise NotImplementedError diff --git a/websockets/extensions/permessage_deflate.py b/websockets/extensions/permessage_deflate.py new file mode 100644 index 0000000..b391837 --- /dev/null +++ b/websockets/extensions/permessage_deflate.py @@ -0,0 +1,660 @@ +from __future__ import annotations + +import dataclasses +import zlib +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union + +from .. import exceptions, frames +from ..typing import ExtensionName, ExtensionParameter +from .base import ClientExtensionFactory, Extension, ServerExtensionFactory + + +__all__ = [ + "PerMessageDeflate", + "ClientPerMessageDeflateFactory", + "enable_client_permessage_deflate", + "ServerPerMessageDeflateFactory", + "enable_server_permessage_deflate", +] + +_EMPTY_UNCOMPRESSED_BLOCK = b"\x00\x00\xff\xff" + +_MAX_WINDOW_BITS_VALUES = [str(bits) for bits in range(8, 16)] + + +class PerMessageDeflate(Extension): + """ + Per-Message Deflate extension. + + """ + + name = ExtensionName("permessage-deflate") + + def __init__( + self, + remote_no_context_takeover: bool, + local_no_context_takeover: bool, + remote_max_window_bits: int, + local_max_window_bits: int, + compress_settings: Optional[Dict[Any, Any]] = None, + ) -> None: + """ + Configure the Per-Message Deflate extension. + + """ + if compress_settings is None: + compress_settings = {} + + assert remote_no_context_takeover in [False, True] + assert local_no_context_takeover in [False, True] + assert 8 <= remote_max_window_bits <= 15 + assert 8 <= local_max_window_bits <= 15 + assert "wbits" not in compress_settings + + self.remote_no_context_takeover = remote_no_context_takeover + self.local_no_context_takeover = local_no_context_takeover + self.remote_max_window_bits = remote_max_window_bits + self.local_max_window_bits = local_max_window_bits + self.compress_settings = compress_settings + + if not self.remote_no_context_takeover: + self.decoder = zlib.decompressobj(wbits=-self.remote_max_window_bits) + + if not self.local_no_context_takeover: + self.encoder = zlib.compressobj( + wbits=-self.local_max_window_bits, **self.compress_settings + ) + + # To handle continuation frames properly, we must keep track of + # whether that initial frame was encoded. + self.decode_cont_data = False + # There's no need for self.encode_cont_data because we always encode + # outgoing frames, so it would always be True. + + def __repr__(self) -> str: + return ( + f"PerMessageDeflate(" + f"remote_no_context_takeover={self.remote_no_context_takeover}, " + f"local_no_context_takeover={self.local_no_context_takeover}, " + f"remote_max_window_bits={self.remote_max_window_bits}, " + f"local_max_window_bits={self.local_max_window_bits})" + ) + + def decode( + self, + frame: frames.Frame, + *, + max_size: Optional[int] = None, + ) -> frames.Frame: + """ + Decode an incoming frame. + + """ + # Skip control frames. + if frame.opcode in frames.CTRL_OPCODES: + return frame + + # Handle continuation data frames: + # - skip if the message isn't encoded + # - reset "decode continuation data" flag if it's a final frame + if frame.opcode is frames.OP_CONT: + if not self.decode_cont_data: + return frame + if frame.fin: + self.decode_cont_data = False + + # Handle text and binary data frames: + # - skip if the message isn't encoded + # - unset the rsv1 flag on the first frame of a compressed message + # - set "decode continuation data" flag if it's a non-final frame + else: + if not frame.rsv1: + return frame + frame = dataclasses.replace(frame, rsv1=False) + if not frame.fin: + self.decode_cont_data = True + + # Re-initialize per-message decoder. + if self.remote_no_context_takeover: + self.decoder = zlib.decompressobj(wbits=-self.remote_max_window_bits) + + # Uncompress data. Protect against zip bombs by preventing zlib from + # decompressing more than max_length bytes (except when the limit is + # disabled with max_size = None). + data = frame.data + if frame.fin: + data += _EMPTY_UNCOMPRESSED_BLOCK + max_length = 0 if max_size is None else max_size + try: + data = self.decoder.decompress(data, max_length) + except zlib.error as exc: + raise exceptions.ProtocolError("decompression failed") from exc + if self.decoder.unconsumed_tail: + raise exceptions.PayloadTooBig(f"over size limit (? > {max_size} bytes)") + + # Allow garbage collection of the decoder if it won't be reused. + if frame.fin and self.remote_no_context_takeover: + del self.decoder + + return dataclasses.replace(frame, data=data) + + def encode(self, frame: frames.Frame) -> frames.Frame: + """ + Encode an outgoing frame. + + """ + # Skip control frames. + if frame.opcode in frames.CTRL_OPCODES: + return frame + + # Since we always encode messages, there's no "encode continuation + # data" flag similar to "decode continuation data" at this time. + + if frame.opcode is not frames.OP_CONT: + # Set the rsv1 flag on the first frame of a compressed message. + frame = dataclasses.replace(frame, rsv1=True) + # Re-initialize per-message decoder. + if self.local_no_context_takeover: + self.encoder = zlib.compressobj( + wbits=-self.local_max_window_bits, **self.compress_settings + ) + + # Compress data. + data = self.encoder.compress(frame.data) + self.encoder.flush(zlib.Z_SYNC_FLUSH) + if frame.fin and data.endswith(_EMPTY_UNCOMPRESSED_BLOCK): + data = data[:-4] + + # Allow garbage collection of the encoder if it won't be reused. + if frame.fin and self.local_no_context_takeover: + del self.encoder + + return dataclasses.replace(frame, data=data) + + +def _build_parameters( + server_no_context_takeover: bool, + client_no_context_takeover: bool, + server_max_window_bits: Optional[int], + client_max_window_bits: Optional[Union[int, bool]], +) -> List[ExtensionParameter]: + """ + Build a list of ``(name, value)`` pairs for some compression parameters. + + """ + params: List[ExtensionParameter] = [] + if server_no_context_takeover: + params.append(("server_no_context_takeover", None)) + if client_no_context_takeover: + params.append(("client_no_context_takeover", None)) + if server_max_window_bits: + params.append(("server_max_window_bits", str(server_max_window_bits))) + if client_max_window_bits is True: # only in handshake requests + params.append(("client_max_window_bits", None)) + elif client_max_window_bits: + params.append(("client_max_window_bits", str(client_max_window_bits))) + return params + + +def _extract_parameters( + params: Sequence[ExtensionParameter], *, is_server: bool +) -> Tuple[bool, bool, Optional[int], Optional[Union[int, bool]]]: + """ + Extract compression parameters from a list of ``(name, value)`` pairs. + + If ``is_server`` is :obj:`True`, ``client_max_window_bits`` may be + provided without a value. This is only allowed in handshake requests. + + """ + server_no_context_takeover: bool = False + client_no_context_takeover: bool = False + server_max_window_bits: Optional[int] = None + client_max_window_bits: Optional[Union[int, bool]] = None + + for name, value in params: + if name == "server_no_context_takeover": + if server_no_context_takeover: + raise exceptions.DuplicateParameter(name) + if value is None: + server_no_context_takeover = True + else: + raise exceptions.InvalidParameterValue(name, value) + + elif name == "client_no_context_takeover": + if client_no_context_takeover: + raise exceptions.DuplicateParameter(name) + if value is None: + client_no_context_takeover = True + else: + raise exceptions.InvalidParameterValue(name, value) + + elif name == "server_max_window_bits": + if server_max_window_bits is not None: + raise exceptions.DuplicateParameter(name) + if value in _MAX_WINDOW_BITS_VALUES: + server_max_window_bits = int(value) + else: + raise exceptions.InvalidParameterValue(name, value) + + elif name == "client_max_window_bits": + if client_max_window_bits is not None: + raise exceptions.DuplicateParameter(name) + if is_server and value is None: # only in handshake requests + client_max_window_bits = True + elif value in _MAX_WINDOW_BITS_VALUES: + client_max_window_bits = int(value) + else: + raise exceptions.InvalidParameterValue(name, value) + + else: + raise exceptions.InvalidParameterName(name) + + return ( + server_no_context_takeover, + client_no_context_takeover, + server_max_window_bits, + client_max_window_bits, + ) + + +class ClientPerMessageDeflateFactory(ClientExtensionFactory): + """ + Client-side extension factory for the Per-Message Deflate extension. + + Parameters behave as described in `section 7.1 of RFC 7692`_. + + .. _section 7.1 of RFC 7692: https://www.rfc-editor.org/rfc/rfc7692.html#section-7.1 + + Set them to :obj:`True` to include them in the negotiation offer without a + value or to an integer value to include them with this value. + + Args: + server_no_context_takeover: prevent server from using context takeover. + client_no_context_takeover: prevent client from using context takeover. + server_max_window_bits: maximum size of the server's LZ77 sliding window + in bits, between 8 and 15. + client_max_window_bits: maximum size of the client's LZ77 sliding window + in bits, between 8 and 15, or :obj:`True` to indicate support without + setting a limit. + compress_settings: additional keyword arguments for :func:`zlib.compressobj`, + excluding ``wbits``. + + """ + + name = ExtensionName("permessage-deflate") + + def __init__( + self, + server_no_context_takeover: bool = False, + client_no_context_takeover: bool = False, + server_max_window_bits: Optional[int] = None, + client_max_window_bits: Optional[Union[int, bool]] = True, + compress_settings: Optional[Dict[str, Any]] = None, + ) -> None: + """ + Configure the Per-Message Deflate extension factory. + + """ + if not (server_max_window_bits is None or 8 <= server_max_window_bits <= 15): + raise ValueError("server_max_window_bits must be between 8 and 15") + if not ( + client_max_window_bits is None + or client_max_window_bits is True + or 8 <= client_max_window_bits <= 15 + ): + raise ValueError("client_max_window_bits must be between 8 and 15") + if compress_settings is not None and "wbits" in compress_settings: + raise ValueError( + "compress_settings must not include wbits, " + "set client_max_window_bits instead" + ) + + self.server_no_context_takeover = server_no_context_takeover + self.client_no_context_takeover = client_no_context_takeover + self.server_max_window_bits = server_max_window_bits + self.client_max_window_bits = client_max_window_bits + self.compress_settings = compress_settings + + def get_request_params(self) -> List[ExtensionParameter]: + """ + Build request parameters. + + """ + return _build_parameters( + self.server_no_context_takeover, + self.client_no_context_takeover, + self.server_max_window_bits, + self.client_max_window_bits, + ) + + def process_response_params( + self, + params: Sequence[ExtensionParameter], + accepted_extensions: Sequence[Extension], + ) -> PerMessageDeflate: + """ + Process response parameters. + + Return an extension instance. + + """ + if any(other.name == self.name for other in accepted_extensions): + raise exceptions.NegotiationError(f"received duplicate {self.name}") + + # Request parameters are available in instance variables. + + # Load response parameters in local variables. + ( + server_no_context_takeover, + client_no_context_takeover, + server_max_window_bits, + client_max_window_bits, + ) = _extract_parameters(params, is_server=False) + + # After comparing the request and the response, the final + # configuration must be available in the local variables. + + # server_no_context_takeover + # + # Req. Resp. Result + # ------ ------ -------------------------------------------------- + # False False False + # False True True + # True False Error! + # True True True + + if self.server_no_context_takeover: + if not server_no_context_takeover: + raise exceptions.NegotiationError("expected server_no_context_takeover") + + # client_no_context_takeover + # + # Req. Resp. Result + # ------ ------ -------------------------------------------------- + # False False False + # False True True + # True False True - must change value + # True True True + + if self.client_no_context_takeover: + if not client_no_context_takeover: + client_no_context_takeover = True + + # server_max_window_bits + + # Req. Resp. Result + # ------ ------ -------------------------------------------------- + # None None None + # None 8≤M≤15 M + # 8≤N≤15 None Error! + # 8≤N≤15 8≤M≤N M + # 8≤N≤15 N self.server_max_window_bits: + raise exceptions.NegotiationError("unsupported server_max_window_bits") + + # client_max_window_bits + + # Req. Resp. Result + # ------ ------ -------------------------------------------------- + # None None None + # None 8≤M≤15 Error! + # True None None + # True 8≤M≤15 M + # 8≤N≤15 None N - must change value + # 8≤N≤15 8≤M≤N M + # 8≤N≤15 N self.client_max_window_bits: + raise exceptions.NegotiationError("unsupported client_max_window_bits") + + return PerMessageDeflate( + server_no_context_takeover, # remote_no_context_takeover + client_no_context_takeover, # local_no_context_takeover + server_max_window_bits or 15, # remote_max_window_bits + client_max_window_bits or 15, # local_max_window_bits + self.compress_settings, + ) + + +def enable_client_permessage_deflate( + extensions: Optional[Sequence[ClientExtensionFactory]], +) -> Sequence[ClientExtensionFactory]: + """ + Enable Per-Message Deflate with default settings in client extensions. + + If the extension is already present, perhaps with non-default settings, + the configuration isn't changed. + + """ + if extensions is None: + extensions = [] + if not any( + extension_factory.name == ClientPerMessageDeflateFactory.name + for extension_factory in extensions + ): + extensions = list(extensions) + [ + ClientPerMessageDeflateFactory( + compress_settings={"memLevel": 5}, + ) + ] + return extensions + + +class ServerPerMessageDeflateFactory(ServerExtensionFactory): + """ + Server-side extension factory for the Per-Message Deflate extension. + + Parameters behave as described in `section 7.1 of RFC 7692`_. + + .. _section 7.1 of RFC 7692: https://www.rfc-editor.org/rfc/rfc7692.html#section-7.1 + + Set them to :obj:`True` to include them in the negotiation offer without a + value or to an integer value to include them with this value. + + Args: + server_no_context_takeover: prevent server from using context takeover. + client_no_context_takeover: prevent client from using context takeover. + server_max_window_bits: maximum size of the server's LZ77 sliding window + in bits, between 8 and 15. + client_max_window_bits: maximum size of the client's LZ77 sliding window + in bits, between 8 and 15. + compress_settings: additional keyword arguments for :func:`zlib.compressobj`, + excluding ``wbits``. + require_client_max_window_bits: do not enable compression at all if + client doesn't advertise support for ``client_max_window_bits``; + the default behavior is to enable compression without enforcing + ``client_max_window_bits``. + + """ + + name = ExtensionName("permessage-deflate") + + def __init__( + self, + server_no_context_takeover: bool = False, + client_no_context_takeover: bool = False, + server_max_window_bits: Optional[int] = None, + client_max_window_bits: Optional[int] = None, + compress_settings: Optional[Dict[str, Any]] = None, + require_client_max_window_bits: bool = False, + ) -> None: + """ + Configure the Per-Message Deflate extension factory. + + """ + if not (server_max_window_bits is None or 8 <= server_max_window_bits <= 15): + raise ValueError("server_max_window_bits must be between 8 and 15") + if not (client_max_window_bits is None or 8 <= client_max_window_bits <= 15): + raise ValueError("client_max_window_bits must be between 8 and 15") + if compress_settings is not None and "wbits" in compress_settings: + raise ValueError( + "compress_settings must not include wbits, " + "set server_max_window_bits instead" + ) + if client_max_window_bits is None and require_client_max_window_bits: + raise ValueError( + "require_client_max_window_bits is enabled, " + "but client_max_window_bits isn't configured" + ) + + self.server_no_context_takeover = server_no_context_takeover + self.client_no_context_takeover = client_no_context_takeover + self.server_max_window_bits = server_max_window_bits + self.client_max_window_bits = client_max_window_bits + self.compress_settings = compress_settings + self.require_client_max_window_bits = require_client_max_window_bits + + def process_request_params( + self, + params: Sequence[ExtensionParameter], + accepted_extensions: Sequence[Extension], + ) -> Tuple[List[ExtensionParameter], PerMessageDeflate]: + """ + Process request parameters. + + Return response params and an extension instance. + + """ + if any(other.name == self.name for other in accepted_extensions): + raise exceptions.NegotiationError(f"skipped duplicate {self.name}") + + # Load request parameters in local variables. + ( + server_no_context_takeover, + client_no_context_takeover, + server_max_window_bits, + client_max_window_bits, + ) = _extract_parameters(params, is_server=True) + + # Configuration parameters are available in instance variables. + + # After comparing the request and the configuration, the response must + # be available in the local variables. + + # server_no_context_takeover + # + # Config Req. Resp. + # ------ ------ -------------------------------------------------- + # False False False + # False True True + # True False True - must change value to True + # True True True + + if self.server_no_context_takeover: + if not server_no_context_takeover: + server_no_context_takeover = True + + # client_no_context_takeover + # + # Config Req. Resp. + # ------ ------ -------------------------------------------------- + # False False False + # False True True (or False) + # True False True - must change value to True + # True True True (or False) + + if self.client_no_context_takeover: + if not client_no_context_takeover: + client_no_context_takeover = True + + # server_max_window_bits + + # Config Req. Resp. + # ------ ------ -------------------------------------------------- + # None None None + # None 8≤M≤15 M + # 8≤N≤15 None N - must change value + # 8≤N≤15 8≤M≤N M + # 8≤N≤15 N self.server_max_window_bits: + server_max_window_bits = self.server_max_window_bits + + # client_max_window_bits + + # Config Req. Resp. + # ------ ------ -------------------------------------------------- + # None None None + # None True None - must change value + # None 8≤M≤15 M (or None) + # 8≤N≤15 None None or Error! + # 8≤N≤15 True N - must change value + # 8≤N≤15 8≤M≤N M (or None) + # 8≤N≤15 N Sequence[ServerExtensionFactory]: + """ + Enable Per-Message Deflate with default settings in server extensions. + + If the extension is already present, perhaps with non-default settings, + the configuration isn't changed. + + """ + if extensions is None: + extensions = [] + if not any( + ext_factory.name == ServerPerMessageDeflateFactory.name + for ext_factory in extensions + ): + extensions = list(extensions) + [ + ServerPerMessageDeflateFactory( + server_max_window_bits=12, + client_max_window_bits=12, + compress_settings={"memLevel": 5}, + ) + ] + return extensions diff --git a/websockets/frames.py b/websockets/frames.py new file mode 100644 index 0000000..6b1befb --- /dev/null +++ b/websockets/frames.py @@ -0,0 +1,470 @@ +from __future__ import annotations + +import dataclasses +import enum +import io +import secrets +import struct +from typing import Callable, Generator, Optional, Sequence, Tuple + +from . import exceptions, extensions +from .typing import Data + + +try: + from .speedups import apply_mask +except ImportError: + from .utils import apply_mask + + +__all__ = [ + "Opcode", + "OP_CONT", + "OP_TEXT", + "OP_BINARY", + "OP_CLOSE", + "OP_PING", + "OP_PONG", + "DATA_OPCODES", + "CTRL_OPCODES", + "Frame", + "prepare_data", + "prepare_ctrl", + "Close", +] + + +class Opcode(enum.IntEnum): + """Opcode values for WebSocket frames.""" + + CONT, TEXT, BINARY = 0x00, 0x01, 0x02 + CLOSE, PING, PONG = 0x08, 0x09, 0x0A + + +OP_CONT = Opcode.CONT +OP_TEXT = Opcode.TEXT +OP_BINARY = Opcode.BINARY +OP_CLOSE = Opcode.CLOSE +OP_PING = Opcode.PING +OP_PONG = Opcode.PONG + +DATA_OPCODES = OP_CONT, OP_TEXT, OP_BINARY +CTRL_OPCODES = OP_CLOSE, OP_PING, OP_PONG + + +class CloseCode(enum.IntEnum): + """Close code values for WebSocket close frames.""" + + NORMAL_CLOSURE = 1000 + GOING_AWAY = 1001 + PROTOCOL_ERROR = 1002 + UNSUPPORTED_DATA = 1003 + # 1004 is reserved + NO_STATUS_RCVD = 1005 + ABNORMAL_CLOSURE = 1006 + INVALID_DATA = 1007 + POLICY_VIOLATION = 1008 + MESSAGE_TOO_BIG = 1009 + MANDATORY_EXTENSION = 1010 + INTERNAL_ERROR = 1011 + SERVICE_RESTART = 1012 + TRY_AGAIN_LATER = 1013 + BAD_GATEWAY = 1014 + TLS_HANDSHAKE = 1015 + + +# See https://www.iana.org/assignments/websocket/websocket.xhtml +CLOSE_CODE_EXPLANATIONS: dict[int, str] = { + CloseCode.NORMAL_CLOSURE: "OK", + CloseCode.GOING_AWAY: "going away", + CloseCode.PROTOCOL_ERROR: "protocol error", + CloseCode.UNSUPPORTED_DATA: "unsupported data", + CloseCode.NO_STATUS_RCVD: "no status received [internal]", + CloseCode.ABNORMAL_CLOSURE: "abnormal closure [internal]", + CloseCode.INVALID_DATA: "invalid frame payload data", + CloseCode.POLICY_VIOLATION: "policy violation", + CloseCode.MESSAGE_TOO_BIG: "message too big", + CloseCode.MANDATORY_EXTENSION: "mandatory extension", + CloseCode.INTERNAL_ERROR: "internal error", + CloseCode.SERVICE_RESTART: "service restart", + CloseCode.TRY_AGAIN_LATER: "try again later", + CloseCode.BAD_GATEWAY: "bad gateway", + CloseCode.TLS_HANDSHAKE: "TLS handshake failure [internal]", +} + + +# Close code that are allowed in a close frame. +# Using a set optimizes `code in EXTERNAL_CLOSE_CODES`. +EXTERNAL_CLOSE_CODES = { + CloseCode.NORMAL_CLOSURE, + CloseCode.GOING_AWAY, + CloseCode.PROTOCOL_ERROR, + CloseCode.UNSUPPORTED_DATA, + CloseCode.INVALID_DATA, + CloseCode.POLICY_VIOLATION, + CloseCode.MESSAGE_TOO_BIG, + CloseCode.MANDATORY_EXTENSION, + CloseCode.INTERNAL_ERROR, + CloseCode.SERVICE_RESTART, + CloseCode.TRY_AGAIN_LATER, + CloseCode.BAD_GATEWAY, +} + + +OK_CLOSE_CODES = { + CloseCode.NORMAL_CLOSURE, + CloseCode.GOING_AWAY, + CloseCode.NO_STATUS_RCVD, +} + + +BytesLike = bytes, bytearray, memoryview + + +@dataclasses.dataclass +class Frame: + """ + WebSocket frame. + + Attributes: + opcode: Opcode. + data: Payload data. + fin: FIN bit. + rsv1: RSV1 bit. + rsv2: RSV2 bit. + rsv3: RSV3 bit. + + Only these fields are needed. The MASK bit, payload length and masking-key + are handled on the fly when parsing and serializing frames. + + """ + + opcode: Opcode + data: bytes + fin: bool = True + rsv1: bool = False + rsv2: bool = False + rsv3: bool = False + + def __str__(self) -> str: + """ + Return a human-readable representation of a frame. + + """ + coding = None + length = f"{len(self.data)} byte{'' if len(self.data) == 1 else 's'}" + non_final = "" if self.fin else "continued" + + if self.opcode is OP_TEXT: + # Decoding only the beginning and the end is needlessly hard. + # Decode the entire payload then elide later if necessary. + data = repr(self.data.decode()) + elif self.opcode is OP_BINARY: + # We'll show at most the first 16 bytes and the last 8 bytes. + # Encode just what we need, plus two dummy bytes to elide later. + binary = self.data + if len(binary) > 25: + binary = b"".join([binary[:16], b"\x00\x00", binary[-8:]]) + data = " ".join(f"{byte:02x}" for byte in binary) + elif self.opcode is OP_CLOSE: + data = str(Close.parse(self.data)) + elif self.data: + # We don't know if a Continuation frame contains text or binary. + # Ping and Pong frames could contain UTF-8. + # Attempt to decode as UTF-8 and display it as text; fallback to + # binary. If self.data is a memoryview, it has no decode() method, + # which raises AttributeError. + try: + data = repr(self.data.decode()) + coding = "text" + except (UnicodeDecodeError, AttributeError): + binary = self.data + if len(binary) > 25: + binary = b"".join([binary[:16], b"\x00\x00", binary[-8:]]) + data = " ".join(f"{byte:02x}" for byte in binary) + coding = "binary" + else: + data = "''" + + if len(data) > 75: + data = data[:48] + "..." + data[-24:] + + metadata = ", ".join(filter(None, [coding, length, non_final])) + + return f"{self.opcode.name} {data} [{metadata}]" + + @classmethod + def parse( + cls, + read_exact: Callable[[int], Generator[None, None, bytes]], + *, + mask: bool, + max_size: Optional[int] = None, + extensions: Optional[Sequence[extensions.Extension]] = None, + ) -> Generator[None, None, Frame]: + """ + Parse a WebSocket frame. + + This is a generator-based coroutine. + + Args: + read_exact: generator-based coroutine that reads the requested + bytes or raises an exception if there isn't enough data. + mask: whether the frame should be masked i.e. whether the read + happens on the server side. + max_size: maximum payload size in bytes. + extensions: list of extensions, applied in reverse order. + + Raises: + EOFError: if the connection is closed without a full WebSocket frame. + UnicodeDecodeError: if the frame contains invalid UTF-8. + PayloadTooBig: if the frame's payload size exceeds ``max_size``. + ProtocolError: if the frame contains incorrect values. + + """ + # Read the header. + data = yield from read_exact(2) + head1, head2 = struct.unpack("!BB", data) + + # While not Pythonic, this is marginally faster than calling bool(). + fin = True if head1 & 0b10000000 else False + rsv1 = True if head1 & 0b01000000 else False + rsv2 = True if head1 & 0b00100000 else False + rsv3 = True if head1 & 0b00010000 else False + + try: + opcode = Opcode(head1 & 0b00001111) + except ValueError as exc: + raise exceptions.ProtocolError("invalid opcode") from exc + + if (True if head2 & 0b10000000 else False) != mask: + raise exceptions.ProtocolError("incorrect masking") + + length = head2 & 0b01111111 + if length == 126: + data = yield from read_exact(2) + (length,) = struct.unpack("!H", data) + elif length == 127: + data = yield from read_exact(8) + (length,) = struct.unpack("!Q", data) + if max_size is not None and length > max_size: + raise exceptions.PayloadTooBig( + f"over size limit ({length} > {max_size} bytes)" + ) + if mask: + mask_bytes = yield from read_exact(4) + + # Read the data. + data = yield from read_exact(length) + if mask: + data = apply_mask(data, mask_bytes) + + frame = cls(opcode, data, fin, rsv1, rsv2, rsv3) + + if extensions is None: + extensions = [] + for extension in reversed(extensions): + frame = extension.decode(frame, max_size=max_size) + + frame.check() + + return frame + + def serialize( + self, + *, + mask: bool, + extensions: Optional[Sequence[extensions.Extension]] = None, + ) -> bytes: + """ + Serialize a WebSocket frame. + + Args: + mask: whether the frame should be masked i.e. whether the write + happens on the client side. + extensions: list of extensions, applied in order. + + Raises: + ProtocolError: if the frame contains incorrect values. + + """ + self.check() + + if extensions is None: + extensions = [] + for extension in extensions: + self = extension.encode(self) + + output = io.BytesIO() + + # Prepare the header. + head1 = ( + (0b10000000 if self.fin else 0) + | (0b01000000 if self.rsv1 else 0) + | (0b00100000 if self.rsv2 else 0) + | (0b00010000 if self.rsv3 else 0) + | self.opcode + ) + + head2 = 0b10000000 if mask else 0 + + length = len(self.data) + if length < 126: + output.write(struct.pack("!BB", head1, head2 | length)) + elif length < 65536: + output.write(struct.pack("!BBH", head1, head2 | 126, length)) + else: + output.write(struct.pack("!BBQ", head1, head2 | 127, length)) + + if mask: + mask_bytes = secrets.token_bytes(4) + output.write(mask_bytes) + + # Prepare the data. + if mask: + data = apply_mask(self.data, mask_bytes) + else: + data = self.data + output.write(data) + + return output.getvalue() + + def check(self) -> None: + """ + Check that reserved bits and opcode have acceptable values. + + Raises: + ProtocolError: if a reserved bit or the opcode is invalid. + + """ + if self.rsv1 or self.rsv2 or self.rsv3: + raise exceptions.ProtocolError("reserved bits must be 0") + + if self.opcode in CTRL_OPCODES: + if len(self.data) > 125: + raise exceptions.ProtocolError("control frame too long") + if not self.fin: + raise exceptions.ProtocolError("fragmented control frame") + + +def prepare_data(data: Data) -> Tuple[int, bytes]: + """ + Convert a string or byte-like object to an opcode and a bytes-like object. + + This function is designed for data frames. + + If ``data`` is a :class:`str`, return ``OP_TEXT`` and a :class:`bytes` + object encoding ``data`` in UTF-8. + + If ``data`` is a bytes-like object, return ``OP_BINARY`` and a bytes-like + object. + + Raises: + TypeError: if ``data`` doesn't have a supported type. + + """ + if isinstance(data, str): + return OP_TEXT, data.encode("utf-8") + elif isinstance(data, BytesLike): + return OP_BINARY, data + else: + raise TypeError("data must be str or bytes-like") + + +def prepare_ctrl(data: Data) -> bytes: + """ + Convert a string or byte-like object to bytes. + + This function is designed for ping and pong frames. + + If ``data`` is a :class:`str`, return a :class:`bytes` object encoding + ``data`` in UTF-8. + + If ``data`` is a bytes-like object, return a :class:`bytes` object. + + Raises: + TypeError: if ``data`` doesn't have a supported type. + + """ + if isinstance(data, str): + return data.encode("utf-8") + elif isinstance(data, BytesLike): + return bytes(data) + else: + raise TypeError("data must be str or bytes-like") + + +@dataclasses.dataclass +class Close: + """ + Code and reason for WebSocket close frames. + + Attributes: + code: Close code. + reason: Close reason. + + """ + + code: int + reason: str + + def __str__(self) -> str: + """ + Return a human-readable representation of a close code and reason. + + """ + if 3000 <= self.code < 4000: + explanation = "registered" + elif 4000 <= self.code < 5000: + explanation = "private use" + else: + explanation = CLOSE_CODE_EXPLANATIONS.get(self.code, "unknown") + result = f"{self.code} ({explanation})" + + if self.reason: + result = f"{result} {self.reason}" + + return result + + @classmethod + def parse(cls, data: bytes) -> Close: + """ + Parse the payload of a close frame. + + Args: + data: payload of the close frame. + + Raises: + ProtocolError: if data is ill-formed. + UnicodeDecodeError: if the reason isn't valid UTF-8. + + """ + if len(data) >= 2: + (code,) = struct.unpack("!H", data[:2]) + reason = data[2:].decode("utf-8") + close = cls(code, reason) + close.check() + return close + elif len(data) == 0: + return cls(CloseCode.NO_STATUS_RCVD, "") + else: + raise exceptions.ProtocolError("close frame too short") + + def serialize(self) -> bytes: + """ + Serialize the payload of a close frame. + + """ + self.check() + return struct.pack("!H", self.code) + self.reason.encode("utf-8") + + def check(self) -> None: + """ + Check that the close code has a valid value for a close frame. + + Raises: + ProtocolError: if the close code is invalid. + + """ + if not (self.code in EXTERNAL_CLOSE_CODES or 3000 <= self.code < 5000): + raise exceptions.ProtocolError("invalid status code") diff --git a/websockets/headers.py b/websockets/headers.py new file mode 100644 index 0000000..9ae3035 --- /dev/null +++ b/websockets/headers.py @@ -0,0 +1,587 @@ +from __future__ import annotations + +import base64 +import binascii +import ipaddress +import re +from typing import Callable, List, Optional, Sequence, Tuple, TypeVar, cast + +from . import exceptions +from .typing import ( + ConnectionOption, + ExtensionHeader, + ExtensionName, + ExtensionParameter, + Subprotocol, + UpgradeProtocol, +) + + +__all__ = [ + "build_host", + "parse_connection", + "parse_upgrade", + "parse_extension", + "build_extension", + "parse_subprotocol", + "build_subprotocol", + "validate_subprotocols", + "build_www_authenticate_basic", + "parse_authorization_basic", + "build_authorization_basic", +] + + +T = TypeVar("T") + + +def build_host(host: str, port: int, secure: bool) -> str: + """ + Build a ``Host`` header. + + """ + # https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.2 + # IPv6 addresses must be enclosed in brackets. + try: + address = ipaddress.ip_address(host) + except ValueError: + # host is a hostname + pass + else: + # host is an IP address + if address.version == 6: + host = f"[{host}]" + + if port != (443 if secure else 80): + host = f"{host}:{port}" + + return host + + +# To avoid a dependency on a parsing library, we implement manually the ABNF +# described in https://www.rfc-editor.org/rfc/rfc6455.html#section-9.1 and +# https://www.rfc-editor.org/rfc/rfc7230.html#appendix-B. + + +def peek_ahead(header: str, pos: int) -> Optional[str]: + """ + Return the next character from ``header`` at the given position. + + Return :obj:`None` at the end of ``header``. + + We never need to peek more than one character ahead. + + """ + return None if pos == len(header) else header[pos] + + +_OWS_re = re.compile(r"[\t ]*") + + +def parse_OWS(header: str, pos: int) -> int: + """ + Parse optional whitespace from ``header`` at the given position. + + Return the new position. + + The whitespace itself isn't returned because it isn't significant. + + """ + # There's always a match, possibly empty, whose content doesn't matter. + match = _OWS_re.match(header, pos) + assert match is not None + return match.end() + + +_token_re = re.compile(r"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+") + + +def parse_token(header: str, pos: int, header_name: str) -> Tuple[str, int]: + """ + Parse a token from ``header`` at the given position. + + Return the token value and the new position. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + match = _token_re.match(header, pos) + if match is None: + raise exceptions.InvalidHeaderFormat(header_name, "expected token", header, pos) + return match.group(), match.end() + + +_quoted_string_re = re.compile( + r'"(?:[\x09\x20-\x21\x23-\x5b\x5d-\x7e]|\\[\x09\x20-\x7e\x80-\xff])*"' +) + + +_unquote_re = re.compile(r"\\([\x09\x20-\x7e\x80-\xff])") + + +def parse_quoted_string(header: str, pos: int, header_name: str) -> Tuple[str, int]: + """ + Parse a quoted string from ``header`` at the given position. + + Return the unquoted value and the new position. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + match = _quoted_string_re.match(header, pos) + if match is None: + raise exceptions.InvalidHeaderFormat( + header_name, "expected quoted string", header, pos + ) + return _unquote_re.sub(r"\1", match.group()[1:-1]), match.end() + + +_quotable_re = re.compile(r"[\x09\x20-\x7e\x80-\xff]*") + + +_quote_re = re.compile(r"([\x22\x5c])") + + +def build_quoted_string(value: str) -> str: + """ + Format ``value`` as a quoted string. + + This is the reverse of :func:`parse_quoted_string`. + + """ + match = _quotable_re.fullmatch(value) + if match is None: + raise ValueError("invalid characters for quoted-string encoding") + return '"' + _quote_re.sub(r"\\\1", value) + '"' + + +def parse_list( + parse_item: Callable[[str, int, str], Tuple[T, int]], + header: str, + pos: int, + header_name: str, +) -> List[T]: + """ + Parse a comma-separated list from ``header`` at the given position. + + This is appropriate for parsing values with the following grammar: + + 1#item + + ``parse_item`` parses one item. + + ``header`` is assumed not to start or end with whitespace. + + (This function is designed for parsing an entire header value and + :func:`~websockets.http.read_headers` strips whitespace from values.) + + Return a list of items. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + # Per https://www.rfc-editor.org/rfc/rfc7230.html#section-7, "a recipient + # MUST parse and ignore a reasonable number of empty list elements"; + # hence while loops that remove extra delimiters. + + # Remove extra delimiters before the first item. + while peek_ahead(header, pos) == ",": + pos = parse_OWS(header, pos + 1) + + items = [] + while True: + # Loop invariant: a item starts at pos in header. + item, pos = parse_item(header, pos, header_name) + items.append(item) + pos = parse_OWS(header, pos) + + # We may have reached the end of the header. + if pos == len(header): + break + + # There must be a delimiter after each element except the last one. + if peek_ahead(header, pos) == ",": + pos = parse_OWS(header, pos + 1) + else: + raise exceptions.InvalidHeaderFormat( + header_name, "expected comma", header, pos + ) + + # Remove extra delimiters before the next item. + while peek_ahead(header, pos) == ",": + pos = parse_OWS(header, pos + 1) + + # We may have reached the end of the header. + if pos == len(header): + break + + # Since we only advance in the header by one character with peek_ahead() + # or with the end position of a regex match, we can't overshoot the end. + assert pos == len(header) + + return items + + +def parse_connection_option( + header: str, pos: int, header_name: str +) -> Tuple[ConnectionOption, int]: + """ + Parse a Connection option from ``header`` at the given position. + + Return the protocol value and the new position. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + item, pos = parse_token(header, pos, header_name) + return cast(ConnectionOption, item), pos + + +def parse_connection(header: str) -> List[ConnectionOption]: + """ + Parse a ``Connection`` header. + + Return a list of HTTP connection options. + + Args + header: value of the ``Connection`` header. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + return parse_list(parse_connection_option, header, 0, "Connection") + + +_protocol_re = re.compile( + r"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+(?:/[-!#$%&\'*+.^_`|~0-9a-zA-Z]+)?" +) + + +def parse_upgrade_protocol( + header: str, pos: int, header_name: str +) -> Tuple[UpgradeProtocol, int]: + """ + Parse an Upgrade protocol from ``header`` at the given position. + + Return the protocol value and the new position. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + match = _protocol_re.match(header, pos) + if match is None: + raise exceptions.InvalidHeaderFormat( + header_name, "expected protocol", header, pos + ) + return cast(UpgradeProtocol, match.group()), match.end() + + +def parse_upgrade(header: str) -> List[UpgradeProtocol]: + """ + Parse an ``Upgrade`` header. + + Return a list of HTTP protocols. + + Args: + header: value of the ``Upgrade`` header. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + return parse_list(parse_upgrade_protocol, header, 0, "Upgrade") + + +def parse_extension_item_param( + header: str, pos: int, header_name: str +) -> Tuple[ExtensionParameter, int]: + """ + Parse a single extension parameter from ``header`` at the given position. + + Return a ``(name, value)`` pair and the new position. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + # Extract parameter name. + name, pos = parse_token(header, pos, header_name) + pos = parse_OWS(header, pos) + # Extract parameter value, if there is one. + value: Optional[str] = None + if peek_ahead(header, pos) == "=": + pos = parse_OWS(header, pos + 1) + if peek_ahead(header, pos) == '"': + pos_before = pos # for proper error reporting below + value, pos = parse_quoted_string(header, pos, header_name) + # https://www.rfc-editor.org/rfc/rfc6455.html#section-9.1 says: + # the value after quoted-string unescaping MUST conform to + # the 'token' ABNF. + if _token_re.fullmatch(value) is None: + raise exceptions.InvalidHeaderFormat( + header_name, "invalid quoted header content", header, pos_before + ) + else: + value, pos = parse_token(header, pos, header_name) + pos = parse_OWS(header, pos) + + return (name, value), pos + + +def parse_extension_item( + header: str, pos: int, header_name: str +) -> Tuple[ExtensionHeader, int]: + """ + Parse an extension definition from ``header`` at the given position. + + Return an ``(extension name, parameters)`` pair, where ``parameters`` is a + list of ``(name, value)`` pairs, and the new position. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + # Extract extension name. + name, pos = parse_token(header, pos, header_name) + pos = parse_OWS(header, pos) + # Extract all parameters. + parameters = [] + while peek_ahead(header, pos) == ";": + pos = parse_OWS(header, pos + 1) + parameter, pos = parse_extension_item_param(header, pos, header_name) + parameters.append(parameter) + return (cast(ExtensionName, name), parameters), pos + + +def parse_extension(header: str) -> List[ExtensionHeader]: + """ + Parse a ``Sec-WebSocket-Extensions`` header. + + Return a list of WebSocket extensions and their parameters in this format:: + + [ + ( + 'extension name', + [ + ('parameter name', 'parameter value'), + .... + ] + ), + ... + ] + + Parameter values are :obj:`None` when no value is provided. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + return parse_list(parse_extension_item, header, 0, "Sec-WebSocket-Extensions") + + +parse_extension_list = parse_extension # alias for backwards compatibility + + +def build_extension_item( + name: ExtensionName, parameters: List[ExtensionParameter] +) -> str: + """ + Build an extension definition. + + This is the reverse of :func:`parse_extension_item`. + + """ + return "; ".join( + [cast(str, name)] + + [ + # Quoted strings aren't necessary because values are always tokens. + name if value is None else f"{name}={value}" + for name, value in parameters + ] + ) + + +def build_extension(extensions: Sequence[ExtensionHeader]) -> str: + """ + Build a ``Sec-WebSocket-Extensions`` header. + + This is the reverse of :func:`parse_extension`. + + """ + return ", ".join( + build_extension_item(name, parameters) for name, parameters in extensions + ) + + +build_extension_list = build_extension # alias for backwards compatibility + + +def parse_subprotocol_item( + header: str, pos: int, header_name: str +) -> Tuple[Subprotocol, int]: + """ + Parse a subprotocol from ``header`` at the given position. + + Return the subprotocol value and the new position. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + item, pos = parse_token(header, pos, header_name) + return cast(Subprotocol, item), pos + + +def parse_subprotocol(header: str) -> List[Subprotocol]: + """ + Parse a ``Sec-WebSocket-Protocol`` header. + + Return a list of WebSocket subprotocols. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + return parse_list(parse_subprotocol_item, header, 0, "Sec-WebSocket-Protocol") + + +parse_subprotocol_list = parse_subprotocol # alias for backwards compatibility + + +def build_subprotocol(subprotocols: Sequence[Subprotocol]) -> str: + """ + Build a ``Sec-WebSocket-Protocol`` header. + + This is the reverse of :func:`parse_subprotocol`. + + """ + return ", ".join(subprotocols) + + +build_subprotocol_list = build_subprotocol # alias for backwards compatibility + + +def validate_subprotocols(subprotocols: Sequence[Subprotocol]) -> None: + """ + Validate that ``subprotocols`` is suitable for :func:`build_subprotocol`. + + """ + if not isinstance(subprotocols, Sequence): + raise TypeError("subprotocols must be a list") + if isinstance(subprotocols, str): + raise TypeError("subprotocols must be a list, not a str") + for subprotocol in subprotocols: + if not _token_re.fullmatch(subprotocol): + raise ValueError(f"invalid subprotocol: {subprotocol}") + + +def build_www_authenticate_basic(realm: str) -> str: + """ + Build a ``WWW-Authenticate`` header for HTTP Basic Auth. + + Args: + realm: identifier of the protection space. + + """ + # https://www.rfc-editor.org/rfc/rfc7617.html#section-2 + realm = build_quoted_string(realm) + charset = build_quoted_string("UTF-8") + return f"Basic realm={realm}, charset={charset}" + + +_token68_re = re.compile(r"[A-Za-z0-9-._~+/]+=*") + + +def parse_token68(header: str, pos: int, header_name: str) -> Tuple[str, int]: + """ + Parse a token68 from ``header`` at the given position. + + Return the token value and the new position. + + Raises: + InvalidHeaderFormat: on invalid inputs. + + """ + match = _token68_re.match(header, pos) + if match is None: + raise exceptions.InvalidHeaderFormat( + header_name, "expected token68", header, pos + ) + return match.group(), match.end() + + +def parse_end(header: str, pos: int, header_name: str) -> None: + """ + Check that parsing reached the end of header. + + """ + if pos < len(header): + raise exceptions.InvalidHeaderFormat(header_name, "trailing data", header, pos) + + +def parse_authorization_basic(header: str) -> Tuple[str, str]: + """ + Parse an ``Authorization`` header for HTTP Basic Auth. + + Return a ``(username, password)`` tuple. + + Args: + header: value of the ``Authorization`` header. + + Raises: + InvalidHeaderFormat: on invalid inputs. + InvalidHeaderValue: on unsupported inputs. + + """ + # https://www.rfc-editor.org/rfc/rfc7235.html#section-2.1 + # https://www.rfc-editor.org/rfc/rfc7617.html#section-2 + scheme, pos = parse_token(header, 0, "Authorization") + if scheme.lower() != "basic": + raise exceptions.InvalidHeaderValue( + "Authorization", + f"unsupported scheme: {scheme}", + ) + if peek_ahead(header, pos) != " ": + raise exceptions.InvalidHeaderFormat( + "Authorization", "expected space after scheme", header, pos + ) + pos += 1 + basic_credentials, pos = parse_token68(header, pos, "Authorization") + parse_end(header, pos, "Authorization") + + try: + user_pass = base64.b64decode(basic_credentials.encode()).decode() + except binascii.Error: + raise exceptions.InvalidHeaderValue( + "Authorization", + "expected base64-encoded credentials", + ) from None + try: + username, password = user_pass.split(":", 1) + except ValueError: + raise exceptions.InvalidHeaderValue( + "Authorization", + "expected username:password credentials", + ) from None + + return username, password + + +def build_authorization_basic(username: str, password: str) -> str: + """ + Build an ``Authorization`` header for HTTP Basic Auth. + + This is the reverse of :func:`parse_authorization_basic`. + + """ + # https://www.rfc-editor.org/rfc/rfc7617.html#section-2 + assert ":" not in username + user_pass = f"{username}:{password}" + basic_credentials = base64.b64encode(user_pass.encode()).decode() + return "Basic " + basic_credentials diff --git a/websockets/http.py b/websockets/http.py new file mode 100644 index 0000000..b14fa94 --- /dev/null +++ b/websockets/http.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import sys + +from .imports import lazy_import +from .version import version as websockets_version + + +# For backwards compatibility: + + +lazy_import( + globals(), + # Headers and MultipleValuesError used to be defined in this module. + aliases={ + "Headers": ".datastructures", + "MultipleValuesError": ".datastructures", + }, + deprecated_aliases={ + "read_request": ".legacy.http", + "read_response": ".legacy.http", + }, +) + + +__all__ = ["USER_AGENT"] + + +PYTHON_VERSION = "{}.{}".format(*sys.version_info) +USER_AGENT = f"Python/{PYTHON_VERSION} websockets/{websockets_version}" diff --git a/websockets/http11.py b/websockets/http11.py new file mode 100644 index 0000000..ec4e3b8 --- /dev/null +++ b/websockets/http11.py @@ -0,0 +1,364 @@ +from __future__ import annotations + +import dataclasses +import re +import warnings +from typing import Callable, Generator, Optional + +from . import datastructures, exceptions + + +# Maximum total size of headers is around 128 * 8 KiB = 1 MiB. +MAX_HEADERS = 128 + +# Limit request line and header lines. 8KiB is the most common default +# configuration of popular HTTP servers. +MAX_LINE = 8192 + +# Support for HTTP response bodies is intended to read an error message +# returned by a server. It isn't designed to perform large file transfers. +MAX_BODY = 2**20 # 1 MiB + + +def d(value: bytes) -> str: + """ + Decode a bytestring for interpolating into an error message. + + """ + return value.decode(errors="backslashreplace") + + +# See https://www.rfc-editor.org/rfc/rfc7230.html#appendix-B. + +# Regex for validating header names. + +_token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+") + +# Regex for validating header values. + +# We don't attempt to support obsolete line folding. + +# Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff). + +# The ABNF is complicated because it attempts to express that optional +# whitespace is ignored. We strip whitespace and don't revalidate that. + +# See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189 + +_value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*") + + +@dataclasses.dataclass +class Request: + """ + WebSocket handshake request. + + Attributes: + path: Request path, including optional query. + headers: Request headers. + """ + + path: str + headers: datastructures.Headers + # body isn't useful is the context of this library. + + _exception: Optional[Exception] = None + + @property + def exception(self) -> Optional[Exception]: # pragma: no cover + warnings.warn( + "Request.exception is deprecated; " + "use ServerProtocol.handshake_exc instead", + DeprecationWarning, + ) + return self._exception + + @classmethod + def parse( + cls, + read_line: Callable[[int], Generator[None, None, bytes]], + ) -> Generator[None, None, Request]: + """ + Parse a WebSocket handshake request. + + This is a generator-based coroutine. + + The request path isn't URL-decoded or validated in any way. + + The request path and headers are expected to contain only ASCII + characters. Other characters are represented with surrogate escapes. + + :meth:`parse` doesn't attempt to read the request body because + WebSocket handshake requests don't have one. If the request contains a + body, it may be read from the data stream after :meth:`parse` returns. + + Args: + read_line: generator-based coroutine that reads a LF-terminated + line or raises an exception if there isn't enough data + + Raises: + EOFError: if the connection is closed without a full HTTP request. + SecurityError: if the request exceeds a security limit. + ValueError: if the request isn't well formatted. + + """ + # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.1 + + # Parsing is simple because fixed values are expected for method and + # version and because path isn't checked. Since WebSocket software tends + # to implement HTTP/1.1 strictly, there's little need for lenient parsing. + + try: + request_line = yield from parse_line(read_line) + except EOFError as exc: + raise EOFError("connection closed while reading HTTP request line") from exc + + try: + method, raw_path, version = request_line.split(b" ", 2) + except ValueError: # not enough values to unpack (expected 3, got 1-2) + raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None + + if method != b"GET": + raise ValueError(f"unsupported HTTP method: {d(method)}") + if version != b"HTTP/1.1": + raise ValueError(f"unsupported HTTP version: {d(version)}") + path = raw_path.decode("ascii", "surrogateescape") + + headers = yield from parse_headers(read_line) + + # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.3.3 + + if "Transfer-Encoding" in headers: + raise NotImplementedError("transfer codings aren't supported") + + if "Content-Length" in headers: + raise ValueError("unsupported request body") + + return cls(path, headers) + + def serialize(self) -> bytes: + """ + Serialize a WebSocket handshake request. + + """ + # Since the request line and headers only contain ASCII characters, + # we can keep this simple. + request = f"GET {self.path} HTTP/1.1\r\n".encode() + request += self.headers.serialize() + return request + + +@dataclasses.dataclass +class Response: + """ + WebSocket handshake response. + + Attributes: + status_code: Response code. + reason_phrase: Response reason. + headers: Response headers. + body: Response body, if any. + + """ + + status_code: int + reason_phrase: str + headers: datastructures.Headers + body: Optional[bytes] = None + + _exception: Optional[Exception] = None + + @property + def exception(self) -> Optional[Exception]: # pragma: no cover + warnings.warn( + "Response.exception is deprecated; " + "use ClientProtocol.handshake_exc instead", + DeprecationWarning, + ) + return self._exception + + @classmethod + def parse( + cls, + read_line: Callable[[int], Generator[None, None, bytes]], + read_exact: Callable[[int], Generator[None, None, bytes]], + read_to_eof: Callable[[int], Generator[None, None, bytes]], + ) -> Generator[None, None, Response]: + """ + Parse a WebSocket handshake response. + + This is a generator-based coroutine. + + The reason phrase and headers are expected to contain only ASCII + characters. Other characters are represented with surrogate escapes. + + Args: + read_line: generator-based coroutine that reads a LF-terminated + line or raises an exception if there isn't enough data. + read_exact: generator-based coroutine that reads the requested + bytes or raises an exception if there isn't enough data. + read_to_eof: generator-based coroutine that reads until the end + of the stream. + + Raises: + EOFError: if the connection is closed without a full HTTP response. + SecurityError: if the response exceeds a security limit. + LookupError: if the response isn't well formatted. + ValueError: if the response isn't well formatted. + + """ + # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.2 + + try: + status_line = yield from parse_line(read_line) + except EOFError as exc: + raise EOFError("connection closed while reading HTTP status line") from exc + + try: + version, raw_status_code, raw_reason = status_line.split(b" ", 2) + except ValueError: # not enough values to unpack (expected 3, got 1-2) + raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None + + if version != b"HTTP/1.1": + raise ValueError(f"unsupported HTTP version: {d(version)}") + try: + status_code = int(raw_status_code) + except ValueError: # invalid literal for int() with base 10 + raise ValueError( + f"invalid HTTP status code: {d(raw_status_code)}" + ) from None + if not 100 <= status_code < 1000: + raise ValueError(f"unsupported HTTP status code: {d(raw_status_code)}") + if not _value_re.fullmatch(raw_reason): + raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}") + reason = raw_reason.decode() + + headers = yield from parse_headers(read_line) + + # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.3.3 + + if "Transfer-Encoding" in headers: + raise NotImplementedError("transfer codings aren't supported") + + # Since websockets only does GET requests (no HEAD, no CONNECT), all + # responses except 1xx, 204, and 304 include a message body. + if 100 <= status_code < 200 or status_code == 204 or status_code == 304: + body = None + else: + content_length: Optional[int] + try: + # MultipleValuesError is sufficiently unlikely that we don't + # attempt to handle it. Instead we document that its parent + # class, LookupError, may be raised. + raw_content_length = headers["Content-Length"] + except KeyError: + content_length = None + else: + content_length = int(raw_content_length) + + if content_length is None: + try: + body = yield from read_to_eof(MAX_BODY) + except RuntimeError: + raise exceptions.SecurityError( + f"body too large: over {MAX_BODY} bytes" + ) + elif content_length > MAX_BODY: + raise exceptions.SecurityError( + f"body too large: {content_length} bytes" + ) + else: + body = yield from read_exact(content_length) + + return cls(status_code, reason, headers, body) + + def serialize(self) -> bytes: + """ + Serialize a WebSocket handshake response. + + """ + # Since the status line and headers only contain ASCII characters, + # we can keep this simple. + response = f"HTTP/1.1 {self.status_code} {self.reason_phrase}\r\n".encode() + response += self.headers.serialize() + if self.body is not None: + response += self.body + return response + + +def parse_headers( + read_line: Callable[[int], Generator[None, None, bytes]], +) -> Generator[None, None, datastructures.Headers]: + """ + Parse HTTP headers. + + Non-ASCII characters are represented with surrogate escapes. + + Args: + read_line: generator-based coroutine that reads a LF-terminated line + or raises an exception if there isn't enough data. + + Raises: + EOFError: if the connection is closed without complete headers. + SecurityError: if the request exceeds a security limit. + ValueError: if the request isn't well formatted. + + """ + # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2 + + # We don't attempt to support obsolete line folding. + + headers = datastructures.Headers() + for _ in range(MAX_HEADERS + 1): + try: + line = yield from parse_line(read_line) + except EOFError as exc: + raise EOFError("connection closed while reading HTTP headers") from exc + if line == b"": + break + + try: + raw_name, raw_value = line.split(b":", 1) + except ValueError: # not enough values to unpack (expected 2, got 1) + raise ValueError(f"invalid HTTP header line: {d(line)}") from None + if not _token_re.fullmatch(raw_name): + raise ValueError(f"invalid HTTP header name: {d(raw_name)}") + raw_value = raw_value.strip(b" \t") + if not _value_re.fullmatch(raw_value): + raise ValueError(f"invalid HTTP header value: {d(raw_value)}") + + name = raw_name.decode("ascii") # guaranteed to be ASCII at this point + value = raw_value.decode("ascii", "surrogateescape") + headers[name] = value + + else: + raise exceptions.SecurityError("too many HTTP headers") + + return headers + + +def parse_line( + read_line: Callable[[int], Generator[None, None, bytes]], +) -> Generator[None, None, bytes]: + """ + Parse a single line. + + CRLF is stripped from the return value. + + Args: + read_line: generator-based coroutine that reads a LF-terminated line + or raises an exception if there isn't enough data. + + Raises: + EOFError: if the connection is closed without a CRLF. + SecurityError: if the response exceeds a security limit. + + """ + try: + line = yield from read_line(MAX_LINE) + except RuntimeError: + raise exceptions.SecurityError("line too long") + # Not mandatory but safe - https://www.rfc-editor.org/rfc/rfc7230.html#section-3.5 + if not line.endswith(b"\r\n"): + raise EOFError("line without CRLF") + return line[:-2] diff --git a/websockets/imports.py b/websockets/imports.py new file mode 100644 index 0000000..a6a59d4 --- /dev/null +++ b/websockets/imports.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import warnings +from typing import Any, Dict, Iterable, Optional + + +__all__ = ["lazy_import"] + + +def import_name(name: str, source: str, namespace: Dict[str, Any]) -> Any: + """ + Import ``name`` from ``source`` in ``namespace``. + + There are two use cases: + + - ``name`` is an object defined in ``source``; + - ``name`` is a submodule of ``source``. + + Neither :func:`__import__` nor :func:`~importlib.import_module` does + exactly this. :func:`__import__` is closer to the intended behavior. + + """ + level = 0 + while source[level] == ".": + level += 1 + assert level < len(source), "importing from parent isn't supported" + module = __import__(source[level:], namespace, None, [name], level) + return getattr(module, name) + + +def lazy_import( + namespace: Dict[str, Any], + aliases: Optional[Dict[str, str]] = None, + deprecated_aliases: Optional[Dict[str, str]] = None, +) -> None: + """ + Provide lazy, module-level imports. + + Typical use:: + + __getattr__, __dir__ = lazy_import( + globals(), + aliases={ + "": "", + ... + }, + deprecated_aliases={ + ..., + } + ) + + This function defines ``__getattr__`` and ``__dir__`` per :pep:`562`. + + """ + if aliases is None: + aliases = {} + if deprecated_aliases is None: + deprecated_aliases = {} + + namespace_set = set(namespace) + aliases_set = set(aliases) + deprecated_aliases_set = set(deprecated_aliases) + + assert not namespace_set & aliases_set, "namespace conflict" + assert not namespace_set & deprecated_aliases_set, "namespace conflict" + assert not aliases_set & deprecated_aliases_set, "namespace conflict" + + package = namespace["__name__"] + + def __getattr__(name: str) -> Any: + assert aliases is not None # mypy cannot figure this out + try: + source = aliases[name] + except KeyError: + pass + else: + return import_name(name, source, namespace) + + assert deprecated_aliases is not None # mypy cannot figure this out + try: + source = deprecated_aliases[name] + except KeyError: + pass + else: + warnings.warn( + f"{package}.{name} is deprecated", + DeprecationWarning, + stacklevel=2, + ) + return import_name(name, source, namespace) + + raise AttributeError(f"module {package!r} has no attribute {name!r}") + + namespace["__getattr__"] = __getattr__ + + def __dir__() -> Iterable[str]: + return sorted(namespace_set | aliases_set | deprecated_aliases_set) + + namespace["__dir__"] = __dir__ diff --git a/websockets/legacy/__init__.py b/websockets/legacy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/websockets/legacy/async_timeout.py b/websockets/legacy/async_timeout.py new file mode 100644 index 0000000..8264094 --- /dev/null +++ b/websockets/legacy/async_timeout.py @@ -0,0 +1,265 @@ +# From https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py +# Licensed under the Apache License (Apache-2.0) + +import asyncio +import enum +import sys +import warnings +from types import TracebackType +from typing import Optional, Type + + +# From https://github.com/python/typing_extensions/blob/main/src/typing_extensions.py +# Licensed under the Python Software Foundation License (PSF-2.0) + +if sys.version_info >= (3, 11): + from typing import final +else: + # @final exists in 3.8+, but we backport it for all versions + # before 3.11 to keep support for the __final__ attribute. + # See https://bugs.python.org/issue46342 + def final(f): + """This decorator can be used to indicate to type checkers that + the decorated method cannot be overridden, and decorated class + cannot be subclassed. For example: + + class Base: + @final + def done(self) -> None: + ... + class Sub(Base): + def done(self) -> None: # Error reported by type checker + ... + @final + class Leaf: + ... + class Other(Leaf): # Error reported by type checker + ... + + There is no runtime checking of these properties. The decorator + sets the ``__final__`` attribute to ``True`` on the decorated object + to allow runtime introspection. + """ + try: + f.__final__ = True + except (AttributeError, TypeError): + # Skip the attribute silently if it is not writable. + # AttributeError happens if the object has __slots__ or a + # read-only property, TypeError if it's a builtin class. + pass + return f + + +# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py + +__version__ = "4.0.2" + + +__all__ = ("timeout", "timeout_at", "Timeout") + + +def timeout(delay: Optional[float]) -> "Timeout": + """timeout context manager. + + Useful in cases when you want to apply timeout logic around block + of code or in cases when asyncio.wait_for is not suitable. For example: + + >>> async with timeout(0.001): + ... async with aiohttp.get('https://github.com') as r: + ... await r.text() + + + delay - value in seconds or None to disable timeout logic + """ + loop = asyncio.get_running_loop() + if delay is not None: + deadline = loop.time() + delay # type: Optional[float] + else: + deadline = None + return Timeout(deadline, loop) + + +def timeout_at(deadline: Optional[float]) -> "Timeout": + """Schedule the timeout at absolute time. + + deadline argument points on the time in the same clock system + as loop.time(). + + Please note: it is not POSIX time but a time with + undefined starting base, e.g. the time of the system power on. + + >>> async with timeout_at(loop.time() + 10): + ... async with aiohttp.get('https://github.com') as r: + ... await r.text() + + + """ + loop = asyncio.get_running_loop() + return Timeout(deadline, loop) + + +class _State(enum.Enum): + INIT = "INIT" + ENTER = "ENTER" + TIMEOUT = "TIMEOUT" + EXIT = "EXIT" + + +@final +class Timeout: + # Internal class, please don't instantiate it directly + # Use timeout() and timeout_at() public factories instead. + # + # Implementation note: `async with timeout()` is preferred + # over `with timeout()`. + # While technically the Timeout class implementation + # doesn't need to be async at all, + # the `async with` statement explicitly points that + # the context manager should be used from async function context. + # + # This design allows to avoid many silly misusages. + # + # TimeoutError is raised immediately when scheduled + # if the deadline is passed. + # The purpose is to time out as soon as possible + # without waiting for the next await expression. + + __slots__ = ("_deadline", "_loop", "_state", "_timeout_handler") + + def __init__( + self, deadline: Optional[float], loop: asyncio.AbstractEventLoop + ) -> None: + self._loop = loop + self._state = _State.INIT + + self._timeout_handler = None # type: Optional[asyncio.Handle] + if deadline is None: + self._deadline = None # type: Optional[float] + else: + self.update(deadline) + + def __enter__(self) -> "Timeout": + warnings.warn( + "with timeout() is deprecated, use async with timeout() instead", + DeprecationWarning, + stacklevel=2, + ) + self._do_enter() + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Optional[bool]: + self._do_exit(exc_type) + return None + + async def __aenter__(self) -> "Timeout": + self._do_enter() + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Optional[bool]: + self._do_exit(exc_type) + return None + + @property + def expired(self) -> bool: + """Is timeout expired during execution?""" + return self._state == _State.TIMEOUT + + @property + def deadline(self) -> Optional[float]: + return self._deadline + + def reject(self) -> None: + """Reject scheduled timeout if any.""" + # cancel is maybe better name but + # task.cancel() raises CancelledError in asyncio world. + if self._state not in (_State.INIT, _State.ENTER): + raise RuntimeError(f"invalid state {self._state.value}") + self._reject() + + def _reject(self) -> None: + if self._timeout_handler is not None: + self._timeout_handler.cancel() + self._timeout_handler = None + + def shift(self, delay: float) -> None: + """Advance timeout on delay seconds. + + The delay can be negative. + + Raise RuntimeError if shift is called when deadline is not scheduled + """ + deadline = self._deadline + if deadline is None: + raise RuntimeError("cannot shift timeout if deadline is not scheduled") + self.update(deadline + delay) + + def update(self, deadline: float) -> None: + """Set deadline to absolute value. + + deadline argument points on the time in the same clock system + as loop.time(). + + If new deadline is in the past the timeout is raised immediately. + + Please note: it is not POSIX time but a time with + undefined starting base, e.g. the time of the system power on. + """ + if self._state == _State.EXIT: + raise RuntimeError("cannot reschedule after exit from context manager") + if self._state == _State.TIMEOUT: + raise RuntimeError("cannot reschedule expired timeout") + if self._timeout_handler is not None: + self._timeout_handler.cancel() + self._deadline = deadline + if self._state != _State.INIT: + self._reschedule() + + def _reschedule(self) -> None: + assert self._state == _State.ENTER + deadline = self._deadline + if deadline is None: + return + + now = self._loop.time() + if self._timeout_handler is not None: + self._timeout_handler.cancel() + + task = asyncio.current_task() + if deadline <= now: + self._timeout_handler = self._loop.call_soon(self._on_timeout, task) + else: + self._timeout_handler = self._loop.call_at(deadline, self._on_timeout, task) + + def _do_enter(self) -> None: + if self._state != _State.INIT: + raise RuntimeError(f"invalid state {self._state.value}") + self._state = _State.ENTER + self._reschedule() + + def _do_exit(self, exc_type: Optional[Type[BaseException]]) -> None: + if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT: + self._timeout_handler = None + raise asyncio.TimeoutError + # timeout has not expired + self._state = _State.EXIT + self._reject() + return None + + def _on_timeout(self, task: "asyncio.Task[None]") -> None: + task.cancel() + self._state = _State.TIMEOUT + # drop the reference early + self._timeout_handler = None + + +# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py diff --git a/websockets/legacy/auth.py b/websockets/legacy/auth.py new file mode 100644 index 0000000..d342583 --- /dev/null +++ b/websockets/legacy/auth.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +import functools +import hmac +import http +from typing import Any, Awaitable, Callable, Iterable, Optional, Tuple, Union, cast + +from ..datastructures import Headers +from ..exceptions import InvalidHeader +from ..headers import build_www_authenticate_basic, parse_authorization_basic +from .server import HTTPResponse, WebSocketServerProtocol + + +__all__ = ["BasicAuthWebSocketServerProtocol", "basic_auth_protocol_factory"] + +Credentials = Tuple[str, str] + + +def is_credentials(value: Any) -> bool: + try: + username, password = value + except (TypeError, ValueError): + return False + else: + return isinstance(username, str) and isinstance(password, str) + + +class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol): + """ + WebSocket server protocol that enforces HTTP Basic Auth. + + """ + + realm: str = "" + """ + Scope of protection. + + If provided, it should contain only ASCII characters because the + encoding of non-ASCII characters is undefined. + """ + + username: Optional[str] = None + """Username of the authenticated user.""" + + def __init__( + self, + *args: Any, + realm: Optional[str] = None, + check_credentials: Optional[Callable[[str, str], Awaitable[bool]]] = None, + **kwargs: Any, + ) -> None: + if realm is not None: + self.realm = realm # shadow class attribute + self._check_credentials = check_credentials + super().__init__(*args, **kwargs) + + async def check_credentials(self, username: str, password: str) -> bool: + """ + Check whether credentials are authorized. + + This coroutine may be overridden in a subclass, for example to + authenticate against a database or an external service. + + Args: + username: HTTP Basic Auth username. + password: HTTP Basic Auth password. + + Returns: + bool: :obj:`True` if the handshake should continue; + :obj:`False` if it should fail with an HTTP 401 error. + + """ + if self._check_credentials is not None: + return await self._check_credentials(username, password) + + return False + + async def process_request( + self, + path: str, + request_headers: Headers, + ) -> Optional[HTTPResponse]: + """ + Check HTTP Basic Auth and return an HTTP 401 response if needed. + + """ + try: + authorization = request_headers["Authorization"] + except KeyError: + return ( + http.HTTPStatus.UNAUTHORIZED, + [("WWW-Authenticate", build_www_authenticate_basic(self.realm))], + b"Missing credentials\n", + ) + + try: + username, password = parse_authorization_basic(authorization) + except InvalidHeader: + return ( + http.HTTPStatus.UNAUTHORIZED, + [("WWW-Authenticate", build_www_authenticate_basic(self.realm))], + b"Unsupported credentials\n", + ) + + if not await self.check_credentials(username, password): + return ( + http.HTTPStatus.UNAUTHORIZED, + [("WWW-Authenticate", build_www_authenticate_basic(self.realm))], + b"Invalid credentials\n", + ) + + self.username = username + + return await super().process_request(path, request_headers) + + +def basic_auth_protocol_factory( + realm: Optional[str] = None, + credentials: Optional[Union[Credentials, Iterable[Credentials]]] = None, + check_credentials: Optional[Callable[[str, str], Awaitable[bool]]] = None, + create_protocol: Optional[Callable[..., BasicAuthWebSocketServerProtocol]] = None, +) -> Callable[..., BasicAuthWebSocketServerProtocol]: + """ + Protocol factory that enforces HTTP Basic Auth. + + :func:`basic_auth_protocol_factory` is designed to integrate with + :func:`~websockets.server.serve` like this:: + + websockets.serve( + ..., + create_protocol=websockets.basic_auth_protocol_factory( + realm="my dev server", + credentials=("hello", "iloveyou"), + ) + ) + + Args: + realm: Scope of protection. It should contain only ASCII characters + because the encoding of non-ASCII characters is undefined. + Refer to section 2.2 of :rfc:`7235` for details. + credentials: Hard coded authorized credentials. It can be a + ``(username, password)`` pair or a list of such pairs. + check_credentials: Coroutine that verifies credentials. + It receives ``username`` and ``password`` arguments + and returns a :class:`bool`. One of ``credentials`` or + ``check_credentials`` must be provided but not both. + create_protocol: Factory that creates the protocol. By default, this + is :class:`BasicAuthWebSocketServerProtocol`. It can be replaced + by a subclass. + Raises: + TypeError: If the ``credentials`` or ``check_credentials`` argument is + wrong. + + """ + if (credentials is None) == (check_credentials is None): + raise TypeError("provide either credentials or check_credentials") + + if credentials is not None: + if is_credentials(credentials): + credentials_list = [cast(Credentials, credentials)] + elif isinstance(credentials, Iterable): + credentials_list = list(credentials) + if not all(is_credentials(item) for item in credentials_list): + raise TypeError(f"invalid credentials argument: {credentials}") + else: + raise TypeError(f"invalid credentials argument: {credentials}") + + credentials_dict = dict(credentials_list) + + async def check_credentials(username: str, password: str) -> bool: + try: + expected_password = credentials_dict[username] + except KeyError: + return False + return hmac.compare_digest(expected_password, password) + + if create_protocol is None: + create_protocol = BasicAuthWebSocketServerProtocol + + return functools.partial( + create_protocol, + realm=realm, + check_credentials=check_credentials, + ) diff --git a/websockets/legacy/client.py b/websockets/legacy/client.py new file mode 100644 index 0000000..4862252 --- /dev/null +++ b/websockets/legacy/client.py @@ -0,0 +1,705 @@ +from __future__ import annotations + +import asyncio +import functools +import logging +import random +import urllib.parse +import warnings +from types import TracebackType +from typing import ( + Any, + AsyncIterator, + Callable, + Generator, + List, + Optional, + Sequence, + Tuple, + Type, + cast, +) + +from ..datastructures import Headers, HeadersLike +from ..exceptions import ( + InvalidHandshake, + InvalidHeader, + InvalidMessage, + InvalidStatusCode, + NegotiationError, + RedirectHandshake, + SecurityError, +) +from ..extensions import ClientExtensionFactory, Extension +from ..extensions.permessage_deflate import enable_client_permessage_deflate +from ..headers import ( + build_authorization_basic, + build_extension, + build_host, + build_subprotocol, + parse_extension, + parse_subprotocol, + validate_subprotocols, +) +from ..http import USER_AGENT +from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol +from ..uri import WebSocketURI, parse_uri +from .compatibility import asyncio_timeout +from .handshake import build_request, check_response +from .http import read_response +from .protocol import WebSocketCommonProtocol + + +__all__ = ["connect", "unix_connect", "WebSocketClientProtocol"] + + +class WebSocketClientProtocol(WebSocketCommonProtocol): + """ + WebSocket client connection. + + :class:`WebSocketClientProtocol` provides :meth:`recv` and :meth:`send` + coroutines for receiving and sending messages. + + It supports asynchronous iteration to receive incoming messages:: + + async for message in websocket: + await process(message) + + The iterator exits normally when the connection is closed with close code + 1000 (OK) or 1001 (going away) or without a close code. It raises + a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection + is closed with any other code. + + See :func:`connect` for the documentation of ``logger``, ``origin``, + ``extensions``, ``subprotocols``, ``extra_headers``, and + ``user_agent_header``. + + See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the + documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, + ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. + + """ + + is_client = True + side = "client" + + def __init__( + self, + *, + logger: Optional[LoggerLike] = None, + origin: Optional[Origin] = None, + extensions: Optional[Sequence[ClientExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + extra_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, + **kwargs: Any, + ) -> None: + if logger is None: + logger = logging.getLogger("websockets.client") + super().__init__(logger=logger, **kwargs) + self.origin = origin + self.available_extensions = extensions + self.available_subprotocols = subprotocols + self.extra_headers = extra_headers + self.user_agent_header = user_agent_header + + def write_http_request(self, path: str, headers: Headers) -> None: + """ + Write request line and headers to the HTTP request. + + """ + self.path = path + self.request_headers = headers + + if self.debug: + self.logger.debug("> GET %s HTTP/1.1", path) + for key, value in headers.raw_items(): + self.logger.debug("> %s: %s", key, value) + + # Since the path and headers only contain ASCII characters, + # we can keep this simple. + request = f"GET {path} HTTP/1.1\r\n" + request += str(headers) + + self.transport.write(request.encode()) + + async def read_http_response(self) -> Tuple[int, Headers]: + """ + Read status line and headers from the HTTP response. + + If the response contains a body, it may be read from ``self.reader`` + after this coroutine returns. + + Raises: + InvalidMessage: If the HTTP message is malformed or isn't an + HTTP/1.1 GET response. + + """ + try: + status_code, reason, headers = await read_response(self.reader) + except Exception as exc: + raise InvalidMessage("did not receive a valid HTTP response") from exc + + if self.debug: + self.logger.debug("< HTTP/1.1 %d %s", status_code, reason) + for key, value in headers.raw_items(): + self.logger.debug("< %s: %s", key, value) + + self.response_headers = headers + + return status_code, self.response_headers + + @staticmethod + def process_extensions( + headers: Headers, + available_extensions: Optional[Sequence[ClientExtensionFactory]], + ) -> List[Extension]: + """ + Handle the Sec-WebSocket-Extensions HTTP response header. + + Check that each extension is supported, as well as its parameters. + + Return the list of accepted extensions. + + Raise :exc:`~websockets.exceptions.InvalidHandshake` to abort the + connection. + + :rfc:`6455` leaves the rules up to the specification of each + :extension. + + To provide this level of flexibility, for each extension accepted by + the server, we check for a match with each extension available in the + client configuration. If no match is found, an exception is raised. + + If several variants of the same extension are accepted by the server, + it may be configured several times, which won't make sense in general. + Extensions must implement their own requirements. For this purpose, + the list of previously accepted extensions is provided. + + Other requirements, for example related to mandatory extensions or the + order of extensions, may be implemented by overriding this method. + + """ + accepted_extensions: List[Extension] = [] + + header_values = headers.get_all("Sec-WebSocket-Extensions") + + if header_values: + if available_extensions is None: + raise InvalidHandshake("no extensions supported") + + parsed_header_values: List[ExtensionHeader] = sum( + [parse_extension(header_value) for header_value in header_values], [] + ) + + for name, response_params in parsed_header_values: + for extension_factory in available_extensions: + # Skip non-matching extensions based on their name. + if extension_factory.name != name: + continue + + # Skip non-matching extensions based on their params. + try: + extension = extension_factory.process_response_params( + response_params, accepted_extensions + ) + except NegotiationError: + continue + + # Add matching extension to the final list. + accepted_extensions.append(extension) + + # Break out of the loop once we have a match. + break + + # If we didn't break from the loop, no extension in our list + # matched what the server sent. Fail the connection. + else: + raise NegotiationError( + f"Unsupported extension: " + f"name = {name}, params = {response_params}" + ) + + return accepted_extensions + + @staticmethod + def process_subprotocol( + headers: Headers, available_subprotocols: Optional[Sequence[Subprotocol]] + ) -> Optional[Subprotocol]: + """ + Handle the Sec-WebSocket-Protocol HTTP response header. + + Check that it contains exactly one supported subprotocol. + + Return the selected subprotocol. + + """ + subprotocol: Optional[Subprotocol] = None + + header_values = headers.get_all("Sec-WebSocket-Protocol") + + if header_values: + if available_subprotocols is None: + raise InvalidHandshake("no subprotocols supported") + + parsed_header_values: Sequence[Subprotocol] = sum( + [parse_subprotocol(header_value) for header_value in header_values], [] + ) + + if len(parsed_header_values) > 1: + subprotocols = ", ".join(parsed_header_values) + raise InvalidHandshake(f"multiple subprotocols: {subprotocols}") + + subprotocol = parsed_header_values[0] + + if subprotocol not in available_subprotocols: + raise NegotiationError(f"unsupported subprotocol: {subprotocol}") + + return subprotocol + + async def handshake( + self, + wsuri: WebSocketURI, + origin: Optional[Origin] = None, + available_extensions: Optional[Sequence[ClientExtensionFactory]] = None, + available_subprotocols: Optional[Sequence[Subprotocol]] = None, + extra_headers: Optional[HeadersLike] = None, + ) -> None: + """ + Perform the client side of the opening handshake. + + Args: + wsuri: URI of the WebSocket server. + origin: Value of the ``Origin`` header. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing + preference. + extra_headers: Arbitrary HTTP headers to add to the handshake request. + + Raises: + InvalidHandshake: If the handshake fails. + + """ + request_headers = Headers() + + request_headers["Host"] = build_host(wsuri.host, wsuri.port, wsuri.secure) + + if wsuri.user_info: + request_headers["Authorization"] = build_authorization_basic( + *wsuri.user_info + ) + + if origin is not None: + request_headers["Origin"] = origin + + key = build_request(request_headers) + + if available_extensions is not None: + extensions_header = build_extension( + [ + (extension_factory.name, extension_factory.get_request_params()) + for extension_factory in available_extensions + ] + ) + request_headers["Sec-WebSocket-Extensions"] = extensions_header + + if available_subprotocols is not None: + protocol_header = build_subprotocol(available_subprotocols) + request_headers["Sec-WebSocket-Protocol"] = protocol_header + + if self.extra_headers is not None: + request_headers.update(self.extra_headers) + + if self.user_agent_header is not None: + request_headers.setdefault("User-Agent", self.user_agent_header) + + self.write_http_request(wsuri.resource_name, request_headers) + + status_code, response_headers = await self.read_http_response() + if status_code in (301, 302, 303, 307, 308): + if "Location" not in response_headers: + raise InvalidHeader("Location") + raise RedirectHandshake(response_headers["Location"]) + elif status_code != 101: + raise InvalidStatusCode(status_code, response_headers) + + check_response(response_headers, key) + + self.extensions = self.process_extensions( + response_headers, available_extensions + ) + + self.subprotocol = self.process_subprotocol( + response_headers, available_subprotocols + ) + + self.connection_open() + + +class Connect: + """ + Connect to the WebSocket server at ``uri``. + + Awaiting :func:`connect` yields a :class:`WebSocketClientProtocol` which + can then be used to send and receive messages. + + :func:`connect` can be used as a asynchronous context manager:: + + async with websockets.connect(...) as websocket: + ... + + The connection is closed automatically when exiting the context. + + :func:`connect` can be used as an infinite asynchronous iterator to + reconnect automatically on errors:: + + async for websocket in websockets.connect(...): + try: + ... + except websockets.ConnectionClosed: + continue + + The connection is closed automatically after each iteration of the loop. + + If an error occurs while establishing the connection, :func:`connect` + retries with exponential backoff. The backoff delay starts at three + seconds and increases up to one minute. + + If an error occurs in the body of the loop, you can handle the exception + and :func:`connect` will reconnect with the next iteration; or you can + let the exception bubble up and break out of the loop. This lets you + decide which errors trigger a reconnection and which errors are fatal. + + Args: + uri: URI of the WebSocket server. + create_protocol: Factory for the :class:`asyncio.Protocol` managing + the connection. It defaults to :class:`WebSocketClientProtocol`. + Set it to a wrapper or a subclass to customize connection handling. + logger: Logger for this client. + It defaults to ``logging.getLogger("websockets.client")``. + See the :doc:`logging guide <../../topics/logging>` for details. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + origin: Value of the ``Origin`` header, for servers that require it. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing + preference. + extra_headers: Arbitrary HTTP headers to add to the handshake request. + user_agent_header: Value of the ``User-Agent`` request header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. + Setting it to :obj:`None` removes the header. + open_timeout: Timeout for opening the connection in seconds. + :obj:`None` disables the timeout. + + See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the + documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, + ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. + + Any other keyword arguments are passed the event loop's + :meth:`~asyncio.loop.create_connection` method. + + For example: + + * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enforce TLS + settings. When connecting to a ``wss://`` URI, if ``ssl`` isn't + provided, a TLS context is created + with :func:`~ssl.create_default_context`. + + * You can set ``host`` and ``port`` to connect to a different host and + port from those found in ``uri``. This only changes the destination of + the TCP connection. The host name from ``uri`` is still used in the TLS + handshake for secure connections and in the ``Host`` header. + + Raises: + InvalidURI: If ``uri`` isn't a valid WebSocket URI. + OSError: If the TCP connection fails. + InvalidHandshake: If the opening handshake fails. + ~asyncio.TimeoutError: If the opening handshake times out. + + """ + + MAX_REDIRECTS_ALLOWED = 10 + + def __init__( + self, + uri: str, + *, + create_protocol: Optional[Callable[..., WebSocketClientProtocol]] = None, + logger: Optional[LoggerLike] = None, + compression: Optional[str] = "deflate", + origin: Optional[Origin] = None, + extensions: Optional[Sequence[ClientExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + extra_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, + open_timeout: Optional[float] = 10, + ping_interval: Optional[float] = 20, + ping_timeout: Optional[float] = 20, + close_timeout: Optional[float] = None, + max_size: Optional[int] = 2**20, + max_queue: Optional[int] = 2**5, + read_limit: int = 2**16, + write_limit: int = 2**16, + **kwargs: Any, + ) -> None: + # Backwards compatibility: close_timeout used to be called timeout. + timeout: Optional[float] = kwargs.pop("timeout", None) + if timeout is None: + timeout = 10 + else: + warnings.warn("rename timeout to close_timeout", DeprecationWarning) + # If both are specified, timeout is ignored. + if close_timeout is None: + close_timeout = timeout + + # Backwards compatibility: create_protocol used to be called klass. + klass: Optional[Type[WebSocketClientProtocol]] = kwargs.pop("klass", None) + if klass is None: + klass = WebSocketClientProtocol + else: + warnings.warn("rename klass to create_protocol", DeprecationWarning) + # If both are specified, klass is ignored. + if create_protocol is None: + create_protocol = klass + + # Backwards compatibility: recv() used to return None on closed connections + legacy_recv: bool = kwargs.pop("legacy_recv", False) + + # Backwards compatibility: the loop parameter used to be supported. + _loop: Optional[asyncio.AbstractEventLoop] = kwargs.pop("loop", None) + if _loop is None: + loop = asyncio.get_event_loop() + else: + loop = _loop + warnings.warn("remove loop argument", DeprecationWarning) + + wsuri = parse_uri(uri) + if wsuri.secure: + kwargs.setdefault("ssl", True) + elif kwargs.get("ssl") is not None: + raise ValueError( + "connect() received a ssl argument for a ws:// URI, " + "use a wss:// URI to enable TLS" + ) + + if compression == "deflate": + extensions = enable_client_permessage_deflate(extensions) + elif compression is not None: + raise ValueError(f"unsupported compression: {compression}") + + if subprotocols is not None: + validate_subprotocols(subprotocols) + + factory = functools.partial( + create_protocol, + logger=logger, + origin=origin, + extensions=extensions, + subprotocols=subprotocols, + extra_headers=extra_headers, + user_agent_header=user_agent_header, + ping_interval=ping_interval, + ping_timeout=ping_timeout, + close_timeout=close_timeout, + max_size=max_size, + max_queue=max_queue, + read_limit=read_limit, + write_limit=write_limit, + host=wsuri.host, + port=wsuri.port, + secure=wsuri.secure, + legacy_recv=legacy_recv, + loop=_loop, + ) + + if kwargs.pop("unix", False): + path: Optional[str] = kwargs.pop("path", None) + create_connection = functools.partial( + loop.create_unix_connection, factory, path, **kwargs + ) + else: + host: Optional[str] + port: Optional[int] + if kwargs.get("sock") is None: + host, port = wsuri.host, wsuri.port + else: + # If sock is given, host and port shouldn't be specified. + host, port = None, None + if kwargs.get("ssl"): + kwargs.setdefault("server_hostname", wsuri.host) + # If host and port are given, override values from the URI. + host = kwargs.pop("host", host) + port = kwargs.pop("port", port) + create_connection = functools.partial( + loop.create_connection, factory, host, port, **kwargs + ) + + self.open_timeout = open_timeout + if logger is None: + logger = logging.getLogger("websockets.client") + self.logger = logger + + # This is a coroutine function. + self._create_connection = create_connection + self._uri = uri + self._wsuri = wsuri + + def handle_redirect(self, uri: str) -> None: + # Update the state of this instance to connect to a new URI. + old_uri = self._uri + old_wsuri = self._wsuri + new_uri = urllib.parse.urljoin(old_uri, uri) + new_wsuri = parse_uri(new_uri) + + # Forbid TLS downgrade. + if old_wsuri.secure and not new_wsuri.secure: + raise SecurityError("redirect from WSS to WS") + + same_origin = ( + old_wsuri.host == new_wsuri.host and old_wsuri.port == new_wsuri.port + ) + + # Rewrite the host and port arguments for cross-origin redirects. + # This preserves connection overrides with the host and port + # arguments if the redirect points to the same host and port. + if not same_origin: + # Replace the host and port argument passed to the protocol factory. + factory = self._create_connection.args[0] + factory = functools.partial( + factory.func, + *factory.args, + **dict(factory.keywords, host=new_wsuri.host, port=new_wsuri.port), + ) + # Replace the host and port argument passed to create_connection. + self._create_connection = functools.partial( + self._create_connection.func, + *(factory, new_wsuri.host, new_wsuri.port), + **self._create_connection.keywords, + ) + + # Set the new WebSocket URI. This suffices for same-origin redirects. + self._uri = new_uri + self._wsuri = new_wsuri + + # async for ... in connect(...): + + BACKOFF_MIN = 1.92 + BACKOFF_MAX = 60.0 + BACKOFF_FACTOR = 1.618 + BACKOFF_INITIAL = 5 + + async def __aiter__(self) -> AsyncIterator[WebSocketClientProtocol]: + backoff_delay = self.BACKOFF_MIN + while True: + try: + async with self as protocol: + yield protocol + except Exception: + # Add a random initial delay between 0 and 5 seconds. + # See 7.2.3. Recovering from Abnormal Closure in RFC 6544. + if backoff_delay == self.BACKOFF_MIN: + initial_delay = random.random() * self.BACKOFF_INITIAL + self.logger.info( + "! connect failed; reconnecting in %.1f seconds", + initial_delay, + exc_info=True, + ) + await asyncio.sleep(initial_delay) + else: + self.logger.info( + "! connect failed again; retrying in %d seconds", + int(backoff_delay), + exc_info=True, + ) + await asyncio.sleep(int(backoff_delay)) + # Increase delay with truncated exponential backoff. + backoff_delay = backoff_delay * self.BACKOFF_FACTOR + backoff_delay = min(backoff_delay, self.BACKOFF_MAX) + continue + else: + # Connection succeeded - reset backoff delay + backoff_delay = self.BACKOFF_MIN + + # async with connect(...) as ...: + + async def __aenter__(self) -> WebSocketClientProtocol: + return await self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + await self.protocol.close() + + # ... = await connect(...) + + def __await__(self) -> Generator[Any, None, WebSocketClientProtocol]: + # Create a suitable iterator by calling __await__ on a coroutine. + return self.__await_impl_timeout__().__await__() + + async def __await_impl_timeout__(self) -> WebSocketClientProtocol: + async with asyncio_timeout(self.open_timeout): + return await self.__await_impl__() + + async def __await_impl__(self) -> WebSocketClientProtocol: + for redirects in range(self.MAX_REDIRECTS_ALLOWED): + _transport, _protocol = await self._create_connection() + protocol = cast(WebSocketClientProtocol, _protocol) + try: + await protocol.handshake( + self._wsuri, + origin=protocol.origin, + available_extensions=protocol.available_extensions, + available_subprotocols=protocol.available_subprotocols, + extra_headers=protocol.extra_headers, + ) + except RedirectHandshake as exc: + protocol.fail_connection() + await protocol.wait_closed() + self.handle_redirect(exc.uri) + # Avoid leaking a connected socket when the handshake fails. + except (Exception, asyncio.CancelledError): + protocol.fail_connection() + await protocol.wait_closed() + raise + else: + self.protocol = protocol + return protocol + else: + raise SecurityError("too many redirects") + + # ... = yield from connect(...) - remove when dropping Python < 3.10 + + __iter__ = __await__ + + +connect = Connect + + +def unix_connect( + path: Optional[str] = None, + uri: str = "ws://localhost/", + **kwargs: Any, +) -> Connect: + """ + Similar to :func:`connect`, but for connecting to a Unix socket. + + This function builds upon the event loop's + :meth:`~asyncio.loop.create_unix_connection` method. + + It is only available on Unix. + + It's mainly useful for debugging servers listening on Unix sockets. + + Args: + path: File system path to the Unix socket. + uri: URI of the WebSocket server; the host is used in the TLS + handshake for secure connections and in the ``Host`` header. + + """ + return connect(uri=uri, path=path, unix=True, **kwargs) diff --git a/websockets/legacy/compatibility.py b/websockets/legacy/compatibility.py new file mode 100644 index 0000000..6bd01e7 --- /dev/null +++ b/websockets/legacy/compatibility.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +import sys + + +__all__ = ["asyncio_timeout"] + + +if sys.version_info[:2] >= (3, 11): + from asyncio import timeout as asyncio_timeout # noqa: F401 +else: + from .async_timeout import timeout as asyncio_timeout # noqa: F401 diff --git a/websockets/legacy/framing.py b/websockets/legacy/framing.py new file mode 100644 index 0000000..b77b869 --- /dev/null +++ b/websockets/legacy/framing.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +import struct +from typing import Any, Awaitable, Callable, NamedTuple, Optional, Sequence, Tuple + +from .. import extensions, frames +from ..exceptions import PayloadTooBig, ProtocolError + + +try: + from ..speedups import apply_mask +except ImportError: + from ..utils import apply_mask + + +class Frame(NamedTuple): + fin: bool + opcode: frames.Opcode + data: bytes + rsv1: bool = False + rsv2: bool = False + rsv3: bool = False + + @property + def new_frame(self) -> frames.Frame: + return frames.Frame( + self.opcode, + self.data, + self.fin, + self.rsv1, + self.rsv2, + self.rsv3, + ) + + def __str__(self) -> str: + return str(self.new_frame) + + def check(self) -> None: + return self.new_frame.check() + + @classmethod + async def read( + cls, + reader: Callable[[int], Awaitable[bytes]], + *, + mask: bool, + max_size: Optional[int] = None, + extensions: Optional[Sequence[extensions.Extension]] = None, + ) -> Frame: + """ + Read a WebSocket frame. + + Args: + reader: Coroutine that reads exactly the requested number of + bytes, unless the end of file is reached. + mask: Whether the frame should be masked i.e. whether the read + happens on the server side. + max_size: Maximum payload size in bytes. + extensions: List of extensions, applied in reverse order. + + Raises: + PayloadTooBig: If the frame exceeds ``max_size``. + ProtocolError: If the frame contains incorrect values. + + """ + + # Read the header. + data = await reader(2) + head1, head2 = struct.unpack("!BB", data) + + # While not Pythonic, this is marginally faster than calling bool(). + fin = True if head1 & 0b10000000 else False + rsv1 = True if head1 & 0b01000000 else False + rsv2 = True if head1 & 0b00100000 else False + rsv3 = True if head1 & 0b00010000 else False + + try: + opcode = frames.Opcode(head1 & 0b00001111) + except ValueError as exc: + raise ProtocolError("invalid opcode") from exc + + if (True if head2 & 0b10000000 else False) != mask: + raise ProtocolError("incorrect masking") + + length = head2 & 0b01111111 + if length == 126: + data = await reader(2) + (length,) = struct.unpack("!H", data) + elif length == 127: + data = await reader(8) + (length,) = struct.unpack("!Q", data) + if max_size is not None and length > max_size: + raise PayloadTooBig(f"over size limit ({length} > {max_size} bytes)") + if mask: + mask_bits = await reader(4) + + # Read the data. + data = await reader(length) + if mask: + data = apply_mask(data, mask_bits) + + new_frame = frames.Frame(opcode, data, fin, rsv1, rsv2, rsv3) + + if extensions is None: + extensions = [] + for extension in reversed(extensions): + new_frame = extension.decode(new_frame, max_size=max_size) + + new_frame.check() + + return cls( + new_frame.fin, + new_frame.opcode, + new_frame.data, + new_frame.rsv1, + new_frame.rsv2, + new_frame.rsv3, + ) + + def write( + self, + write: Callable[[bytes], Any], + *, + mask: bool, + extensions: Optional[Sequence[extensions.Extension]] = None, + ) -> None: + """ + Write a WebSocket frame. + + Args: + frame: Frame to write. + write: Function that writes bytes. + mask: Whether the frame should be masked i.e. whether the write + happens on the client side. + extensions: List of extensions, applied in order. + + Raises: + ProtocolError: If the frame contains incorrect values. + + """ + # The frame is written in a single call to write in order to prevent + # TCP fragmentation. See #68 for details. This also makes it safe to + # send frames concurrently from multiple coroutines. + write(self.new_frame.serialize(mask=mask, extensions=extensions)) + + +# Backwards compatibility with previously documented public APIs +from ..frames import ( # noqa: E402, F401, I001 + Close, + prepare_ctrl as encode_data, + prepare_data, +) + + +def parse_close(data: bytes) -> Tuple[int, str]: + """ + Parse the payload from a close frame. + + Returns: + Close code and reason. + + Raises: + ProtocolError: If data is ill-formed. + UnicodeDecodeError: If the reason isn't valid UTF-8. + + """ + close = Close.parse(data) + return close.code, close.reason + + +def serialize_close(code: int, reason: str) -> bytes: + """ + Serialize the payload for a close frame. + + """ + return Close(code, reason).serialize() diff --git a/websockets/legacy/handshake.py b/websockets/legacy/handshake.py new file mode 100644 index 0000000..ad8faf0 --- /dev/null +++ b/websockets/legacy/handshake.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +import base64 +import binascii +from typing import List + +from ..datastructures import Headers, MultipleValuesError +from ..exceptions import InvalidHeader, InvalidHeaderValue, InvalidUpgrade +from ..headers import parse_connection, parse_upgrade +from ..typing import ConnectionOption, UpgradeProtocol +from ..utils import accept_key as accept, generate_key + + +__all__ = ["build_request", "check_request", "build_response", "check_response"] + + +def build_request(headers: Headers) -> str: + """ + Build a handshake request to send to the server. + + Update request headers passed in argument. + + Args: + headers: Handshake request headers. + + Returns: + str: ``key`` that must be passed to :func:`check_response`. + + """ + key = generate_key() + headers["Upgrade"] = "websocket" + headers["Connection"] = "Upgrade" + headers["Sec-WebSocket-Key"] = key + headers["Sec-WebSocket-Version"] = "13" + return key + + +def check_request(headers: Headers) -> str: + """ + Check a handshake request received from the client. + + This function doesn't verify that the request is an HTTP/1.1 or higher GET + request and doesn't perform ``Host`` and ``Origin`` checks. These controls + are usually performed earlier in the HTTP request handling code. They're + the responsibility of the caller. + + Args: + headers: Handshake request headers. + + Returns: + str: ``key`` that must be passed to :func:`build_response`. + + Raises: + InvalidHandshake: If the handshake request is invalid. + Then, the server must return a 400 Bad Request error. + + """ + connection: List[ConnectionOption] = sum( + [parse_connection(value) for value in headers.get_all("Connection")], [] + ) + + if not any(value.lower() == "upgrade" for value in connection): + raise InvalidUpgrade("Connection", ", ".join(connection)) + + upgrade: List[UpgradeProtocol] = sum( + [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] + ) + + # For compatibility with non-strict implementations, ignore case when + # checking the Upgrade header. The RFC always uses "websocket", except + # in section 11.2. (IANA registration) where it uses "WebSocket". + if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): + raise InvalidUpgrade("Upgrade", ", ".join(upgrade)) + + try: + s_w_key = headers["Sec-WebSocket-Key"] + except KeyError as exc: + raise InvalidHeader("Sec-WebSocket-Key") from exc + except MultipleValuesError as exc: + raise InvalidHeader( + "Sec-WebSocket-Key", "more than one Sec-WebSocket-Key header found" + ) from exc + + try: + raw_key = base64.b64decode(s_w_key.encode(), validate=True) + except binascii.Error as exc: + raise InvalidHeaderValue("Sec-WebSocket-Key", s_w_key) from exc + if len(raw_key) != 16: + raise InvalidHeaderValue("Sec-WebSocket-Key", s_w_key) + + try: + s_w_version = headers["Sec-WebSocket-Version"] + except KeyError as exc: + raise InvalidHeader("Sec-WebSocket-Version") from exc + except MultipleValuesError as exc: + raise InvalidHeader( + "Sec-WebSocket-Version", "more than one Sec-WebSocket-Version header found" + ) from exc + + if s_w_version != "13": + raise InvalidHeaderValue("Sec-WebSocket-Version", s_w_version) + + return s_w_key + + +def build_response(headers: Headers, key: str) -> None: + """ + Build a handshake response to send to the client. + + Update response headers passed in argument. + + Args: + headers: Handshake response headers. + key: Returned by :func:`check_request`. + + """ + headers["Upgrade"] = "websocket" + headers["Connection"] = "Upgrade" + headers["Sec-WebSocket-Accept"] = accept(key) + + +def check_response(headers: Headers, key: str) -> None: + """ + Check a handshake response received from the server. + + This function doesn't verify that the response is an HTTP/1.1 or higher + response with a 101 status code. These controls are the responsibility of + the caller. + + Args: + headers: Handshake response headers. + key: Returned by :func:`build_request`. + + Raises: + InvalidHandshake: If the handshake response is invalid. + + """ + connection: List[ConnectionOption] = sum( + [parse_connection(value) for value in headers.get_all("Connection")], [] + ) + + if not any(value.lower() == "upgrade" for value in connection): + raise InvalidUpgrade("Connection", " ".join(connection)) + + upgrade: List[UpgradeProtocol] = sum( + [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] + ) + + # For compatibility with non-strict implementations, ignore case when + # checking the Upgrade header. The RFC always uses "websocket", except + # in section 11.2. (IANA registration) where it uses "WebSocket". + if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): + raise InvalidUpgrade("Upgrade", ", ".join(upgrade)) + + try: + s_w_accept = headers["Sec-WebSocket-Accept"] + except KeyError as exc: + raise InvalidHeader("Sec-WebSocket-Accept") from exc + except MultipleValuesError as exc: + raise InvalidHeader( + "Sec-WebSocket-Accept", "more than one Sec-WebSocket-Accept header found" + ) from exc + + if s_w_accept != accept(key): + raise InvalidHeaderValue("Sec-WebSocket-Accept", s_w_accept) diff --git a/websockets/legacy/http.py b/websockets/legacy/http.py new file mode 100644 index 0000000..2ac7f70 --- /dev/null +++ b/websockets/legacy/http.py @@ -0,0 +1,201 @@ +from __future__ import annotations + +import asyncio +import re +from typing import Tuple + +from ..datastructures import Headers +from ..exceptions import SecurityError + + +__all__ = ["read_request", "read_response"] + +MAX_HEADERS = 128 +MAX_LINE = 8192 + + +def d(value: bytes) -> str: + """ + Decode a bytestring for interpolating into an error message. + + """ + return value.decode(errors="backslashreplace") + + +# See https://www.rfc-editor.org/rfc/rfc7230.html#appendix-B. + +# Regex for validating header names. + +_token_re = re.compile(rb"[-!#$%&\'*+.^_`|~0-9a-zA-Z]+") + +# Regex for validating header values. + +# We don't attempt to support obsolete line folding. + +# Include HTAB (\x09), SP (\x20), VCHAR (\x21-\x7e), obs-text (\x80-\xff). + +# The ABNF is complicated because it attempts to express that optional +# whitespace is ignored. We strip whitespace and don't revalidate that. + +# See also https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189 + +_value_re = re.compile(rb"[\x09\x20-\x7e\x80-\xff]*") + + +async def read_request(stream: asyncio.StreamReader) -> Tuple[str, Headers]: + """ + Read an HTTP/1.1 GET request and return ``(path, headers)``. + + ``path`` isn't URL-decoded or validated in any way. + + ``path`` and ``headers`` are expected to contain only ASCII characters. + Other characters are represented with surrogate escapes. + + :func:`read_request` doesn't attempt to read the request body because + WebSocket handshake requests don't have one. If the request contains a + body, it may be read from ``stream`` after this coroutine returns. + + Args: + stream: Input to read the request from. + + Raises: + EOFError: If the connection is closed without a full HTTP request. + SecurityError: If the request exceeds a security limit. + ValueError: If the request isn't well formatted. + + """ + # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.1 + + # Parsing is simple because fixed values are expected for method and + # version and because path isn't checked. Since WebSocket software tends + # to implement HTTP/1.1 strictly, there's little need for lenient parsing. + + try: + request_line = await read_line(stream) + except EOFError as exc: + raise EOFError("connection closed while reading HTTP request line") from exc + + try: + method, raw_path, version = request_line.split(b" ", 2) + except ValueError: # not enough values to unpack (expected 3, got 1-2) + raise ValueError(f"invalid HTTP request line: {d(request_line)}") from None + + if method != b"GET": + raise ValueError(f"unsupported HTTP method: {d(method)}") + if version != b"HTTP/1.1": + raise ValueError(f"unsupported HTTP version: {d(version)}") + path = raw_path.decode("ascii", "surrogateescape") + + headers = await read_headers(stream) + + return path, headers + + +async def read_response(stream: asyncio.StreamReader) -> Tuple[int, str, Headers]: + """ + Read an HTTP/1.1 response and return ``(status_code, reason, headers)``. + + ``reason`` and ``headers`` are expected to contain only ASCII characters. + Other characters are represented with surrogate escapes. + + :func:`read_request` doesn't attempt to read the response body because + WebSocket handshake responses don't have one. If the response contains a + body, it may be read from ``stream`` after this coroutine returns. + + Args: + stream: Input to read the response from. + + Raises: + EOFError: If the connection is closed without a full HTTP response. + SecurityError: If the response exceeds a security limit. + ValueError: If the response isn't well formatted. + + """ + # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.2 + + # As in read_request, parsing is simple because a fixed value is expected + # for version, status_code is a 3-digit number, and reason can be ignored. + + try: + status_line = await read_line(stream) + except EOFError as exc: + raise EOFError("connection closed while reading HTTP status line") from exc + + try: + version, raw_status_code, raw_reason = status_line.split(b" ", 2) + except ValueError: # not enough values to unpack (expected 3, got 1-2) + raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None + + if version != b"HTTP/1.1": + raise ValueError(f"unsupported HTTP version: {d(version)}") + try: + status_code = int(raw_status_code) + except ValueError: # invalid literal for int() with base 10 + raise ValueError(f"invalid HTTP status code: {d(raw_status_code)}") from None + if not 100 <= status_code < 1000: + raise ValueError(f"unsupported HTTP status code: {d(raw_status_code)}") + if not _value_re.fullmatch(raw_reason): + raise ValueError(f"invalid HTTP reason phrase: {d(raw_reason)}") + reason = raw_reason.decode() + + headers = await read_headers(stream) + + return status_code, reason, headers + + +async def read_headers(stream: asyncio.StreamReader) -> Headers: + """ + Read HTTP headers from ``stream``. + + Non-ASCII characters are represented with surrogate escapes. + + """ + # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2 + + # We don't attempt to support obsolete line folding. + + headers = Headers() + for _ in range(MAX_HEADERS + 1): + try: + line = await read_line(stream) + except EOFError as exc: + raise EOFError("connection closed while reading HTTP headers") from exc + if line == b"": + break + + try: + raw_name, raw_value = line.split(b":", 1) + except ValueError: # not enough values to unpack (expected 2, got 1) + raise ValueError(f"invalid HTTP header line: {d(line)}") from None + if not _token_re.fullmatch(raw_name): + raise ValueError(f"invalid HTTP header name: {d(raw_name)}") + raw_value = raw_value.strip(b" \t") + if not _value_re.fullmatch(raw_value): + raise ValueError(f"invalid HTTP header value: {d(raw_value)}") + + name = raw_name.decode("ascii") # guaranteed to be ASCII at this point + value = raw_value.decode("ascii", "surrogateescape") + headers[name] = value + + else: + raise SecurityError("too many HTTP headers") + + return headers + + +async def read_line(stream: asyncio.StreamReader) -> bytes: + """ + Read a single line from ``stream``. + + CRLF is stripped from the return value. + + """ + # Security: this is bounded by the StreamReader's limit (default = 32 KiB). + line = await stream.readline() + # Security: this guarantees header values are small (hard-coded = 8 KiB) + if len(line) > MAX_LINE: + raise SecurityError("line too long") + # Not mandatory but safe - https://www.rfc-editor.org/rfc/rfc7230.html#section-3.5 + if not line.endswith(b"\r\n"): + raise EOFError("line without CRLF") + return line[:-2] diff --git a/websockets/legacy/protocol.py b/websockets/legacy/protocol.py new file mode 100644 index 0000000..19cee0e --- /dev/null +++ b/websockets/legacy/protocol.py @@ -0,0 +1,1645 @@ +from __future__ import annotations + +import asyncio +import codecs +import collections +import logging +import random +import ssl +import struct +import sys +import time +import uuid +import warnings +from typing import ( + Any, + AsyncIterable, + AsyncIterator, + Awaitable, + Callable, + Deque, + Dict, + Iterable, + List, + Mapping, + Optional, + Tuple, + Union, + cast, +) + +from ..datastructures import Headers +from ..exceptions import ( + ConnectionClosed, + ConnectionClosedError, + ConnectionClosedOK, + InvalidState, + PayloadTooBig, + ProtocolError, +) +from ..extensions import Extension +from ..frames import ( + OK_CLOSE_CODES, + OP_BINARY, + OP_CLOSE, + OP_CONT, + OP_PING, + OP_PONG, + OP_TEXT, + Close, + CloseCode, + Opcode, + prepare_ctrl, + prepare_data, +) +from ..protocol import State +from ..typing import Data, LoggerLike, Subprotocol +from .compatibility import asyncio_timeout +from .framing import Frame + + +__all__ = ["WebSocketCommonProtocol", "broadcast"] + + +# In order to ensure consistency, the code always checks the current value of +# WebSocketCommonProtocol.state before assigning a new value and never yields +# between the check and the assignment. + + +class WebSocketCommonProtocol(asyncio.Protocol): + """ + WebSocket connection. + + :class:`WebSocketCommonProtocol` provides APIs shared between WebSocket + servers and clients. You shouldn't use it directly. Instead, use + :class:`~websockets.client.WebSocketClientProtocol` or + :class:`~websockets.server.WebSocketServerProtocol`. + + This documentation focuses on low-level details that aren't covered in the + documentation of :class:`~websockets.client.WebSocketClientProtocol` and + :class:`~websockets.server.WebSocketServerProtocol` for the sake of + simplicity. + + Once the connection is open, a Ping_ frame is sent every ``ping_interval`` + seconds. This serves as a keepalive. It helps keeping the connection open, + especially in the presence of proxies with short timeouts on inactive + connections. Set ``ping_interval`` to :obj:`None` to disable this behavior. + + .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 + + If the corresponding Pong_ frame isn't received within ``ping_timeout`` + seconds, the connection is considered unusable and is closed with code 1011. + This ensures that the remote endpoint remains responsive. Set + ``ping_timeout`` to :obj:`None` to disable this behavior. + + .. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + + See the discussion of :doc:`timeouts <../../topics/timeouts>` for details. + + The ``close_timeout`` parameter defines a maximum wait time for completing + the closing handshake and terminating the TCP connection. For legacy + reasons, :meth:`close` completes in at most ``5 * close_timeout`` seconds + for clients and ``4 * close_timeout`` for servers. + + ``close_timeout`` is a parameter of the protocol because websockets usually + calls :meth:`close` implicitly upon exit: + + * on the client side, when using :func:`~websockets.client.connect` as a + context manager; + * on the server side, when the connection handler terminates. + + To apply a timeout to any other API, wrap it in :func:`~asyncio.timeout` or + :func:`~asyncio.wait_for`. + + The ``max_size`` parameter enforces the maximum size for incoming messages + in bytes. The default value is 1 MiB. If a larger message is received, + :meth:`recv` will raise :exc:`~websockets.exceptions.ConnectionClosedError` + and the connection will be closed with code 1009. + + The ``max_queue`` parameter sets the maximum length of the queue that + holds incoming messages. The default value is ``32``. Messages are added + to an in-memory queue when they're received; then :meth:`recv` pops from + that queue. In order to prevent excessive memory consumption when + messages are received faster than they can be processed, the queue must + be bounded. If the queue fills up, the protocol stops processing incoming + data until :meth:`recv` is called. In this situation, various receive + buffers (at least in :mod:`asyncio` and in the OS) will fill up, then the + TCP receive window will shrink, slowing down transmission to avoid packet + loss. + + Since Python can use up to 4 bytes of memory to represent a single + character, each connection may use up to ``4 * max_size * max_queue`` + bytes of memory to store incoming messages. By default, this is 128 MiB. + You may want to lower the limits, depending on your application's + requirements. + + The ``read_limit`` argument sets the high-water limit of the buffer for + incoming bytes. The low-water limit is half the high-water limit. The + default value is 64 KiB, half of asyncio's default (based on the current + implementation of :class:`~asyncio.StreamReader`). + + The ``write_limit`` argument sets the high-water limit of the buffer for + outgoing bytes. The low-water limit is a quarter of the high-water limit. + The default value is 64 KiB, equal to asyncio's default (based on the + current implementation of ``FlowControlMixin``). + + See the discussion of :doc:`memory usage <../../topics/memory>` for details. + + Args: + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.protocol")``. + See the :doc:`logging guide <../../topics/logging>` for details. + ping_interval: Delay between keepalive pings in seconds. + :obj:`None` disables keepalive pings. + ping_timeout: Timeout for keepalive pings in seconds. + :obj:`None` disables timeouts. + close_timeout: Timeout for closing the connection in seconds. + For legacy reasons, the actual timeout is 4 or 5 times larger. + max_size: Maximum size of incoming messages in bytes. + :obj:`None` disables the limit. + max_queue: Maximum number of incoming messages in receive buffer. + :obj:`None` disables the limit. + read_limit: High-water mark of read buffer in bytes. + write_limit: High-water mark of write buffer in bytes. + + """ + + # There are only two differences between the client-side and server-side + # behavior: masking the payload and closing the underlying TCP connection. + # Set is_client = True/False and side = "client"/"server" to pick a side. + is_client: bool + side: str = "undefined" + + def __init__( + self, + *, + logger: Optional[LoggerLike] = None, + ping_interval: Optional[float] = 20, + ping_timeout: Optional[float] = 20, + close_timeout: Optional[float] = None, + max_size: Optional[int] = 2**20, + max_queue: Optional[int] = 2**5, + read_limit: int = 2**16, + write_limit: int = 2**16, + # The following arguments are kept only for backwards compatibility. + host: Optional[str] = None, + port: Optional[int] = None, + secure: Optional[bool] = None, + legacy_recv: bool = False, + loop: Optional[asyncio.AbstractEventLoop] = None, + timeout: Optional[float] = None, + ) -> None: + if legacy_recv: # pragma: no cover + warnings.warn("legacy_recv is deprecated", DeprecationWarning) + + # Backwards compatibility: close_timeout used to be called timeout. + if timeout is None: + timeout = 10 + else: + warnings.warn("rename timeout to close_timeout", DeprecationWarning) + # If both are specified, timeout is ignored. + if close_timeout is None: + close_timeout = timeout + + # Backwards compatibility: the loop parameter used to be supported. + if loop is None: + loop = asyncio.get_event_loop() + else: + warnings.warn("remove loop argument", DeprecationWarning) + + self.ping_interval = ping_interval + self.ping_timeout = ping_timeout + self.close_timeout = close_timeout + self.max_size = max_size + self.max_queue = max_queue + self.read_limit = read_limit + self.write_limit = write_limit + + # Unique identifier. For logs. + self.id: uuid.UUID = uuid.uuid4() + """Unique identifier of the connection. Useful in logs.""" + + # Logger or LoggerAdapter for this connection. + if logger is None: + logger = logging.getLogger("websockets.protocol") + self.logger: LoggerLike = logging.LoggerAdapter(logger, {"websocket": self}) + """Logger for this connection.""" + + # Track if DEBUG is enabled. Shortcut logging calls if it isn't. + self.debug = logger.isEnabledFor(logging.DEBUG) + + self.loop = loop + + self._host = host + self._port = port + self._secure = secure + self.legacy_recv = legacy_recv + + # Configure read buffer limits. The high-water limit is defined by + # ``self.read_limit``. The ``limit`` argument controls the line length + # limit and half the buffer limit of :class:`~asyncio.StreamReader`. + # That's why it must be set to half of ``self.read_limit``. + self.reader = asyncio.StreamReader(limit=read_limit // 2, loop=loop) + + # Copied from asyncio.FlowControlMixin + self._paused = False + self._drain_waiter: Optional[asyncio.Future[None]] = None + + self._drain_lock = asyncio.Lock() + + # This class implements the data transfer and closing handshake, which + # are shared between the client-side and the server-side. + # Subclasses implement the opening handshake and, on success, execute + # :meth:`connection_open` to change the state to OPEN. + self.state = State.CONNECTING + if self.debug: + self.logger.debug("= connection is CONNECTING") + + # HTTP protocol parameters. + self.path: str + """Path of the opening handshake request.""" + self.request_headers: Headers + """Opening handshake request headers.""" + self.response_headers: Headers + """Opening handshake response headers.""" + + # WebSocket protocol parameters. + self.extensions: List[Extension] = [] + self.subprotocol: Optional[Subprotocol] = None + """Subprotocol, if one was negotiated.""" + + # Close code and reason, set when a close frame is sent or received. + self.close_rcvd: Optional[Close] = None + self.close_sent: Optional[Close] = None + self.close_rcvd_then_sent: Optional[bool] = None + + # Completed when the connection state becomes CLOSED. Translates the + # :meth:`connection_lost` callback to a :class:`~asyncio.Future` + # that can be awaited. (Other :class:`~asyncio.Protocol` callbacks are + # translated by ``self.stream_reader``). + self.connection_lost_waiter: asyncio.Future[None] = loop.create_future() + + # Queue of received messages. + self.messages: Deque[Data] = collections.deque() + self._pop_message_waiter: Optional[asyncio.Future[None]] = None + self._put_message_waiter: Optional[asyncio.Future[None]] = None + + # Protect sending fragmented messages. + self._fragmented_message_waiter: Optional[asyncio.Future[None]] = None + + # Mapping of ping IDs to pong waiters, in chronological order. + self.pings: Dict[bytes, Tuple[asyncio.Future[float], float]] = {} + + self.latency: float = 0 + """ + Latency of the connection, in seconds. + + This value is updated after sending a ping frame and receiving a + matching pong frame. Before the first ping, :attr:`latency` is ``0``. + + By default, websockets enables a :ref:`keepalive ` mechanism + that sends ping frames automatically at regular intervals. You can also + send ping frames and measure latency with :meth:`ping`. + """ + + # Task running the data transfer. + self.transfer_data_task: asyncio.Task[None] + + # Exception that occurred during data transfer, if any. + self.transfer_data_exc: Optional[BaseException] = None + + # Task sending keepalive pings. + self.keepalive_ping_task: asyncio.Task[None] + + # Task closing the TCP connection. + self.close_connection_task: asyncio.Task[None] + + # Copied from asyncio.FlowControlMixin + async def _drain_helper(self) -> None: # pragma: no cover + if self.connection_lost_waiter.done(): + raise ConnectionResetError("Connection lost") + if not self._paused: + return + waiter = self._drain_waiter + assert waiter is None or waiter.cancelled() + waiter = self.loop.create_future() + self._drain_waiter = waiter + await waiter + + # Copied from asyncio.StreamWriter + async def _drain(self) -> None: # pragma: no cover + if self.reader is not None: + exc = self.reader.exception() + if exc is not None: + raise exc + if self.transport is not None: + if self.transport.is_closing(): + # Yield to the event loop so connection_lost() may be + # called. Without this, _drain_helper() would return + # immediately, and code that calls + # write(...); yield from drain() + # in a loop would never call connection_lost(), so it + # would not see an error when the socket is closed. + await asyncio.sleep(0) + await self._drain_helper() + + def connection_open(self) -> None: + """ + Callback when the WebSocket opening handshake completes. + + Enter the OPEN state and start the data transfer phase. + + """ + # 4.1. The WebSocket Connection is Established. + assert self.state is State.CONNECTING + self.state = State.OPEN + if self.debug: + self.logger.debug("= connection is OPEN") + # Start the task that receives incoming WebSocket messages. + self.transfer_data_task = self.loop.create_task(self.transfer_data()) + # Start the task that sends pings at regular intervals. + self.keepalive_ping_task = self.loop.create_task(self.keepalive_ping()) + # Start the task that eventually closes the TCP connection. + self.close_connection_task = self.loop.create_task(self.close_connection()) + + @property + def host(self) -> Optional[str]: + alternative = "remote_address" if self.is_client else "local_address" + warnings.warn(f"use {alternative}[0] instead of host", DeprecationWarning) + return self._host + + @property + def port(self) -> Optional[int]: + alternative = "remote_address" if self.is_client else "local_address" + warnings.warn(f"use {alternative}[1] instead of port", DeprecationWarning) + return self._port + + @property + def secure(self) -> Optional[bool]: + warnings.warn("don't use secure", DeprecationWarning) + return self._secure + + # Public API + + @property + def local_address(self) -> Any: + """ + Local address of the connection. + + For IPv4 connections, this is a ``(host, port)`` tuple. + + The format of the address depends on the address family; + see :meth:`~socket.socket.getsockname`. + + :obj:`None` if the TCP connection isn't established yet. + + """ + try: + transport = self.transport + except AttributeError: + return None + else: + return transport.get_extra_info("sockname") + + @property + def remote_address(self) -> Any: + """ + Remote address of the connection. + + For IPv4 connections, this is a ``(host, port)`` tuple. + + The format of the address depends on the address family; + see :meth:`~socket.socket.getpeername`. + + :obj:`None` if the TCP connection isn't established yet. + + """ + try: + transport = self.transport + except AttributeError: + return None + else: + return transport.get_extra_info("peername") + + @property + def open(self) -> bool: + """ + :obj:`True` when the connection is open; :obj:`False` otherwise. + + This attribute may be used to detect disconnections. However, this + approach is discouraged per the EAFP_ principle. Instead, you should + handle :exc:`~websockets.exceptions.ConnectionClosed` exceptions. + + .. _EAFP: https://docs.python.org/3/glossary.html#term-eafp + + """ + return self.state is State.OPEN and not self.transfer_data_task.done() + + @property + def closed(self) -> bool: + """ + :obj:`True` when the connection is closed; :obj:`False` otherwise. + + Be aware that both :attr:`open` and :attr:`closed` are :obj:`False` + during the opening and closing sequences. + + """ + return self.state is State.CLOSED + + @property + def close_code(self) -> Optional[int]: + """ + WebSocket close code, defined in `section 7.1.5 of RFC 6455`_. + + .. _section 7.1.5 of RFC 6455: + https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 + + :obj:`None` if the connection isn't closed yet. + + """ + if self.state is not State.CLOSED: + return None + elif self.close_rcvd is None: + return CloseCode.ABNORMAL_CLOSURE + else: + return self.close_rcvd.code + + @property + def close_reason(self) -> Optional[str]: + """ + WebSocket close reason, defined in `section 7.1.6 of RFC 6455`_. + + .. _section 7.1.6 of RFC 6455: + https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 + + :obj:`None` if the connection isn't closed yet. + + """ + if self.state is not State.CLOSED: + return None + elif self.close_rcvd is None: + return "" + else: + return self.close_rcvd.reason + + async def __aiter__(self) -> AsyncIterator[Data]: + """ + Iterate on incoming messages. + + The iterator exits normally when the connection is closed with the close + code 1000 (OK) or 1001 (going away) or without a close code. + + It raises a :exc:`~websockets.exceptions.ConnectionClosedError` + exception when the connection is closed with any other code. + + """ + try: + while True: + yield await self.recv() + except ConnectionClosedOK: + return + + async def recv(self) -> Data: + """ + Receive the next message. + + When the connection is closed, :meth:`recv` raises + :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises + :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal + connection closure and + :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol + error or a network failure. This is how you detect the end of the + message stream. + + Canceling :meth:`recv` is safe. There's no risk of losing the next + message. The next invocation of :meth:`recv` will return it. + + This makes it possible to enforce a timeout by wrapping :meth:`recv` in + :func:`~asyncio.timeout` or :func:`~asyncio.wait_for`. + + Returns: + Data: A string (:class:`str`) for a Text_ frame. A bytestring + (:class:`bytes`) for a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If two coroutines call :meth:`recv` concurrently. + + """ + if self._pop_message_waiter is not None: + raise RuntimeError( + "cannot call recv while another coroutine " + "is already waiting for the next message" + ) + + # Don't await self.ensure_open() here: + # - messages could be available in the queue even if the connection + # is closed; + # - messages could be received before the closing frame even if the + # connection is closing. + + # Wait until there's a message in the queue (if necessary) or the + # connection is closed. + while len(self.messages) <= 0: + pop_message_waiter: asyncio.Future[None] = self.loop.create_future() + self._pop_message_waiter = pop_message_waiter + try: + # If asyncio.wait() is canceled, it doesn't cancel + # pop_message_waiter and self.transfer_data_task. + await asyncio.wait( + [pop_message_waiter, self.transfer_data_task], + return_when=asyncio.FIRST_COMPLETED, + ) + finally: + self._pop_message_waiter = None + + # If asyncio.wait(...) exited because self.transfer_data_task + # completed before receiving a new message, raise a suitable + # exception (or return None if legacy_recv is enabled). + if not pop_message_waiter.done(): + if self.legacy_recv: + return None # type: ignore + else: + # Wait until the connection is closed to raise + # ConnectionClosed with the correct code and reason. + await self.ensure_open() + + # Pop a message from the queue. + message = self.messages.popleft() + + # Notify transfer_data(). + if self._put_message_waiter is not None: + self._put_message_waiter.set_result(None) + self._put_message_waiter = None + + return message + + async def send( + self, + message: Union[Data, Iterable[Data], AsyncIterable[Data]], + ) -> None: + """ + Send a message. + + A string (:class:`str`) is sent as a Text_ frame. A bytestring or + bytes-like object (:class:`bytes`, :class:`bytearray`, or + :class:`memoryview`) is sent as a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + :meth:`send` also accepts an iterable or an asynchronous iterable of + strings, bytestrings, or bytes-like objects to enable fragmentation_. + Each item is treated as a message fragment and sent in its own frame. + All items must be of the same type, or else :meth:`send` will raise a + :exc:`TypeError` and the connection will be closed. + + .. _fragmentation: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.4 + + :meth:`send` rejects dict-like objects because this is often an error. + (If you want to send the keys of a dict-like object as fragments, call + its :meth:`~dict.keys` method and pass the result to :meth:`send`.) + + Canceling :meth:`send` is discouraged. Instead, you should close the + connection with :meth:`close`. Indeed, there are only two situations + where :meth:`send` may yield control to the event loop and then get + canceled; in both cases, :meth:`close` has the same effect and is + more clear: + + 1. The write buffer is full. If you don't want to wait until enough + data is sent, your only alternative is to close the connection. + :meth:`close` will likely time out then abort the TCP connection. + 2. ``message`` is an asynchronous iterator that yields control. + Stopping in the middle of a fragmented message will cause a + protocol error and the connection will be closed. + + When the connection is closed, :meth:`send` raises + :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it + raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal + connection closure and + :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol + error or a network failure. + + Args: + message (Union[Data, Iterable[Data], AsyncIterable[Data]): message + to send. + + Raises: + ConnectionClosed: When the connection is closed. + TypeError: If ``message`` doesn't have a supported type. + + """ + await self.ensure_open() + + # While sending a fragmented message, prevent sending other messages + # until all fragments are sent. + while self._fragmented_message_waiter is not None: + await asyncio.shield(self._fragmented_message_waiter) + + # Unfragmented message -- this case must be handled first because + # strings and bytes-like objects are iterable. + + if isinstance(message, (str, bytes, bytearray, memoryview)): + opcode, data = prepare_data(message) + await self.write_frame(True, opcode, data) + + # Catch a common mistake -- passing a dict to send(). + + elif isinstance(message, Mapping): + raise TypeError("data is a dict-like object") + + # Fragmented message -- regular iterator. + + elif isinstance(message, Iterable): + # Work around https://github.com/python/mypy/issues/6227 + message = cast(Iterable[Data], message) + + iter_message = iter(message) + try: + fragment = next(iter_message) + except StopIteration: + return + opcode, data = prepare_data(fragment) + + self._fragmented_message_waiter = asyncio.Future() + try: + # First fragment. + await self.write_frame(False, opcode, data) + + # Other fragments. + for fragment in iter_message: + confirm_opcode, data = prepare_data(fragment) + if confirm_opcode != opcode: + raise TypeError("data contains inconsistent types") + await self.write_frame(False, OP_CONT, data) + + # Final fragment. + await self.write_frame(True, OP_CONT, b"") + + except (Exception, asyncio.CancelledError): + # We're half-way through a fragmented message and we can't + # complete it. This makes the connection unusable. + self.fail_connection(CloseCode.INTERNAL_ERROR) + raise + + finally: + self._fragmented_message_waiter.set_result(None) + self._fragmented_message_waiter = None + + # Fragmented message -- asynchronous iterator + + elif isinstance(message, AsyncIterable): + # Implement aiter_message = aiter(message) without aiter + # Work around https://github.com/python/mypy/issues/5738 + aiter_message = cast( + Callable[[AsyncIterable[Data]], AsyncIterator[Data]], + type(message).__aiter__, + )(message) + try: + # Implement fragment = anext(aiter_message) without anext + # Work around https://github.com/python/mypy/issues/5738 + fragment = await cast( + Callable[[AsyncIterator[Data]], Awaitable[Data]], + type(aiter_message).__anext__, + )(aiter_message) + except StopAsyncIteration: + return + opcode, data = prepare_data(fragment) + + self._fragmented_message_waiter = asyncio.Future() + try: + # First fragment. + await self.write_frame(False, opcode, data) + + # Other fragments. + async for fragment in aiter_message: + confirm_opcode, data = prepare_data(fragment) + if confirm_opcode != opcode: + raise TypeError("data contains inconsistent types") + await self.write_frame(False, OP_CONT, data) + + # Final fragment. + await self.write_frame(True, OP_CONT, b"") + + except (Exception, asyncio.CancelledError): + # We're half-way through a fragmented message and we can't + # complete it. This makes the connection unusable. + self.fail_connection(CloseCode.INTERNAL_ERROR) + raise + + finally: + self._fragmented_message_waiter.set_result(None) + self._fragmented_message_waiter = None + + else: + raise TypeError("data must be str, bytes-like, or iterable") + + async def close( + self, + code: int = CloseCode.NORMAL_CLOSURE, + reason: str = "", + ) -> None: + """ + Perform the closing handshake. + + :meth:`close` waits for the other end to complete the handshake and + for the TCP connection to terminate. As a consequence, there's no need + to await :meth:`wait_closed` after :meth:`close`. + + :meth:`close` is idempotent: it doesn't do anything once the + connection is closed. + + Wrapping :func:`close` in :func:`~asyncio.create_task` is safe, given + that errors during connection termination aren't particularly useful. + + Canceling :meth:`close` is discouraged. If it takes too long, you can + set a shorter ``close_timeout``. If you don't want to wait, let the + Python process exit, then the OS will take care of closing the TCP + connection. + + Args: + code: WebSocket close code. + reason: WebSocket close reason. + + """ + try: + async with asyncio_timeout(self.close_timeout): + await self.write_close_frame(Close(code, reason)) + except asyncio.TimeoutError: + # If the close frame cannot be sent because the send buffers + # are full, the closing handshake won't complete anyway. + # Fail the connection to shut down faster. + self.fail_connection() + + # If no close frame is received within the timeout, asyncio_timeout() + # cancels the data transfer task and raises TimeoutError. + + # If close() is called multiple times concurrently and one of these + # calls hits the timeout, the data transfer task will be canceled. + # Other calls will receive a CancelledError here. + + try: + # If close() is canceled during the wait, self.transfer_data_task + # is canceled before the timeout elapses. + async with asyncio_timeout(self.close_timeout): + await self.transfer_data_task + except (asyncio.TimeoutError, asyncio.CancelledError): + pass + + # Wait for the close connection task to close the TCP connection. + await asyncio.shield(self.close_connection_task) + + async def wait_closed(self) -> None: + """ + Wait until the connection is closed. + + This coroutine is identical to the :attr:`closed` attribute, except it + can be awaited. + + This can make it easier to detect connection termination, regardless + of its cause, in tasks that interact with the WebSocket connection. + + """ + await asyncio.shield(self.connection_lost_waiter) + + async def ping(self, data: Optional[Data] = None) -> Awaitable[None]: + """ + Send a Ping_. + + .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 + + A ping may serve as a keepalive, as a check that the remote endpoint + received all messages up to this point, or to measure :attr:`latency`. + + Canceling :meth:`ping` is discouraged. If :meth:`ping` doesn't return + immediately, it means the write buffer is full. If you don't want to + wait, you should close the connection. + + Canceling the :class:`~asyncio.Future` returned by :meth:`ping` has no + effect. + + Args: + data (Optional[Data]): payload of the ping; a string will be + encoded to UTF-8; or :obj:`None` to generate a payload + containing four random bytes. + + Returns: + ~asyncio.Future[float]: A future that will be completed when the + corresponding pong is received. You can ignore it if you don't + intend to wait. The result of the future is the latency of the + connection in seconds. + + :: + + pong_waiter = await ws.ping() + # only if you want to wait for the corresponding pong + latency = await pong_waiter + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If another ping was sent with the same data and + the corresponding pong wasn't received yet. + + """ + await self.ensure_open() + + if data is not None: + data = prepare_ctrl(data) + + # Protect against duplicates if a payload is explicitly set. + if data in self.pings: + raise RuntimeError("already waiting for a pong with the same data") + + # Generate a unique random payload otherwise. + while data is None or data in self.pings: + data = struct.pack("!I", random.getrandbits(32)) + + pong_waiter = self.loop.create_future() + # Resolution of time.monotonic() may be too low on Windows. + ping_timestamp = time.perf_counter() + self.pings[data] = (pong_waiter, ping_timestamp) + + await self.write_frame(True, OP_PING, data) + + return asyncio.shield(pong_waiter) + + async def pong(self, data: Data = b"") -> None: + """ + Send a Pong_. + + .. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + + An unsolicited pong may serve as a unidirectional heartbeat. + + Canceling :meth:`pong` is discouraged. If :meth:`pong` doesn't return + immediately, it means the write buffer is full. If you don't want to + wait, you should close the connection. + + Args: + data (Data): Payload of the pong. A string will be encoded to + UTF-8. + + Raises: + ConnectionClosed: When the connection is closed. + + """ + await self.ensure_open() + + data = prepare_ctrl(data) + + await self.write_frame(True, OP_PONG, data) + + # Private methods - no guarantees. + + def connection_closed_exc(self) -> ConnectionClosed: + exc: ConnectionClosed + if ( + self.close_rcvd is not None + and self.close_rcvd.code in OK_CLOSE_CODES + and self.close_sent is not None + and self.close_sent.code in OK_CLOSE_CODES + ): + exc = ConnectionClosedOK( + self.close_rcvd, + self.close_sent, + self.close_rcvd_then_sent, + ) + else: + exc = ConnectionClosedError( + self.close_rcvd, + self.close_sent, + self.close_rcvd_then_sent, + ) + # Chain to the exception that terminated data transfer, if any. + exc.__cause__ = self.transfer_data_exc + return exc + + async def ensure_open(self) -> None: + """ + Check that the WebSocket connection is open. + + Raise :exc:`~websockets.exceptions.ConnectionClosed` if it isn't. + + """ + # Handle cases from most common to least common for performance. + if self.state is State.OPEN: + # If self.transfer_data_task exited without a closing handshake, + # self.close_connection_task may be closing the connection, going + # straight from OPEN to CLOSED. + if self.transfer_data_task.done(): + await asyncio.shield(self.close_connection_task) + raise self.connection_closed_exc() + else: + return + + if self.state is State.CLOSED: + raise self.connection_closed_exc() + + if self.state is State.CLOSING: + # If we started the closing handshake, wait for its completion to + # get the proper close code and reason. self.close_connection_task + # will complete within 4 or 5 * close_timeout after close(). The + # CLOSING state also occurs when failing the connection. In that + # case self.close_connection_task will complete even faster. + await asyncio.shield(self.close_connection_task) + raise self.connection_closed_exc() + + # Control may only reach this point in buggy third-party subclasses. + assert self.state is State.CONNECTING + raise InvalidState("WebSocket connection isn't established yet") + + async def transfer_data(self) -> None: + """ + Read incoming messages and put them in a queue. + + This coroutine runs in a task until the closing handshake is started. + + """ + try: + while True: + message = await self.read_message() + + # Exit the loop when receiving a close frame. + if message is None: + break + + # Wait until there's room in the queue (if necessary). + if self.max_queue is not None: + while len(self.messages) >= self.max_queue: + self._put_message_waiter = self.loop.create_future() + try: + await asyncio.shield(self._put_message_waiter) + finally: + self._put_message_waiter = None + + # Put the message in the queue. + self.messages.append(message) + + # Notify recv(). + if self._pop_message_waiter is not None: + self._pop_message_waiter.set_result(None) + self._pop_message_waiter = None + + except asyncio.CancelledError as exc: + self.transfer_data_exc = exc + # If fail_connection() cancels this task, avoid logging the error + # twice and failing the connection again. + raise + + except ProtocolError as exc: + self.transfer_data_exc = exc + self.fail_connection(CloseCode.PROTOCOL_ERROR) + + except (ConnectionError, TimeoutError, EOFError, ssl.SSLError) as exc: + # Reading data with self.reader.readexactly may raise: + # - most subclasses of ConnectionError if the TCP connection + # breaks, is reset, or is aborted; + # - TimeoutError if the TCP connection times out; + # - IncompleteReadError, a subclass of EOFError, if fewer + # bytes are available than requested; + # - ssl.SSLError if the other side infringes the TLS protocol. + self.transfer_data_exc = exc + self.fail_connection(CloseCode.ABNORMAL_CLOSURE) + + except UnicodeDecodeError as exc: + self.transfer_data_exc = exc + self.fail_connection(CloseCode.INVALID_DATA) + + except PayloadTooBig as exc: + self.transfer_data_exc = exc + self.fail_connection(CloseCode.MESSAGE_TOO_BIG) + + except Exception as exc: + # This shouldn't happen often because exceptions expected under + # regular circumstances are handled above. If it does, consider + # catching and handling more exceptions. + self.logger.error("data transfer failed", exc_info=True) + + self.transfer_data_exc = exc + self.fail_connection(CloseCode.INTERNAL_ERROR) + + async def read_message(self) -> Optional[Data]: + """ + Read a single message from the connection. + + Re-assemble data frames if the message is fragmented. + + Return :obj:`None` when the closing handshake is started. + + """ + frame = await self.read_data_frame(max_size=self.max_size) + + # A close frame was received. + if frame is None: + return None + + if frame.opcode == OP_TEXT: + text = True + elif frame.opcode == OP_BINARY: + text = False + else: # frame.opcode == OP_CONT + raise ProtocolError("unexpected opcode") + + # Shortcut for the common case - no fragmentation + if frame.fin: + return frame.data.decode("utf-8") if text else frame.data + + # 5.4. Fragmentation + fragments: List[Data] = [] + max_size = self.max_size + if text: + decoder_factory = codecs.getincrementaldecoder("utf-8") + decoder = decoder_factory(errors="strict") + if max_size is None: + + def append(frame: Frame) -> None: + nonlocal fragments + fragments.append(decoder.decode(frame.data, frame.fin)) + + else: + + def append(frame: Frame) -> None: + nonlocal fragments, max_size + fragments.append(decoder.decode(frame.data, frame.fin)) + assert isinstance(max_size, int) + max_size -= len(frame.data) + + else: + if max_size is None: + + def append(frame: Frame) -> None: + nonlocal fragments + fragments.append(frame.data) + + else: + + def append(frame: Frame) -> None: + nonlocal fragments, max_size + fragments.append(frame.data) + assert isinstance(max_size, int) + max_size -= len(frame.data) + + append(frame) + + while not frame.fin: + frame = await self.read_data_frame(max_size=max_size) + if frame is None: + raise ProtocolError("incomplete fragmented message") + if frame.opcode != OP_CONT: + raise ProtocolError("unexpected opcode") + append(frame) + + return ("" if text else b"").join(fragments) + + async def read_data_frame(self, max_size: Optional[int]) -> Optional[Frame]: + """ + Read a single data frame from the connection. + + Process control frames received before the next data frame. + + Return :obj:`None` if a close frame is encountered before any data frame. + + """ + # 6.2. Receiving Data + while True: + frame = await self.read_frame(max_size) + + # 5.5. Control Frames + if frame.opcode == OP_CLOSE: + # 7.1.5. The WebSocket Connection Close Code + # 7.1.6. The WebSocket Connection Close Reason + self.close_rcvd = Close.parse(frame.data) + if self.close_sent is not None: + self.close_rcvd_then_sent = False + try: + # Echo the original data instead of re-serializing it with + # Close.serialize() because that fails when the close frame + # is empty and Close.parse() synthesizes a 1005 close code. + await self.write_close_frame(self.close_rcvd, frame.data) + except ConnectionClosed: + # Connection closed before we could echo the close frame. + pass + return None + + elif frame.opcode == OP_PING: + # Answer pings, unless connection is CLOSING. + if self.state is State.OPEN: + try: + await self.pong(frame.data) + except ConnectionClosed: + # Connection closed while draining write buffer. + pass + + elif frame.opcode == OP_PONG: + if frame.data in self.pings: + pong_timestamp = time.perf_counter() + # Sending a pong for only the most recent ping is legal. + # Acknowledge all previous pings too in that case. + ping_id = None + ping_ids = [] + for ping_id, (pong_waiter, ping_timestamp) in self.pings.items(): + ping_ids.append(ping_id) + if not pong_waiter.done(): + pong_waiter.set_result(pong_timestamp - ping_timestamp) + if ping_id == frame.data: + self.latency = pong_timestamp - ping_timestamp + break + else: + raise AssertionError("solicited pong not found in pings") + # Remove acknowledged pings from self.pings. + for ping_id in ping_ids: + del self.pings[ping_id] + + # 5.6. Data Frames + else: + return frame + + async def read_frame(self, max_size: Optional[int]) -> Frame: + """ + Read a single frame from the connection. + + """ + frame = await Frame.read( + self.reader.readexactly, + mask=not self.is_client, + max_size=max_size, + extensions=self.extensions, + ) + if self.debug: + self.logger.debug("< %s", frame) + return frame + + def write_frame_sync(self, fin: bool, opcode: int, data: bytes) -> None: + frame = Frame(fin, Opcode(opcode), data) + if self.debug: + self.logger.debug("> %s", frame) + frame.write( + self.transport.write, + mask=self.is_client, + extensions=self.extensions, + ) + + async def drain(self) -> None: + try: + # drain() cannot be called concurrently by multiple coroutines: + # http://bugs.python.org/issue29930. Remove this lock when no + # version of Python where this bugs exists is supported anymore. + async with self._drain_lock: + # Handle flow control automatically. + await self._drain() + except ConnectionError: + # Terminate the connection if the socket died. + self.fail_connection() + # Wait until the connection is closed to raise ConnectionClosed + # with the correct code and reason. + await self.ensure_open() + + async def write_frame( + self, fin: bool, opcode: int, data: bytes, *, _state: int = State.OPEN + ) -> None: + # Defensive assertion for protocol compliance. + if self.state is not _state: # pragma: no cover + raise InvalidState( + f"Cannot write to a WebSocket in the {self.state.name} state" + ) + self.write_frame_sync(fin, opcode, data) + await self.drain() + + async def write_close_frame( + self, close: Close, data: Optional[bytes] = None + ) -> None: + """ + Write a close frame if and only if the connection state is OPEN. + + This dedicated coroutine must be used for writing close frames to + ensure that at most one close frame is sent on a given connection. + + """ + # Test and set the connection state before sending the close frame to + # avoid sending two frames in case of concurrent calls. + if self.state is State.OPEN: + # 7.1.3. The WebSocket Closing Handshake is Started + self.state = State.CLOSING + if self.debug: + self.logger.debug("= connection is CLOSING") + + self.close_sent = close + if self.close_rcvd is not None: + self.close_rcvd_then_sent = True + if data is None: + data = close.serialize() + + # 7.1.2. Start the WebSocket Closing Handshake + await self.write_frame(True, OP_CLOSE, data, _state=State.CLOSING) + + async def keepalive_ping(self) -> None: + """ + Send a Ping frame and wait for a Pong frame at regular intervals. + + This coroutine exits when the connection terminates and one of the + following happens: + + - :meth:`ping` raises :exc:`ConnectionClosed`, or + - :meth:`close_connection` cancels :attr:`keepalive_ping_task`. + + """ + if self.ping_interval is None: + return + + try: + while True: + await asyncio.sleep(self.ping_interval) + + # ping() raises CancelledError if the connection is closed, + # when close_connection() cancels self.keepalive_ping_task. + + # ping() raises ConnectionClosed if the connection is lost, + # when connection_lost() calls abort_pings(). + + self.logger.debug("% sending keepalive ping") + pong_waiter = await self.ping() + + if self.ping_timeout is not None: + try: + async with asyncio_timeout(self.ping_timeout): + await pong_waiter + self.logger.debug("% received keepalive pong") + except asyncio.TimeoutError: + if self.debug: + self.logger.debug("! timed out waiting for keepalive pong") + self.fail_connection( + CloseCode.INTERNAL_ERROR, + "keepalive ping timeout", + ) + break + + except ConnectionClosed: + pass + + except Exception: + self.logger.error("keepalive ping failed", exc_info=True) + + async def close_connection(self) -> None: + """ + 7.1.1. Close the WebSocket Connection + + When the opening handshake succeeds, :meth:`connection_open` starts + this coroutine in a task. It waits for the data transfer phase to + complete then it closes the TCP connection cleanly. + + When the opening handshake fails, :meth:`fail_connection` does the + same. There's no data transfer phase in that case. + + """ + try: + # Wait for the data transfer phase to complete. + if hasattr(self, "transfer_data_task"): + try: + await self.transfer_data_task + except asyncio.CancelledError: + pass + + # Cancel the keepalive ping task. + if hasattr(self, "keepalive_ping_task"): + self.keepalive_ping_task.cancel() + + # A client should wait for a TCP close from the server. + if self.is_client and hasattr(self, "transfer_data_task"): + if await self.wait_for_connection_lost(): + return + if self.debug: + self.logger.debug("! timed out waiting for TCP close") + + # Half-close the TCP connection if possible (when there's no TLS). + if self.transport.can_write_eof(): + if self.debug: + self.logger.debug("x half-closing TCP connection") + # write_eof() doesn't document which exceptions it raises. + # "[Errno 107] Transport endpoint is not connected" happens + # but it isn't completely clear under which circumstances. + # uvloop can raise RuntimeError here. + try: + self.transport.write_eof() + except (OSError, RuntimeError): # pragma: no cover + pass + + if await self.wait_for_connection_lost(): + return + if self.debug: + self.logger.debug("! timed out waiting for TCP close") + + finally: + # The try/finally ensures that the transport never remains open, + # even if this coroutine is canceled (for example). + await self.close_transport() + + async def close_transport(self) -> None: + """ + Close the TCP connection. + + """ + # If connection_lost() was called, the TCP connection is closed. + # However, if TLS is enabled, the transport still needs closing. + # Else asyncio complains: ResourceWarning: unclosed transport. + if self.connection_lost_waiter.done() and self.transport.is_closing(): + return + + # Close the TCP connection. Buffers are flushed asynchronously. + if self.debug: + self.logger.debug("x closing TCP connection") + self.transport.close() + + if await self.wait_for_connection_lost(): + return + if self.debug: + self.logger.debug("! timed out waiting for TCP close") + + # Abort the TCP connection. Buffers are discarded. + if self.debug: + self.logger.debug("x aborting TCP connection") + # Due to a bug in coverage, this is erroneously reported as not covered. + self.transport.abort() # pragma: no cover + + # connection_lost() is called quickly after aborting. + await self.wait_for_connection_lost() + + async def wait_for_connection_lost(self) -> bool: + """ + Wait until the TCP connection is closed or ``self.close_timeout`` elapses. + + Return :obj:`True` if the connection is closed and :obj:`False` + otherwise. + + """ + if not self.connection_lost_waiter.done(): + try: + async with asyncio_timeout(self.close_timeout): + await asyncio.shield(self.connection_lost_waiter) + except asyncio.TimeoutError: + pass + # Re-check self.connection_lost_waiter.done() synchronously because + # connection_lost() could run between the moment the timeout occurs + # and the moment this coroutine resumes running. + return self.connection_lost_waiter.done() + + def fail_connection( + self, + code: int = CloseCode.ABNORMAL_CLOSURE, + reason: str = "", + ) -> None: + """ + 7.1.7. Fail the WebSocket Connection + + This requires: + + 1. Stopping all processing of incoming data, which means cancelling + :attr:`transfer_data_task`. The close code will be 1006 unless a + close frame was received earlier. + + 2. Sending a close frame with an appropriate code if the opening + handshake succeeded and the other side is likely to process it. + + 3. Closing the connection. :meth:`close_connection` takes care of + this once :attr:`transfer_data_task` exits after being canceled. + + (The specification describes these steps in the opposite order.) + + """ + if self.debug: + self.logger.debug("! failing connection with code %d", code) + + # Cancel transfer_data_task if the opening handshake succeeded. + # cancel() is idempotent and ignored if the task is done already. + if hasattr(self, "transfer_data_task"): + self.transfer_data_task.cancel() + + # Send a close frame when the state is OPEN (a close frame was already + # sent if it's CLOSING), except when failing the connection because of + # an error reading from or writing to the network. + # Don't send a close frame if the connection is broken. + if code != CloseCode.ABNORMAL_CLOSURE and self.state is State.OPEN: + close = Close(code, reason) + + # Write the close frame without draining the write buffer. + + # Keeping fail_connection() synchronous guarantees it can't + # get stuck and simplifies the implementation of the callers. + # Not drainig the write buffer is acceptable in this context. + + # This duplicates a few lines of code from write_close_frame(). + + self.state = State.CLOSING + if self.debug: + self.logger.debug("= connection is CLOSING") + + # If self.close_rcvd was set, the connection state would be + # CLOSING. Therefore self.close_rcvd isn't set and we don't + # have to set self.close_rcvd_then_sent. + assert self.close_rcvd is None + self.close_sent = close + + self.write_frame_sync(True, OP_CLOSE, close.serialize()) + + # Start close_connection_task if the opening handshake didn't succeed. + if not hasattr(self, "close_connection_task"): + self.close_connection_task = self.loop.create_task(self.close_connection()) + + def abort_pings(self) -> None: + """ + Raise ConnectionClosed in pending keepalive pings. + + They'll never receive a pong once the connection is closed. + + """ + assert self.state is State.CLOSED + exc = self.connection_closed_exc() + + for pong_waiter, _ping_timestamp in self.pings.values(): + pong_waiter.set_exception(exc) + # If the exception is never retrieved, it will be logged when ping + # is garbage-collected. This is confusing for users. + # Given that ping is done (with an exception), canceling it does + # nothing, but it prevents logging the exception. + pong_waiter.cancel() + + # asyncio.Protocol methods + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + """ + Configure write buffer limits. + + The high-water limit is defined by ``self.write_limit``. + + The low-water limit currently defaults to ``self.write_limit // 4`` in + :meth:`~asyncio.WriteTransport.set_write_buffer_limits`, which should + be all right for reasonable use cases of this library. + + This is the earliest point where we can get hold of the transport, + which means it's the best point for configuring it. + + """ + transport = cast(asyncio.Transport, transport) + transport.set_write_buffer_limits(self.write_limit) + self.transport = transport + + # Copied from asyncio.StreamReaderProtocol + self.reader.set_transport(transport) + + def connection_lost(self, exc: Optional[Exception]) -> None: + """ + 7.1.4. The WebSocket Connection is Closed. + + """ + self.state = State.CLOSED + self.logger.debug("= connection is CLOSED") + + self.abort_pings() + + # If self.connection_lost_waiter isn't pending, that's a bug, because: + # - it's set only here in connection_lost() which is called only once; + # - it must never be canceled. + self.connection_lost_waiter.set_result(None) + + if True: # pragma: no cover + # Copied from asyncio.StreamReaderProtocol + if self.reader is not None: + if exc is None: + self.reader.feed_eof() + else: + self.reader.set_exception(exc) + + # Copied from asyncio.FlowControlMixin + # Wake up the writer if currently paused. + if not self._paused: + return + waiter = self._drain_waiter + if waiter is None: + return + self._drain_waiter = None + if waiter.done(): + return + if exc is None: + waiter.set_result(None) + else: + waiter.set_exception(exc) + + def pause_writing(self) -> None: # pragma: no cover + assert not self._paused + self._paused = True + + def resume_writing(self) -> None: # pragma: no cover + assert self._paused + self._paused = False + + waiter = self._drain_waiter + if waiter is not None: + self._drain_waiter = None + if not waiter.done(): + waiter.set_result(None) + + def data_received(self, data: bytes) -> None: + self.reader.feed_data(data) + + def eof_received(self) -> None: + """ + Close the transport after receiving EOF. + + The WebSocket protocol has its own closing handshake: endpoints close + the TCP or TLS connection after sending and receiving a close frame. + + As a consequence, they never need to write after receiving EOF, so + there's no reason to keep the transport open by returning :obj:`True`. + + Besides, that doesn't work on TLS connections. + + """ + self.reader.feed_eof() + + +def broadcast( + websockets: Iterable[WebSocketCommonProtocol], + message: Data, + raise_exceptions: bool = False, +) -> None: + """ + Broadcast a message to several WebSocket connections. + + A string (:class:`str`) is sent as a Text_ frame. A bytestring or bytes-like + object (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) is sent + as a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + :func:`broadcast` pushes the message synchronously to all connections even + if their write buffers are overflowing. There's no backpressure. + + If you broadcast messages faster than a connection can handle them, messages + will pile up in its write buffer until the connection times out. Keep + ``ping_interval`` and ``ping_timeout`` low to prevent excessive memory usage + from slow connections. + + Unlike :meth:`~websockets.server.WebSocketServerProtocol.send`, + :func:`broadcast` doesn't support sending fragmented messages. Indeed, + fragmentation is useful for sending large messages without buffering them in + memory, while :func:`broadcast` buffers one copy per connection as fast as + possible. + + :func:`broadcast` skips connections that aren't open in order to avoid + errors on connections where the closing handshake is in progress. + + :func:`broadcast` ignores failures to write the message on some connections. + It continues writing to other connections. On Python 3.11 and above, you + may set ``raise_exceptions`` to :obj:`True` to record failures and raise all + exceptions in a :pep:`654` :exc:`ExceptionGroup`. + + Args: + websockets: WebSocket connections to which the message will be sent. + message: Message to send. + raise_exceptions: Whether to raise an exception in case of failures. + + Raises: + TypeError: If ``message`` doesn't have a supported type. + + """ + if not isinstance(message, (str, bytes, bytearray, memoryview)): + raise TypeError("data must be str or bytes-like") + + if raise_exceptions: + if sys.version_info[:2] < (3, 11): # pragma: no cover + raise ValueError("raise_exceptions requires at least Python 3.11") + exceptions = [] + + opcode, data = prepare_data(message) + + for websocket in websockets: + if websocket.state is not State.OPEN: + continue + + if websocket._fragmented_message_waiter is not None: + if raise_exceptions: + exception = RuntimeError("sending a fragmented message") + exceptions.append(exception) + else: + websocket.logger.warning( + "skipped broadcast: sending a fragmented message", + ) + + try: + websocket.write_frame_sync(True, opcode, data) + except Exception as write_exception: + if raise_exceptions: + exception = RuntimeError("failed to write message") + exception.__cause__ = write_exception + exceptions.append(exception) + else: + websocket.logger.warning( + "skipped broadcast: failed to write message", + exc_info=True, + ) + + if raise_exceptions: + raise ExceptionGroup("skipped broadcast", exceptions) diff --git a/websockets/legacy/server.py b/websockets/legacy/server.py new file mode 100644 index 0000000..77e0fda --- /dev/null +++ b/websockets/legacy/server.py @@ -0,0 +1,1185 @@ +from __future__ import annotations + +import asyncio +import email.utils +import functools +import http +import inspect +import logging +import socket +import warnings +from types import TracebackType +from typing import ( + Any, + Awaitable, + Callable, + Generator, + Iterable, + List, + Optional, + Sequence, + Set, + Tuple, + Type, + Union, + cast, +) + +from ..datastructures import Headers, HeadersLike, MultipleValuesError +from ..exceptions import ( + AbortHandshake, + InvalidHandshake, + InvalidHeader, + InvalidMessage, + InvalidOrigin, + InvalidUpgrade, + NegotiationError, +) +from ..extensions import Extension, ServerExtensionFactory +from ..extensions.permessage_deflate import enable_server_permessage_deflate +from ..headers import ( + build_extension, + parse_extension, + parse_subprotocol, + validate_subprotocols, +) +from ..http import USER_AGENT +from ..protocol import State +from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol +from .compatibility import asyncio_timeout +from .handshake import build_response, check_request +from .http import read_request +from .protocol import WebSocketCommonProtocol + + +__all__ = ["serve", "unix_serve", "WebSocketServerProtocol", "WebSocketServer"] + + +HeadersLikeOrCallable = Union[HeadersLike, Callable[[str, Headers], HeadersLike]] + +HTTPResponse = Tuple[http.HTTPStatus, HeadersLike, bytes] + + +class WebSocketServerProtocol(WebSocketCommonProtocol): + """ + WebSocket server connection. + + :class:`WebSocketServerProtocol` provides :meth:`recv` and :meth:`send` + coroutines for receiving and sending messages. + + It supports asynchronous iteration to receive messages:: + + async for message in websocket: + await process(message) + + The iterator exits normally when the connection is closed with close code + 1000 (OK) or 1001 (going away) or without a close code. It raises + a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection + is closed with any other code. + + You may customize the opening handshake in a subclass by + overriding :meth:`process_request` or :meth:`select_subprotocol`. + + Args: + ws_server: WebSocket server that created this connection. + + See :func:`serve` for the documentation of ``ws_handler``, ``logger``, ``origins``, + ``extensions``, ``subprotocols``, ``extra_headers``, and ``server_header``. + + See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the + documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, + ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. + + """ + + is_client = False + side = "server" + + def __init__( + self, + ws_handler: Union[ + Callable[[WebSocketServerProtocol], Awaitable[Any]], + Callable[[WebSocketServerProtocol, str], Awaitable[Any]], # deprecated + ], + ws_server: WebSocketServer, + *, + logger: Optional[LoggerLike] = None, + origins: Optional[Sequence[Optional[Origin]]] = None, + extensions: Optional[Sequence[ServerExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + extra_headers: Optional[HeadersLikeOrCallable] = None, + server_header: Optional[str] = USER_AGENT, + process_request: Optional[ + Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]] + ] = None, + select_subprotocol: Optional[ + Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] + ] = None, + open_timeout: Optional[float] = 10, + **kwargs: Any, + ) -> None: + if logger is None: + logger = logging.getLogger("websockets.server") + super().__init__(logger=logger, **kwargs) + # For backwards compatibility with 6.0 or earlier. + if origins is not None and "" in origins: + warnings.warn("use None instead of '' in origins", DeprecationWarning) + origins = [None if origin == "" else origin for origin in origins] + # For backwards compatibility with 10.0 or earlier. Done here in + # addition to serve to trigger the deprecation warning on direct + # use of WebSocketServerProtocol. + self.ws_handler = remove_path_argument(ws_handler) + self.ws_server = ws_server + self.origins = origins + self.available_extensions = extensions + self.available_subprotocols = subprotocols + self.extra_headers = extra_headers + self.server_header = server_header + self._process_request = process_request + self._select_subprotocol = select_subprotocol + self.open_timeout = open_timeout + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + """ + Register connection and initialize a task to handle it. + + """ + super().connection_made(transport) + # Register the connection with the server before creating the handler + # task. Registering at the beginning of the handler coroutine would + # create a race condition between the creation of the task, which + # schedules its execution, and the moment the handler starts running. + self.ws_server.register(self) + self.handler_task = self.loop.create_task(self.handler()) + + async def handler(self) -> None: + """ + Handle the lifecycle of a WebSocket connection. + + Since this method doesn't have a caller able to handle exceptions, it + attempts to log relevant ones and guarantees that the TCP connection is + closed before exiting. + + """ + try: + try: + async with asyncio_timeout(self.open_timeout): + await self.handshake( + origins=self.origins, + available_extensions=self.available_extensions, + available_subprotocols=self.available_subprotocols, + extra_headers=self.extra_headers, + ) + except asyncio.TimeoutError: # pragma: no cover + raise + except ConnectionError: + raise + except Exception as exc: + if isinstance(exc, AbortHandshake): + status, headers, body = exc.status, exc.headers, exc.body + elif isinstance(exc, InvalidOrigin): + if self.debug: + self.logger.debug("! invalid origin", exc_info=True) + status, headers, body = ( + http.HTTPStatus.FORBIDDEN, + Headers(), + f"Failed to open a WebSocket connection: {exc}.\n".encode(), + ) + elif isinstance(exc, InvalidUpgrade): + if self.debug: + self.logger.debug("! invalid upgrade", exc_info=True) + status, headers, body = ( + http.HTTPStatus.UPGRADE_REQUIRED, + Headers([("Upgrade", "websocket")]), + ( + f"Failed to open a WebSocket connection: {exc}.\n" + f"\n" + f"You cannot access a WebSocket server directly " + f"with a browser. You need a WebSocket client.\n" + ).encode(), + ) + elif isinstance(exc, InvalidHandshake): + if self.debug: + self.logger.debug("! invalid handshake", exc_info=True) + status, headers, body = ( + http.HTTPStatus.BAD_REQUEST, + Headers(), + f"Failed to open a WebSocket connection: {exc}.\n".encode(), + ) + else: + self.logger.error("opening handshake failed", exc_info=True) + status, headers, body = ( + http.HTTPStatus.INTERNAL_SERVER_ERROR, + Headers(), + ( + b"Failed to open a WebSocket connection.\n" + b"See server log for more information.\n" + ), + ) + + headers.setdefault("Date", email.utils.formatdate(usegmt=True)) + if self.server_header is not None: + headers.setdefault("Server", self.server_header) + + headers.setdefault("Content-Length", str(len(body))) + headers.setdefault("Content-Type", "text/plain") + headers.setdefault("Connection", "close") + + self.write_http_response(status, headers, body) + self.logger.info( + "connection failed (%d %s)", status.value, status.phrase + ) + await self.close_transport() + return + + try: + await self.ws_handler(self) + except Exception: + self.logger.error("connection handler failed", exc_info=True) + if not self.closed: + self.fail_connection(1011) + raise + + try: + await self.close() + except ConnectionError: + raise + except Exception: + self.logger.error("closing handshake failed", exc_info=True) + raise + + except Exception: + # Last-ditch attempt to avoid leaking connections on errors. + try: + self.transport.close() + except Exception: # pragma: no cover + pass + + finally: + # Unregister the connection with the server when the handler task + # terminates. Registration is tied to the lifecycle of the handler + # task because the server waits for tasks attached to registered + # connections before terminating. + self.ws_server.unregister(self) + self.logger.info("connection closed") + + async def read_http_request(self) -> Tuple[str, Headers]: + """ + Read request line and headers from the HTTP request. + + If the request contains a body, it may be read from ``self.reader`` + after this coroutine returns. + + Raises: + InvalidMessage: if the HTTP message is malformed or isn't an + HTTP/1.1 GET request. + + """ + try: + path, headers = await read_request(self.reader) + except asyncio.CancelledError: # pragma: no cover + raise + except Exception as exc: + raise InvalidMessage("did not receive a valid HTTP request") from exc + + if self.debug: + self.logger.debug("< GET %s HTTP/1.1", path) + for key, value in headers.raw_items(): + self.logger.debug("< %s: %s", key, value) + + self.path = path + self.request_headers = headers + + return path, headers + + def write_http_response( + self, status: http.HTTPStatus, headers: Headers, body: Optional[bytes] = None + ) -> None: + """ + Write status line and headers to the HTTP response. + + This coroutine is also able to write a response body. + + """ + self.response_headers = headers + + if self.debug: + self.logger.debug("> HTTP/1.1 %d %s", status.value, status.phrase) + for key, value in headers.raw_items(): + self.logger.debug("> %s: %s", key, value) + if body is not None: + self.logger.debug("> [body] (%d bytes)", len(body)) + + # Since the status line and headers only contain ASCII characters, + # we can keep this simple. + response = f"HTTP/1.1 {status.value} {status.phrase}\r\n" + response += str(headers) + + self.transport.write(response.encode()) + + if body is not None: + self.transport.write(body) + + async def process_request( + self, path: str, request_headers: Headers + ) -> Optional[HTTPResponse]: + """ + Intercept the HTTP request and return an HTTP response if appropriate. + + You may override this method in a :class:`WebSocketServerProtocol` + subclass, for example: + + * to return an HTTP 200 OK response on a given path; then a load + balancer can use this path for a health check; + * to authenticate the request and return an HTTP 401 Unauthorized or an + HTTP 403 Forbidden when authentication fails. + + You may also override this method with the ``process_request`` + argument of :func:`serve` and :class:`WebSocketServerProtocol`. This + is equivalent, except ``process_request`` won't have access to the + protocol instance, so it can't store information for later use. + + :meth:`process_request` is expected to complete quickly. If it may run + for a long time, then it should await :meth:`wait_closed` and exit if + :meth:`wait_closed` completes, or else it could prevent the server + from shutting down. + + Args: + path: request path, including optional query string. + request_headers: request headers. + + Returns: + Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]: :obj:`None` + to continue the WebSocket handshake normally. + + An HTTP response, represented by a 3-uple of the response status, + headers, and body, to abort the WebSocket handshake and return + that HTTP response instead. + + """ + if self._process_request is not None: + response = self._process_request(path, request_headers) + if isinstance(response, Awaitable): + return await response + else: + # For backwards compatibility with 7.0. + warnings.warn( + "declare process_request as a coroutine", DeprecationWarning + ) + return response + return None + + @staticmethod + def process_origin( + headers: Headers, origins: Optional[Sequence[Optional[Origin]]] = None + ) -> Optional[Origin]: + """ + Handle the Origin HTTP request header. + + Args: + headers: request headers. + origins: optional list of acceptable origins. + + Raises: + InvalidOrigin: if the origin isn't acceptable. + + """ + # "The user agent MUST NOT include more than one Origin header field" + # per https://www.rfc-editor.org/rfc/rfc6454.html#section-7.3. + try: + origin = cast(Optional[Origin], headers.get("Origin")) + except MultipleValuesError as exc: + raise InvalidHeader("Origin", "more than one Origin header found") from exc + if origins is not None: + if origin not in origins: + raise InvalidOrigin(origin) + return origin + + @staticmethod + def process_extensions( + headers: Headers, + available_extensions: Optional[Sequence[ServerExtensionFactory]], + ) -> Tuple[Optional[str], List[Extension]]: + """ + Handle the Sec-WebSocket-Extensions HTTP request header. + + Accept or reject each extension proposed in the client request. + Negotiate parameters for accepted extensions. + + Return the Sec-WebSocket-Extensions HTTP response header and the list + of accepted extensions. + + :rfc:`6455` leaves the rules up to the specification of each + :extension. + + To provide this level of flexibility, for each extension proposed by + the client, we check for a match with each extension available in the + server configuration. If no match is found, the extension is ignored. + + If several variants of the same extension are proposed by the client, + it may be accepted several times, which won't make sense in general. + Extensions must implement their own requirements. For this purpose, + the list of previously accepted extensions is provided. + + This process doesn't allow the server to reorder extensions. It can + only select a subset of the extensions proposed by the client. + + Other requirements, for example related to mandatory extensions or the + order of extensions, may be implemented by overriding this method. + + Args: + headers: request headers. + extensions: optional list of supported extensions. + + Raises: + InvalidHandshake: to abort the handshake with an HTTP 400 error. + + """ + response_header_value: Optional[str] = None + + extension_headers: List[ExtensionHeader] = [] + accepted_extensions: List[Extension] = [] + + header_values = headers.get_all("Sec-WebSocket-Extensions") + + if header_values and available_extensions: + parsed_header_values: List[ExtensionHeader] = sum( + [parse_extension(header_value) for header_value in header_values], [] + ) + + for name, request_params in parsed_header_values: + for ext_factory in available_extensions: + # Skip non-matching extensions based on their name. + if ext_factory.name != name: + continue + + # Skip non-matching extensions based on their params. + try: + response_params, extension = ext_factory.process_request_params( + request_params, accepted_extensions + ) + except NegotiationError: + continue + + # Add matching extension to the final list. + extension_headers.append((name, response_params)) + accepted_extensions.append(extension) + + # Break out of the loop once we have a match. + break + + # If we didn't break from the loop, no extension in our list + # matched what the client sent. The extension is declined. + + # Serialize extension header. + if extension_headers: + response_header_value = build_extension(extension_headers) + + return response_header_value, accepted_extensions + + # Not @staticmethod because it calls self.select_subprotocol() + def process_subprotocol( + self, headers: Headers, available_subprotocols: Optional[Sequence[Subprotocol]] + ) -> Optional[Subprotocol]: + """ + Handle the Sec-WebSocket-Protocol HTTP request header. + + Return Sec-WebSocket-Protocol HTTP response header, which is the same + as the selected subprotocol. + + Args: + headers: request headers. + available_subprotocols: optional list of supported subprotocols. + + Raises: + InvalidHandshake: to abort the handshake with an HTTP 400 error. + + """ + subprotocol: Optional[Subprotocol] = None + + header_values = headers.get_all("Sec-WebSocket-Protocol") + + if header_values and available_subprotocols: + parsed_header_values: List[Subprotocol] = sum( + [parse_subprotocol(header_value) for header_value in header_values], [] + ) + + subprotocol = self.select_subprotocol( + parsed_header_values, available_subprotocols + ) + + return subprotocol + + def select_subprotocol( + self, + client_subprotocols: Sequence[Subprotocol], + server_subprotocols: Sequence[Subprotocol], + ) -> Optional[Subprotocol]: + """ + Pick a subprotocol among those supported by the client and the server. + + If several subprotocols are available, select the preferred subprotocol + by giving equal weight to the preferences of the client and the server. + + If no subprotocol is available, proceed without a subprotocol. + + You may provide a ``select_subprotocol`` argument to :func:`serve` or + :class:`WebSocketServerProtocol` to override this logic. For example, + you could reject the handshake if the client doesn't support a + particular subprotocol, rather than accept the handshake without that + subprotocol. + + Args: + client_subprotocols: list of subprotocols offered by the client. + server_subprotocols: list of subprotocols available on the server. + + Returns: + Optional[Subprotocol]: Selected subprotocol, if a common subprotocol + was found. + + :obj:`None` to continue without a subprotocol. + + """ + if self._select_subprotocol is not None: + return self._select_subprotocol(client_subprotocols, server_subprotocols) + + subprotocols = set(client_subprotocols) & set(server_subprotocols) + if not subprotocols: + return None + return sorted( + subprotocols, + key=lambda p: client_subprotocols.index(p) + server_subprotocols.index(p), + )[0] + + async def handshake( + self, + origins: Optional[Sequence[Optional[Origin]]] = None, + available_extensions: Optional[Sequence[ServerExtensionFactory]] = None, + available_subprotocols: Optional[Sequence[Subprotocol]] = None, + extra_headers: Optional[HeadersLikeOrCallable] = None, + ) -> str: + """ + Perform the server side of the opening handshake. + + Args: + origins: list of acceptable values of the Origin HTTP header; + include :obj:`None` if the lack of an origin is acceptable. + extensions: list of supported extensions, in order in which they + should be tried. + subprotocols: list of supported subprotocols, in order of + decreasing preference. + extra_headers: arbitrary HTTP headers to add to the response when + the handshake succeeds. + + Returns: + str: path of the URI of the request. + + Raises: + InvalidHandshake: if the handshake fails. + + """ + path, request_headers = await self.read_http_request() + + # Hook for customizing request handling, for example checking + # authentication or treating some paths as plain HTTP endpoints. + early_response_awaitable = self.process_request(path, request_headers) + if isinstance(early_response_awaitable, Awaitable): + early_response = await early_response_awaitable + else: + # For backwards compatibility with 7.0. + warnings.warn("declare process_request as a coroutine", DeprecationWarning) + early_response = early_response_awaitable + + # The connection may drop while process_request is running. + if self.state is State.CLOSED: + # This subclass of ConnectionError is silently ignored in handler(). + raise BrokenPipeError("connection closed during opening handshake") + + # Change the response to a 503 error if the server is shutting down. + if not self.ws_server.is_serving(): + early_response = ( + http.HTTPStatus.SERVICE_UNAVAILABLE, + [], + b"Server is shutting down.\n", + ) + + if early_response is not None: + raise AbortHandshake(*early_response) + + key = check_request(request_headers) + + self.origin = self.process_origin(request_headers, origins) + + extensions_header, self.extensions = self.process_extensions( + request_headers, available_extensions + ) + + protocol_header = self.subprotocol = self.process_subprotocol( + request_headers, available_subprotocols + ) + + response_headers = Headers() + + build_response(response_headers, key) + + if extensions_header is not None: + response_headers["Sec-WebSocket-Extensions"] = extensions_header + + if protocol_header is not None: + response_headers["Sec-WebSocket-Protocol"] = protocol_header + + if callable(extra_headers): + extra_headers = extra_headers(path, self.request_headers) + if extra_headers is not None: + response_headers.update(extra_headers) + + response_headers.setdefault("Date", email.utils.formatdate(usegmt=True)) + if self.server_header is not None: + response_headers.setdefault("Server", self.server_header) + + self.write_http_response(http.HTTPStatus.SWITCHING_PROTOCOLS, response_headers) + + self.logger.info("connection open") + + self.connection_open() + + return path + + +class WebSocketServer: + """ + WebSocket server returned by :func:`serve`. + + This class provides the same interface as :class:`~asyncio.Server`, + notably the :meth:`~asyncio.Server.close` + and :meth:`~asyncio.Server.wait_closed` methods. + + It keeps track of WebSocket connections in order to close them properly + when shutting down. + + Args: + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.server")``. + See the :doc:`logging guide <../../topics/logging>` for details. + + """ + + def __init__(self, logger: Optional[LoggerLike] = None): + if logger is None: + logger = logging.getLogger("websockets.server") + self.logger = logger + + # Keep track of active connections. + self.websockets: Set[WebSocketServerProtocol] = set() + + # Task responsible for closing the server and terminating connections. + self.close_task: Optional[asyncio.Task[None]] = None + + # Completed when the server is closed and connections are terminated. + self.closed_waiter: asyncio.Future[None] + + def wrap(self, server: asyncio.base_events.Server) -> None: + """ + Attach to a given :class:`~asyncio.Server`. + + Since :meth:`~asyncio.loop.create_server` doesn't support injecting a + custom ``Server`` class, the easiest solution that doesn't rely on + private :mod:`asyncio` APIs is to: + + - instantiate a :class:`WebSocketServer` + - give the protocol factory a reference to that instance + - call :meth:`~asyncio.loop.create_server` with the factory + - attach the resulting :class:`~asyncio.Server` with this method + + """ + self.server = server + for sock in server.sockets: + if sock.family == socket.AF_INET: + name = "%s:%d" % sock.getsockname() + elif sock.family == socket.AF_INET6: + name = "[%s]:%d" % sock.getsockname()[:2] + elif sock.family == socket.AF_UNIX: + name = sock.getsockname() + # In the unlikely event that someone runs websockets over a + # protocol other than IP or Unix sockets, avoid crashing. + else: # pragma: no cover + name = str(sock.getsockname()) + self.logger.info("server listening on %s", name) + + # Initialized here because we need a reference to the event loop. + # This should be moved back to __init__ when dropping Python < 3.10. + self.closed_waiter = server.get_loop().create_future() + + def register(self, protocol: WebSocketServerProtocol) -> None: + """ + Register a connection with this server. + + """ + self.websockets.add(protocol) + + def unregister(self, protocol: WebSocketServerProtocol) -> None: + """ + Unregister a connection with this server. + + """ + self.websockets.remove(protocol) + + def close(self, close_connections: bool = True) -> None: + """ + Close the server. + + * Close the underlying :class:`~asyncio.Server`. + * When ``close_connections`` is :obj:`True`, which is the default, + close existing connections. Specifically: + + * Reject opening WebSocket connections with an HTTP 503 (service + unavailable) error. This happens when the server accepted the TCP + connection but didn't complete the opening handshake before closing. + * Close open WebSocket connections with close code 1001 (going away). + + * Wait until all connection handlers terminate. + + :meth:`close` is idempotent. + + """ + if self.close_task is None: + self.close_task = self.get_loop().create_task( + self._close(close_connections) + ) + + async def _close(self, close_connections: bool) -> None: + """ + Implementation of :meth:`close`. + + This calls :meth:`~asyncio.Server.close` on the underlying + :class:`~asyncio.Server` object to stop accepting new connections and + then closes open connections with close code 1001. + + """ + self.logger.info("server closing") + + # Stop accepting new connections. + self.server.close() + + # Wait until all accepted connections reach connection_made() and call + # register(). See https://bugs.python.org/issue34852 for details. + await asyncio.sleep(0) + + if close_connections: + # Close OPEN connections with close code 1001. After server.close(), + # handshake() closes OPENING connections with an HTTP 503 error. + close_tasks = [ + asyncio.create_task(websocket.close(1001)) + for websocket in self.websockets + if websocket.state is not State.CONNECTING + ] + # asyncio.wait doesn't accept an empty first argument. + if close_tasks: + await asyncio.wait(close_tasks) + + # Wait until all TCP connections are closed. + await self.server.wait_closed() + + # Wait until all connection handlers terminate. + # asyncio.wait doesn't accept an empty first argument. + if self.websockets: + await asyncio.wait( + [websocket.handler_task for websocket in self.websockets] + ) + + # Tell wait_closed() to return. + self.closed_waiter.set_result(None) + + self.logger.info("server closed") + + async def wait_closed(self) -> None: + """ + Wait until the server is closed. + + When :meth:`wait_closed` returns, all TCP connections are closed and + all connection handlers have returned. + + To ensure a fast shutdown, a connection handler should always be + awaiting at least one of: + + * :meth:`~WebSocketServerProtocol.recv`: when the connection is closed, + it raises :exc:`~websockets.exceptions.ConnectionClosedOK`; + * :meth:`~WebSocketServerProtocol.wait_closed`: when the connection is + closed, it returns. + + Then the connection handler is immediately notified of the shutdown; + it can clean up and exit. + + """ + await asyncio.shield(self.closed_waiter) + + def get_loop(self) -> asyncio.AbstractEventLoop: + """ + See :meth:`asyncio.Server.get_loop`. + + """ + return self.server.get_loop() + + def is_serving(self) -> bool: + """ + See :meth:`asyncio.Server.is_serving`. + + """ + return self.server.is_serving() + + async def start_serving(self) -> None: # pragma: no cover + """ + See :meth:`asyncio.Server.start_serving`. + + Typical use:: + + server = await serve(..., start_serving=False) + # perform additional setup here... + # ... then start the server + await server.start_serving() + + """ + await self.server.start_serving() + + async def serve_forever(self) -> None: # pragma: no cover + """ + See :meth:`asyncio.Server.serve_forever`. + + Typical use:: + + server = await serve(...) + # this coroutine doesn't return + # canceling it stops the server + await server.serve_forever() + + This is an alternative to using :func:`serve` as an asynchronous context + manager. Shutdown is triggered by canceling :meth:`serve_forever` + instead of exiting a :func:`serve` context. + + """ + await self.server.serve_forever() + + @property + def sockets(self) -> Iterable[socket.socket]: + """ + See :attr:`asyncio.Server.sockets`. + + """ + return self.server.sockets + + async def __aenter__(self) -> WebSocketServer: # pragma: no cover + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: # pragma: no cover + self.close() + await self.wait_closed() + + +class Serve: + """ + Start a WebSocket server listening on ``host`` and ``port``. + + Whenever a client connects, the server creates a + :class:`WebSocketServerProtocol`, performs the opening handshake, and + delegates to the connection handler, ``ws_handler``. + + The handler receives the :class:`WebSocketServerProtocol` and uses it to + send and receive messages. + + Once the handler completes, either normally or with an exception, the + server performs the closing handshake and closes the connection. + + Awaiting :func:`serve` yields a :class:`WebSocketServer`. This object + provides a :meth:`~WebSocketServer.close` method to shut down the server:: + + stop = asyncio.Future() # set this future to exit the server + + server = await serve(...) + await stop + await server.close() + + :func:`serve` can be used as an asynchronous context manager. Then, the + server is shut down automatically when exiting the context:: + + stop = asyncio.Future() # set this future to exit the server + + async with serve(...): + await stop + + Args: + ws_handler: Connection handler. It receives the WebSocket connection, + which is a :class:`WebSocketServerProtocol`, in argument. + host: Network interfaces the server binds to. + See :meth:`~asyncio.loop.create_server` for details. + port: TCP port the server listens on. + See :meth:`~asyncio.loop.create_server` for details. + create_protocol: Factory for the :class:`asyncio.Protocol` managing + the connection. It defaults to :class:`WebSocketServerProtocol`. + Set it to a wrapper or a subclass to customize connection handling. + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.server")``. + See the :doc:`logging guide <../../topics/logging>` for details. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + origins: Acceptable values of the ``Origin`` header, for defending + against Cross-Site WebSocket Hijacking attacks. Include :obj:`None` + in the list if the lack of an origin is acceptable. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing + preference. + extra_headers (Union[HeadersLike, Callable[[str, Headers], HeadersLike]]): + Arbitrary HTTP headers to add to the response. This can be + a :data:`~websockets.datastructures.HeadersLike` or a callable + taking the request path and headers in arguments and returning + a :data:`~websockets.datastructures.HeadersLike`. + server_header: Value of the ``Server`` response header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. + Setting it to :obj:`None` removes the header. + process_request (Optional[Callable[[str, Headers], \ + Awaitable[Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]]]]): + Intercept HTTP request before the opening handshake. + See :meth:`~WebSocketServerProtocol.process_request` for details. + select_subprotocol: Select a subprotocol supported by the client. + See :meth:`~WebSocketServerProtocol.select_subprotocol` for details. + open_timeout: Timeout for opening connections in seconds. + :obj:`None` disables the timeout. + + See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the + documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, + ``max_size``, ``max_queue``, ``read_limit``, and ``write_limit``. + + Any other keyword arguments are passed the event loop's + :meth:`~asyncio.loop.create_server` method. + + For example: + + * You can set ``ssl`` to a :class:`~ssl.SSLContext` to enable TLS. + + * You can set ``sock`` to a :obj:`~socket.socket` that you created + outside of websockets. + + Returns: + WebSocketServer: WebSocket server. + + """ + + def __init__( + self, + ws_handler: Union[ + Callable[[WebSocketServerProtocol], Awaitable[Any]], + Callable[[WebSocketServerProtocol, str], Awaitable[Any]], # deprecated + ], + host: Optional[Union[str, Sequence[str]]] = None, + port: Optional[int] = None, + *, + create_protocol: Optional[Callable[..., WebSocketServerProtocol]] = None, + logger: Optional[LoggerLike] = None, + compression: Optional[str] = "deflate", + origins: Optional[Sequence[Optional[Origin]]] = None, + extensions: Optional[Sequence[ServerExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + extra_headers: Optional[HeadersLikeOrCallable] = None, + server_header: Optional[str] = USER_AGENT, + process_request: Optional[ + Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]] + ] = None, + select_subprotocol: Optional[ + Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] + ] = None, + open_timeout: Optional[float] = 10, + ping_interval: Optional[float] = 20, + ping_timeout: Optional[float] = 20, + close_timeout: Optional[float] = None, + max_size: Optional[int] = 2**20, + max_queue: Optional[int] = 2**5, + read_limit: int = 2**16, + write_limit: int = 2**16, + **kwargs: Any, + ) -> None: + # Backwards compatibility: close_timeout used to be called timeout. + timeout: Optional[float] = kwargs.pop("timeout", None) + if timeout is None: + timeout = 10 + else: + warnings.warn("rename timeout to close_timeout", DeprecationWarning) + # If both are specified, timeout is ignored. + if close_timeout is None: + close_timeout = timeout + + # Backwards compatibility: create_protocol used to be called klass. + klass: Optional[Type[WebSocketServerProtocol]] = kwargs.pop("klass", None) + if klass is None: + klass = WebSocketServerProtocol + else: + warnings.warn("rename klass to create_protocol", DeprecationWarning) + # If both are specified, klass is ignored. + if create_protocol is None: + create_protocol = klass + + # Backwards compatibility: recv() used to return None on closed connections + legacy_recv: bool = kwargs.pop("legacy_recv", False) + + # Backwards compatibility: the loop parameter used to be supported. + _loop: Optional[asyncio.AbstractEventLoop] = kwargs.pop("loop", None) + if _loop is None: + loop = asyncio.get_event_loop() + else: + loop = _loop + warnings.warn("remove loop argument", DeprecationWarning) + + ws_server = WebSocketServer(logger=logger) + + secure = kwargs.get("ssl") is not None + + if compression == "deflate": + extensions = enable_server_permessage_deflate(extensions) + elif compression is not None: + raise ValueError(f"unsupported compression: {compression}") + + if subprotocols is not None: + validate_subprotocols(subprotocols) + + factory = functools.partial( + create_protocol, + # For backwards compatibility with 10.0 or earlier. Done here in + # addition to WebSocketServerProtocol to trigger the deprecation + # warning once per serve() call rather than once per connection. + remove_path_argument(ws_handler), + ws_server, + host=host, + port=port, + secure=secure, + open_timeout=open_timeout, + ping_interval=ping_interval, + ping_timeout=ping_timeout, + close_timeout=close_timeout, + max_size=max_size, + max_queue=max_queue, + read_limit=read_limit, + write_limit=write_limit, + loop=_loop, + legacy_recv=legacy_recv, + origins=origins, + extensions=extensions, + subprotocols=subprotocols, + extra_headers=extra_headers, + server_header=server_header, + process_request=process_request, + select_subprotocol=select_subprotocol, + logger=logger, + ) + + if kwargs.pop("unix", False): + path: Optional[str] = kwargs.pop("path", None) + # unix_serve(path) must not specify host and port parameters. + assert host is None and port is None + create_server = functools.partial( + loop.create_unix_server, factory, path, **kwargs + ) + else: + create_server = functools.partial( + loop.create_server, factory, host, port, **kwargs + ) + + # This is a coroutine function. + self._create_server = create_server + self.ws_server = ws_server + + # async with serve(...) + + async def __aenter__(self) -> WebSocketServer: + return await self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + self.ws_server.close() + await self.ws_server.wait_closed() + + # await serve(...) + + def __await__(self) -> Generator[Any, None, WebSocketServer]: + # Create a suitable iterator by calling __await__ on a coroutine. + return self.__await_impl__().__await__() + + async def __await_impl__(self) -> WebSocketServer: + server = await self._create_server() + self.ws_server.wrap(server) + return self.ws_server + + # yield from serve(...) - remove when dropping Python < 3.10 + + __iter__ = __await__ + + +serve = Serve + + +def unix_serve( + ws_handler: Union[ + Callable[[WebSocketServerProtocol], Awaitable[Any]], + Callable[[WebSocketServerProtocol, str], Awaitable[Any]], # deprecated + ], + path: Optional[str] = None, + **kwargs: Any, +) -> Serve: + """ + Start a WebSocket server listening on a Unix socket. + + This function is identical to :func:`serve`, except the ``host`` and + ``port`` arguments are replaced by ``path``. It is only available on Unix. + + Unrecognized keyword arguments are passed the event loop's + :meth:`~asyncio.loop.create_unix_server` method. + + It's useful for deploying a server behind a reverse proxy such as nginx. + + Args: + path: File system path to the Unix socket. + + """ + return serve(ws_handler, path=path, unix=True, **kwargs) + + +def remove_path_argument( + ws_handler: Union[ + Callable[[WebSocketServerProtocol], Awaitable[Any]], + Callable[[WebSocketServerProtocol, str], Awaitable[Any]], + ] +) -> Callable[[WebSocketServerProtocol], Awaitable[Any]]: + try: + inspect.signature(ws_handler).bind(None) + except TypeError: + try: + inspect.signature(ws_handler).bind(None, "") + except TypeError: # pragma: no cover + # ws_handler accepts neither one nor two arguments; leave it alone. + pass + else: + # ws_handler accepts two arguments; activate backwards compatibility. + + # Enable deprecation warning and announce deprecation in 11.0. + # warnings.warn("remove second argument of ws_handler", DeprecationWarning) + + async def _ws_handler(websocket: WebSocketServerProtocol) -> Any: + return await cast( + Callable[[WebSocketServerProtocol, str], Awaitable[Any]], + ws_handler, + )(websocket, websocket.path) + + return _ws_handler + + return cast( + Callable[[WebSocketServerProtocol], Awaitable[Any]], + ws_handler, + ) diff --git a/websockets/protocol.py b/websockets/protocol.py new file mode 100644 index 0000000..765e6b9 --- /dev/null +++ b/websockets/protocol.py @@ -0,0 +1,708 @@ +from __future__ import annotations + +import enum +import logging +import uuid +from typing import Generator, List, Optional, Type, Union + +from .exceptions import ( + ConnectionClosed, + ConnectionClosedError, + ConnectionClosedOK, + InvalidState, + PayloadTooBig, + ProtocolError, +) +from .extensions import Extension +from .frames import ( + OK_CLOSE_CODES, + OP_BINARY, + OP_CLOSE, + OP_CONT, + OP_PING, + OP_PONG, + OP_TEXT, + Close, + CloseCode, + Frame, +) +from .http11 import Request, Response +from .streams import StreamReader +from .typing import LoggerLike, Origin, Subprotocol + + +__all__ = [ + "Protocol", + "Side", + "State", + "SEND_EOF", +] + +Event = Union[Request, Response, Frame] +"""Events that :meth:`~Protocol.events_received` may return.""" + + +class Side(enum.IntEnum): + """A WebSocket connection is either a server or a client.""" + + SERVER, CLIENT = range(2) + + +SERVER = Side.SERVER +CLIENT = Side.CLIENT + + +class State(enum.IntEnum): + """A WebSocket connection is in one of these four states.""" + + CONNECTING, OPEN, CLOSING, CLOSED = range(4) + + +CONNECTING = State.CONNECTING +OPEN = State.OPEN +CLOSING = State.CLOSING +CLOSED = State.CLOSED + + +SEND_EOF = b"" +"""Sentinel signaling that the TCP connection must be half-closed.""" + + +class Protocol: + """ + Sans-I/O implementation of a WebSocket connection. + + Args: + side: :attr:`~Side.CLIENT` or :attr:`~Side.SERVER`. + state: initial state of the WebSocket connection. + max_size: maximum size of incoming messages in bytes; + :obj:`None` disables the limit. + logger: logger for this connection; depending on ``side``, + defaults to ``logging.getLogger("websockets.client")`` + or ``logging.getLogger("websockets.server")``; + see the :doc:`logging guide <../../topics/logging>` for details. + + """ + + def __init__( + self, + side: Side, + *, + state: State = OPEN, + max_size: Optional[int] = 2**20, + logger: Optional[LoggerLike] = None, + ) -> None: + # Unique identifier. For logs. + self.id: uuid.UUID = uuid.uuid4() + """Unique identifier of the connection. Useful in logs.""" + + # Logger or LoggerAdapter for this connection. + if logger is None: + logger = logging.getLogger(f"websockets.{side.name.lower()}") + self.logger: LoggerLike = logger + """Logger for this connection.""" + + # Track if DEBUG is enabled. Shortcut logging calls if it isn't. + self.debug = logger.isEnabledFor(logging.DEBUG) + + # Connection side. CLIENT or SERVER. + self.side = side + + # Connection state. Initially OPEN because subclasses handle CONNECTING. + self.state = state + + # Maximum size of incoming messages in bytes. + self.max_size = max_size + + # Current size of incoming message in bytes. Only set while reading a + # fragmented message i.e. a data frames with the FIN bit not set. + self.cur_size: Optional[int] = None + + # True while sending a fragmented message i.e. a data frames with the + # FIN bit not set. + self.expect_continuation_frame = False + + # WebSocket protocol parameters. + self.origin: Optional[Origin] = None + self.extensions: List[Extension] = [] + self.subprotocol: Optional[Subprotocol] = None + + # Close code and reason, set when a close frame is sent or received. + self.close_rcvd: Optional[Close] = None + self.close_sent: Optional[Close] = None + self.close_rcvd_then_sent: Optional[bool] = None + + # Track if an exception happened during the handshake. + self.handshake_exc: Optional[Exception] = None + """ + Exception to raise if the opening handshake failed. + + :obj:`None` if the opening handshake succeeded. + + """ + + # Track if send_eof() was called. + self.eof_sent = False + + # Parser state. + self.reader = StreamReader() + self.events: List[Event] = [] + self.writes: List[bytes] = [] + self.parser = self.parse() + next(self.parser) # start coroutine + self.parser_exc: Optional[Exception] = None + + @property + def state(self) -> State: + """ + WebSocket connection state. + + Defined in 4.1, 4.2, 7.1.3, and 7.1.4 of :rfc:`6455`. + + """ + return self._state + + @state.setter + def state(self, state: State) -> None: + if self.debug: + self.logger.debug("= connection is %s", state.name) + self._state = state + + @property + def close_code(self) -> Optional[int]: + """ + `WebSocket close code`_. + + .. _WebSocket close code: + https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 + + :obj:`None` if the connection isn't closed yet. + + """ + if self.state is not CLOSED: + return None + elif self.close_rcvd is None: + return CloseCode.ABNORMAL_CLOSURE + else: + return self.close_rcvd.code + + @property + def close_reason(self) -> Optional[str]: + """ + `WebSocket close reason`_. + + .. _WebSocket close reason: + https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 + + :obj:`None` if the connection isn't closed yet. + + """ + if self.state is not CLOSED: + return None + elif self.close_rcvd is None: + return "" + else: + return self.close_rcvd.reason + + @property + def close_exc(self) -> ConnectionClosed: + """ + Exception to raise when trying to interact with a closed connection. + + Don't raise this exception while the connection :attr:`state` + is :attr:`~websockets.protocol.State.CLOSING`; wait until + it's :attr:`~websockets.protocol.State.CLOSED`. + + Indeed, the exception includes the close code and reason, which are + known only once the connection is closed. + + Raises: + AssertionError: if the connection isn't closed yet. + + """ + assert self.state is CLOSED, "connection isn't closed yet" + exc_type: Type[ConnectionClosed] + if ( + self.close_rcvd is not None + and self.close_sent is not None + and self.close_rcvd.code in OK_CLOSE_CODES + and self.close_sent.code in OK_CLOSE_CODES + ): + exc_type = ConnectionClosedOK + else: + exc_type = ConnectionClosedError + exc: ConnectionClosed = exc_type( + self.close_rcvd, + self.close_sent, + self.close_rcvd_then_sent, + ) + # Chain to the exception raised in the parser, if any. + exc.__cause__ = self.parser_exc + return exc + + # Public methods for receiving data. + + def receive_data(self, data: bytes) -> None: + """ + Receive data from the network. + + After calling this method: + + - You must call :meth:`data_to_send` and send this data to the network. + - You should call :meth:`events_received` and process resulting events. + + Raises: + EOFError: if :meth:`receive_eof` was called earlier. + + """ + self.reader.feed_data(data) + next(self.parser) + + def receive_eof(self) -> None: + """ + Receive the end of the data stream from the network. + + After calling this method: + + - You must call :meth:`data_to_send` and send this data to the network; + it will return ``[b""]``, signaling the end of the stream, or ``[]``. + - You aren't expected to call :meth:`events_received`; it won't return + any new events. + + Raises: + EOFError: if :meth:`receive_eof` was called earlier. + + """ + self.reader.feed_eof() + next(self.parser) + + # Public methods for sending events. + + def send_continuation(self, data: bytes, fin: bool) -> None: + """ + Send a `Continuation frame`_. + + .. _Continuation frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 + + Parameters: + data: payload containing the same kind of data + as the initial frame. + fin: FIN bit; set it to :obj:`True` if this is the last frame + of a fragmented message and to :obj:`False` otherwise. + + Raises: + ProtocolError: if a fragmented message isn't in progress. + + """ + if not self.expect_continuation_frame: + raise ProtocolError("unexpected continuation frame") + self.expect_continuation_frame = not fin + self.send_frame(Frame(OP_CONT, data, fin)) + + def send_text(self, data: bytes, fin: bool = True) -> None: + """ + Send a `Text frame`_. + + .. _Text frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 + + Parameters: + data: payload containing text encoded with UTF-8. + fin: FIN bit; set it to :obj:`False` if this is the first frame of + a fragmented message. + + Raises: + ProtocolError: if a fragmented message is in progress. + + """ + if self.expect_continuation_frame: + raise ProtocolError("expected a continuation frame") + self.expect_continuation_frame = not fin + self.send_frame(Frame(OP_TEXT, data, fin)) + + def send_binary(self, data: bytes, fin: bool = True) -> None: + """ + Send a `Binary frame`_. + + .. _Binary frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 + + Parameters: + data: payload containing arbitrary binary data. + fin: FIN bit; set it to :obj:`False` if this is the first frame of + a fragmented message. + + Raises: + ProtocolError: if a fragmented message is in progress. + + """ + if self.expect_continuation_frame: + raise ProtocolError("expected a continuation frame") + self.expect_continuation_frame = not fin + self.send_frame(Frame(OP_BINARY, data, fin)) + + def send_close(self, code: Optional[int] = None, reason: str = "") -> None: + """ + Send a `Close frame`_. + + .. _Close frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 + + Parameters: + code: close code. + reason: close reason. + + Raises: + ProtocolError: if a fragmented message is being sent, if the code + isn't valid, or if a reason is provided without a code + + """ + if self.expect_continuation_frame: + raise ProtocolError("expected a continuation frame") + if code is None: + if reason != "": + raise ProtocolError("cannot send a reason without a code") + close = Close(CloseCode.NO_STATUS_RCVD, "") + data = b"" + else: + close = Close(code, reason) + data = close.serialize() + # send_frame() guarantees that self.state is OPEN at this point. + # 7.1.3. The WebSocket Closing Handshake is Started + self.send_frame(Frame(OP_CLOSE, data)) + self.close_sent = close + self.state = CLOSING + + def send_ping(self, data: bytes) -> None: + """ + Send a `Ping frame`_. + + .. _Ping frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 + + Parameters: + data: payload containing arbitrary binary data. + + """ + self.send_frame(Frame(OP_PING, data)) + + def send_pong(self, data: bytes) -> None: + """ + Send a `Pong frame`_. + + .. _Pong frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 + + Parameters: + data: payload containing arbitrary binary data. + + """ + self.send_frame(Frame(OP_PONG, data)) + + def fail(self, code: int, reason: str = "") -> None: + """ + `Fail the WebSocket connection`_. + + .. _Fail the WebSocket connection: + https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.7 + + Parameters: + code: close code + reason: close reason + + Raises: + ProtocolError: if the code isn't valid. + """ + # 7.1.7. Fail the WebSocket Connection + + # Send a close frame when the state is OPEN (a close frame was already + # sent if it's CLOSING), except when failing the connection because + # of an error reading from or writing to the network. + if self.state is OPEN: + if code != CloseCode.ABNORMAL_CLOSURE: + close = Close(code, reason) + data = close.serialize() + self.send_frame(Frame(OP_CLOSE, data)) + self.close_sent = close + self.state = CLOSING + + # When failing the connection, a server closes the TCP connection + # without waiting for the client to complete the handshake, while a + # client waits for the server to close the TCP connection, possibly + # after sending a close frame that the client will ignore. + if self.side is SERVER and not self.eof_sent: + self.send_eof() + + # 7.1.7. Fail the WebSocket Connection "An endpoint MUST NOT continue + # to attempt to process data(including a responding Close frame) from + # the remote endpoint after being instructed to _Fail the WebSocket + # Connection_." + self.parser = self.discard() + next(self.parser) # start coroutine + + # Public method for getting incoming events after receiving data. + + def events_received(self) -> List[Event]: + """ + Fetch events generated from data received from the network. + + Call this method immediately after any of the ``receive_*()`` methods. + + Process resulting events, likely by passing them to the application. + + Returns: + List[Event]: Events read from the connection. + """ + events, self.events = self.events, [] + return events + + # Public method for getting outgoing data after receiving data or sending events. + + def data_to_send(self) -> List[bytes]: + """ + Obtain data to send to the network. + + Call this method immediately after any of the ``receive_*()``, + ``send_*()``, or :meth:`fail` methods. + + Write resulting data to the connection. + + The empty bytestring :data:`~websockets.protocol.SEND_EOF` signals + the end of the data stream. When you receive it, half-close the TCP + connection. + + Returns: + List[bytes]: Data to write to the connection. + + """ + writes, self.writes = self.writes, [] + return writes + + def close_expected(self) -> bool: + """ + Tell if the TCP connection is expected to close soon. + + Call this method immediately after any of the ``receive_*()``, + ``send_close()``, or :meth:`fail` methods. + + If it returns :obj:`True`, schedule closing the TCP connection after a + short timeout if the other side hasn't already closed it. + + Returns: + bool: Whether the TCP connection is expected to close soon. + + """ + # We expect a TCP close if and only if we sent a close frame: + # * Normal closure: once we send a close frame, we expect a TCP close: + # server waits for client to complete the TCP closing handshake; + # client waits for server to initiate the TCP closing handshake. + # * Abnormal closure: we always send a close frame and the same logic + # applies, except on EOFError where we don't send a close frame + # because we already received the TCP close, so we don't expect it. + # We already got a TCP Close if and only if the state is CLOSED. + return self.state is CLOSING or self.handshake_exc is not None + + # Private methods for receiving data. + + def parse(self) -> Generator[None, None, None]: + """ + Parse incoming data into frames. + + :meth:`receive_data` and :meth:`receive_eof` run this generator + coroutine until it needs more data or reaches EOF. + + :meth:`parse` never raises an exception. Instead, it sets the + :attr:`parser_exc` and yields control. + + """ + try: + while True: + if (yield from self.reader.at_eof()): + if self.debug: + self.logger.debug("< EOF") + # If the WebSocket connection is closed cleanly, with a + # closing handhshake, recv_frame() substitutes parse() + # with discard(). This branch is reached only when the + # connection isn't closed cleanly. + raise EOFError("unexpected end of stream") + + if self.max_size is None: + max_size = None + elif self.cur_size is None: + max_size = self.max_size + else: + max_size = self.max_size - self.cur_size + + # During a normal closure, execution ends here on the next + # iteration of the loop after receiving a close frame. At + # this point, recv_frame() replaced parse() by discard(). + frame = yield from Frame.parse( + self.reader.read_exact, + mask=self.side is SERVER, + max_size=max_size, + extensions=self.extensions, + ) + + if self.debug: + self.logger.debug("< %s", frame) + + self.recv_frame(frame) + + except ProtocolError as exc: + self.fail(CloseCode.PROTOCOL_ERROR, str(exc)) + self.parser_exc = exc + + except EOFError as exc: + self.fail(CloseCode.ABNORMAL_CLOSURE, str(exc)) + self.parser_exc = exc + + except UnicodeDecodeError as exc: + self.fail(CloseCode.INVALID_DATA, f"{exc.reason} at position {exc.start}") + self.parser_exc = exc + + except PayloadTooBig as exc: + self.fail(CloseCode.MESSAGE_TOO_BIG, str(exc)) + self.parser_exc = exc + + except Exception as exc: + self.logger.error("parser failed", exc_info=True) + # Don't include exception details, which may be security-sensitive. + self.fail(CloseCode.INTERNAL_ERROR) + self.parser_exc = exc + + # During an abnormal closure, execution ends here after catching an + # exception. At this point, fail() replaced parse() by discard(). + yield + raise AssertionError("parse() shouldn't step after error") + + def discard(self) -> Generator[None, None, None]: + """ + Discard incoming data. + + This coroutine replaces :meth:`parse`: + + - after receiving a close frame, during a normal closure (1.4); + - after sending a close frame, during an abnormal closure (7.1.7). + + """ + # The server close the TCP connection in the same circumstances where + # discard() replaces parse(). The client closes the connection later, + # after the server closes the connection or a timeout elapses. + # (The latter case cannot be handled in this Sans-I/O layer.) + assert (self.side is SERVER) == (self.eof_sent) + while not (yield from self.reader.at_eof()): + self.reader.discard() + if self.debug: + self.logger.debug("< EOF") + # A server closes the TCP connection immediately, while a client + # waits for the server to close the TCP connection. + if self.side is CLIENT: + self.send_eof() + self.state = CLOSED + # If discard() completes normally, execution ends here. + yield + # Once the reader reaches EOF, its feed_data/eof() methods raise an + # error, so our receive_data/eof() methods don't step the generator. + raise AssertionError("discard() shouldn't step after EOF") + + def recv_frame(self, frame: Frame) -> None: + """ + Process an incoming frame. + + """ + if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY: + if self.cur_size is not None: + raise ProtocolError("expected a continuation frame") + if frame.fin: + self.cur_size = None + else: + self.cur_size = len(frame.data) + + elif frame.opcode is OP_CONT: + if self.cur_size is None: + raise ProtocolError("unexpected continuation frame") + if frame.fin: + self.cur_size = None + else: + self.cur_size += len(frame.data) + + elif frame.opcode is OP_PING: + # 5.5.2. Ping: "Upon receipt of a Ping frame, an endpoint MUST + # send a Pong frame in response" + pong_frame = Frame(OP_PONG, frame.data) + self.send_frame(pong_frame) + + elif frame.opcode is OP_PONG: + # 5.5.3 Pong: "A response to an unsolicited Pong frame is not + # expected." + pass + + elif frame.opcode is OP_CLOSE: + # 7.1.5. The WebSocket Connection Close Code + # 7.1.6. The WebSocket Connection Close Reason + self.close_rcvd = Close.parse(frame.data) + if self.state is CLOSING: + assert self.close_sent is not None + self.close_rcvd_then_sent = False + + if self.cur_size is not None: + raise ProtocolError("incomplete fragmented message") + + # 5.5.1 Close: "If an endpoint receives a Close frame and did + # not previously send a Close frame, the endpoint MUST send a + # Close frame in response. (When sending a Close frame in + # response, the endpoint typically echos the status code it + # received.)" + + if self.state is OPEN: + # Echo the original data instead of re-serializing it with + # Close.serialize() because that fails when the close frame + # is empty and Close.parse() synthesizes a 1005 close code. + # The rest is identical to send_close(). + self.send_frame(Frame(OP_CLOSE, frame.data)) + self.close_sent = self.close_rcvd + self.close_rcvd_then_sent = True + self.state = CLOSING + + # 7.1.2. Start the WebSocket Closing Handshake: "Once an + # endpoint has both sent and received a Close control frame, + # that endpoint SHOULD _Close the WebSocket Connection_" + + # A server closes the TCP connection immediately, while a client + # waits for the server to close the TCP connection. + if self.side is SERVER: + self.send_eof() + + # 1.4. Closing Handshake: "after receiving a control frame + # indicating the connection should be closed, a peer discards + # any further data received." + self.parser = self.discard() + next(self.parser) # start coroutine + + else: + # This can't happen because Frame.parse() validates opcodes. + raise AssertionError(f"unexpected opcode: {frame.opcode:02x}") + + self.events.append(frame) + + # Private methods for sending events. + + def send_frame(self, frame: Frame) -> None: + if self.state is not OPEN: + raise InvalidState( + f"cannot write to a WebSocket in the {self.state.name} state" + ) + + if self.debug: + self.logger.debug("> %s", frame) + self.writes.append( + frame.serialize(mask=self.side is CLIENT, extensions=self.extensions) + ) + + def send_eof(self) -> None: + assert not self.eof_sent + self.eof_sent = True + if self.debug: + self.logger.debug("> EOF") + self.writes.append(SEND_EOF) diff --git a/websockets/py.typed b/websockets/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/websockets/server.py b/websockets/server.py new file mode 100644 index 0000000..b9646ea --- /dev/null +++ b/websockets/server.py @@ -0,0 +1,577 @@ +from __future__ import annotations + +import base64 +import binascii +import email.utils +import http +import warnings +from typing import Any, Callable, Generator, List, Optional, Sequence, Tuple, cast + +from .datastructures import Headers, MultipleValuesError +from .exceptions import ( + InvalidHandshake, + InvalidHeader, + InvalidHeaderValue, + InvalidOrigin, + InvalidStatus, + InvalidUpgrade, + NegotiationError, +) +from .extensions import Extension, ServerExtensionFactory +from .headers import ( + build_extension, + parse_connection, + parse_extension, + parse_subprotocol, + parse_upgrade, +) +from .http11 import Request, Response +from .protocol import CONNECTING, OPEN, SERVER, Protocol, State +from .typing import ( + ConnectionOption, + ExtensionHeader, + LoggerLike, + Origin, + Subprotocol, + UpgradeProtocol, +) +from .utils import accept_key + + +# See #940 for why lazy_import isn't used here for backwards compatibility. +from .legacy.server import * # isort:skip # noqa: I001 + + +__all__ = ["ServerProtocol"] + + +class ServerProtocol(Protocol): + """ + Sans-I/O implementation of a WebSocket server connection. + + Args: + origins: acceptable values of the ``Origin`` header; include + :obj:`None` in the list if the lack of an origin is acceptable. + This is useful for defending against Cross-Site WebSocket + Hijacking attacks. + extensions: list of supported extensions, in order in which they + should be tried. + subprotocols: list of supported subprotocols, in order of decreasing + preference. + select_subprotocol: Callback for selecting a subprotocol among + those supported by the client and the server. It has the same + signature as the :meth:`select_subprotocol` method, including a + :class:`ServerProtocol` instance as first argument. + state: initial state of the WebSocket connection. + max_size: maximum size of incoming messages in bytes; + :obj:`None` disables the limit. + logger: logger for this connection; + defaults to ``logging.getLogger("websockets.client")``; + see the :doc:`logging guide <../../topics/logging>` for details. + + """ + + def __init__( + self, + *, + origins: Optional[Sequence[Optional[Origin]]] = None, + extensions: Optional[Sequence[ServerExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + select_subprotocol: Optional[ + Callable[ + [ServerProtocol, Sequence[Subprotocol]], + Optional[Subprotocol], + ] + ] = None, + state: State = CONNECTING, + max_size: Optional[int] = 2**20, + logger: Optional[LoggerLike] = None, + ): + super().__init__( + side=SERVER, + state=state, + max_size=max_size, + logger=logger, + ) + self.origins = origins + self.available_extensions = extensions + self.available_subprotocols = subprotocols + if select_subprotocol is not None: + # Bind select_subprotocol then shadow self.select_subprotocol. + # Use setattr to work around https://github.com/python/mypy/issues/2427. + setattr( + self, + "select_subprotocol", + select_subprotocol.__get__(self, self.__class__), + ) + + def accept(self, request: Request) -> Response: + """ + Create a handshake response to accept the connection. + + If the connection cannot be established, the handshake response + actually rejects the handshake. + + You must send the handshake response with :meth:`send_response`. + + You may modify it before sending it, for example to add HTTP headers. + + Args: + request: WebSocket handshake request event received from the client. + + Returns: + WebSocket handshake response event to send to the client. + + """ + try: + ( + accept_header, + extensions_header, + protocol_header, + ) = self.process_request(request) + except InvalidOrigin as exc: + request._exception = exc + self.handshake_exc = exc + if self.debug: + self.logger.debug("! invalid origin", exc_info=True) + return self.reject( + http.HTTPStatus.FORBIDDEN, + f"Failed to open a WebSocket connection: {exc}.\n", + ) + except InvalidUpgrade as exc: + request._exception = exc + self.handshake_exc = exc + if self.debug: + self.logger.debug("! invalid upgrade", exc_info=True) + response = self.reject( + http.HTTPStatus.UPGRADE_REQUIRED, + ( + f"Failed to open a WebSocket connection: {exc}.\n" + f"\n" + f"You cannot access a WebSocket server directly " + f"with a browser. You need a WebSocket client.\n" + ), + ) + response.headers["Upgrade"] = "websocket" + return response + except InvalidHandshake as exc: + request._exception = exc + self.handshake_exc = exc + if self.debug: + self.logger.debug("! invalid handshake", exc_info=True) + return self.reject( + http.HTTPStatus.BAD_REQUEST, + f"Failed to open a WebSocket connection: {exc}.\n", + ) + except Exception as exc: + # Handle exceptions raised by user-provided select_subprotocol and + # unexpected errors. + request._exception = exc + self.handshake_exc = exc + self.logger.error("opening handshake failed", exc_info=True) + return self.reject( + http.HTTPStatus.INTERNAL_SERVER_ERROR, + ( + "Failed to open a WebSocket connection.\n" + "See server log for more information.\n" + ), + ) + + headers = Headers() + + headers["Date"] = email.utils.formatdate(usegmt=True) + + headers["Upgrade"] = "websocket" + headers["Connection"] = "Upgrade" + headers["Sec-WebSocket-Accept"] = accept_header + + if extensions_header is not None: + headers["Sec-WebSocket-Extensions"] = extensions_header + + if protocol_header is not None: + headers["Sec-WebSocket-Protocol"] = protocol_header + + self.logger.info("connection open") + return Response(101, "Switching Protocols", headers) + + def process_request( + self, + request: Request, + ) -> Tuple[str, Optional[str], Optional[str]]: + """ + Check a handshake request and negotiate extensions and subprotocol. + + This function doesn't verify that the request is an HTTP/1.1 or higher + GET request and doesn't check the ``Host`` header. These controls are + usually performed earlier in the HTTP request handling code. They're + the responsibility of the caller. + + Args: + request: WebSocket handshake request received from the client. + + Returns: + Tuple[str, Optional[str], Optional[str]]: + ``Sec-WebSocket-Accept``, ``Sec-WebSocket-Extensions``, and + ``Sec-WebSocket-Protocol`` headers for the handshake response. + + Raises: + InvalidHandshake: if the handshake request is invalid; + then the server must return 400 Bad Request error. + + """ + headers = request.headers + + connection: List[ConnectionOption] = sum( + [parse_connection(value) for value in headers.get_all("Connection")], [] + ) + + if not any(value.lower() == "upgrade" for value in connection): + raise InvalidUpgrade( + "Connection", ", ".join(connection) if connection else None + ) + + upgrade: List[UpgradeProtocol] = sum( + [parse_upgrade(value) for value in headers.get_all("Upgrade")], [] + ) + + # For compatibility with non-strict implementations, ignore case when + # checking the Upgrade header. The RFC always uses "websocket", except + # in section 11.2. (IANA registration) where it uses "WebSocket". + if not (len(upgrade) == 1 and upgrade[0].lower() == "websocket"): + raise InvalidUpgrade("Upgrade", ", ".join(upgrade) if upgrade else None) + + try: + key = headers["Sec-WebSocket-Key"] + except KeyError as exc: + raise InvalidHeader("Sec-WebSocket-Key") from exc + except MultipleValuesError as exc: + raise InvalidHeader( + "Sec-WebSocket-Key", "more than one Sec-WebSocket-Key header found" + ) from exc + + try: + raw_key = base64.b64decode(key.encode(), validate=True) + except binascii.Error as exc: + raise InvalidHeaderValue("Sec-WebSocket-Key", key) from exc + if len(raw_key) != 16: + raise InvalidHeaderValue("Sec-WebSocket-Key", key) + + try: + version = headers["Sec-WebSocket-Version"] + except KeyError as exc: + raise InvalidHeader("Sec-WebSocket-Version") from exc + except MultipleValuesError as exc: + raise InvalidHeader( + "Sec-WebSocket-Version", + "more than one Sec-WebSocket-Version header found", + ) from exc + + if version != "13": + raise InvalidHeaderValue("Sec-WebSocket-Version", version) + + accept_header = accept_key(key) + + self.origin = self.process_origin(headers) + + extensions_header, self.extensions = self.process_extensions(headers) + + protocol_header = self.subprotocol = self.process_subprotocol(headers) + + return ( + accept_header, + extensions_header, + protocol_header, + ) + + def process_origin(self, headers: Headers) -> Optional[Origin]: + """ + Handle the Origin HTTP request header. + + Args: + headers: WebSocket handshake request headers. + + Returns: + Optional[Origin]: origin, if it is acceptable. + + Raises: + InvalidHandshake: if the Origin header is invalid. + InvalidOrigin: if the origin isn't acceptable. + + """ + # "The user agent MUST NOT include more than one Origin header field" + # per https://www.rfc-editor.org/rfc/rfc6454.html#section-7.3. + try: + origin = cast(Optional[Origin], headers.get("Origin")) + except MultipleValuesError as exc: + raise InvalidHeader("Origin", "more than one Origin header found") from exc + if self.origins is not None: + if origin not in self.origins: + raise InvalidOrigin(origin) + return origin + + def process_extensions( + self, + headers: Headers, + ) -> Tuple[Optional[str], List[Extension]]: + """ + Handle the Sec-WebSocket-Extensions HTTP request header. + + Accept or reject each extension proposed in the client request. + Negotiate parameters for accepted extensions. + + Per :rfc:`6455`, negotiation rules are defined by the specification of + each extension. + + To provide this level of flexibility, for each extension proposed by + the client, we check for a match with each extension available in the + server configuration. If no match is found, the extension is ignored. + + If several variants of the same extension are proposed by the client, + it may be accepted several times, which won't make sense in general. + Extensions must implement their own requirements. For this purpose, + the list of previously accepted extensions is provided. + + This process doesn't allow the server to reorder extensions. It can + only select a subset of the extensions proposed by the client. + + Other requirements, for example related to mandatory extensions or the + order of extensions, may be implemented by overriding this method. + + Args: + headers: WebSocket handshake request headers. + + Returns: + Tuple[Optional[str], List[Extension]]: ``Sec-WebSocket-Extensions`` + HTTP response header and list of accepted extensions. + + Raises: + InvalidHandshake: if the Sec-WebSocket-Extensions header is invalid. + + """ + response_header_value: Optional[str] = None + + extension_headers: List[ExtensionHeader] = [] + accepted_extensions: List[Extension] = [] + + header_values = headers.get_all("Sec-WebSocket-Extensions") + + if header_values and self.available_extensions: + parsed_header_values: List[ExtensionHeader] = sum( + [parse_extension(header_value) for header_value in header_values], [] + ) + + for name, request_params in parsed_header_values: + for ext_factory in self.available_extensions: + # Skip non-matching extensions based on their name. + if ext_factory.name != name: + continue + + # Skip non-matching extensions based on their params. + try: + response_params, extension = ext_factory.process_request_params( + request_params, accepted_extensions + ) + except NegotiationError: + continue + + # Add matching extension to the final list. + extension_headers.append((name, response_params)) + accepted_extensions.append(extension) + + # Break out of the loop once we have a match. + break + + # If we didn't break from the loop, no extension in our list + # matched what the client sent. The extension is declined. + + # Serialize extension header. + if extension_headers: + response_header_value = build_extension(extension_headers) + + return response_header_value, accepted_extensions + + def process_subprotocol(self, headers: Headers) -> Optional[Subprotocol]: + """ + Handle the Sec-WebSocket-Protocol HTTP request header. + + Args: + headers: WebSocket handshake request headers. + + Returns: + Optional[Subprotocol]: Subprotocol, if one was selected; this is + also the value of the ``Sec-WebSocket-Protocol`` response header. + + Raises: + InvalidHandshake: if the Sec-WebSocket-Subprotocol header is invalid. + + """ + subprotocols: Sequence[Subprotocol] = sum( + [ + parse_subprotocol(header_value) + for header_value in headers.get_all("Sec-WebSocket-Protocol") + ], + [], + ) + + return self.select_subprotocol(subprotocols) + + def select_subprotocol( + self, + subprotocols: Sequence[Subprotocol], + ) -> Optional[Subprotocol]: + """ + Pick a subprotocol among those offered by the client. + + If several subprotocols are supported by both the client and the server, + pick the first one in the list declared the server. + + If the server doesn't support any subprotocols, continue without a + subprotocol, regardless of what the client offers. + + If the server supports at least one subprotocol and the client doesn't + offer any, abort the handshake with an HTTP 400 error. + + You provide a ``select_subprotocol`` argument to :class:`ServerProtocol` + to override this logic. For example, you could accept the connection + even if client doesn't offer a subprotocol, rather than reject it. + + Here's how to negotiate the ``chat`` subprotocol if the client supports + it and continue without a subprotocol otherwise:: + + def select_subprotocol(protocol, subprotocols): + if "chat" in subprotocols: + return "chat" + + Args: + subprotocols: list of subprotocols offered by the client. + + Returns: + Optional[Subprotocol]: Selected subprotocol, if a common subprotocol + was found. + + :obj:`None` to continue without a subprotocol. + + Raises: + NegotiationError: custom implementations may raise this exception + to abort the handshake with an HTTP 400 error. + + """ + # Server doesn't offer any subprotocols. + if not self.available_subprotocols: # None or empty list + return None + + # Server offers at least one subprotocol but client doesn't offer any. + if not subprotocols: + raise NegotiationError("missing subprotocol") + + # Server and client both offer subprotocols. Look for a shared one. + proposed_subprotocols = set(subprotocols) + for subprotocol in self.available_subprotocols: + if subprotocol in proposed_subprotocols: + return subprotocol + + # No common subprotocol was found. + raise NegotiationError( + "invalid subprotocol; expected one of " + + ", ".join(self.available_subprotocols) + ) + + def reject( + self, + status: http.HTTPStatus, + text: str, + ) -> Response: + """ + Create a handshake response to reject the connection. + + A short plain text response is the best fallback when failing to + establish a WebSocket connection. + + You must send the handshake response with :meth:`send_response`. + + You can modify it before sending it, for example to alter HTTP headers. + + Args: + status: HTTP status code. + text: HTTP response body; will be encoded to UTF-8. + + Returns: + Response: WebSocket handshake response event to send to the client. + + """ + # If a user passes an int instead of a HTTPStatus, fix it automatically. + status = http.HTTPStatus(status) + body = text.encode() + headers = Headers( + [ + ("Date", email.utils.formatdate(usegmt=True)), + ("Connection", "close"), + ("Content-Length", str(len(body))), + ("Content-Type", "text/plain; charset=utf-8"), + ] + ) + response = Response(status.value, status.phrase, headers, body) + # When reject() is called from accept(), handshake_exc is already set. + # If a user calls reject(), set handshake_exc to guarantee invariant: + # "handshake_exc is None if and only if opening handshake succeeded." + if self.handshake_exc is None: + self.handshake_exc = InvalidStatus(response) + self.logger.info("connection failed (%d %s)", status.value, status.phrase) + return response + + def send_response(self, response: Response) -> None: + """ + Send a handshake response to the client. + + Args: + response: WebSocket handshake response event to send. + + """ + if self.debug: + code, phrase = response.status_code, response.reason_phrase + self.logger.debug("> HTTP/1.1 %d %s", code, phrase) + for key, value in response.headers.raw_items(): + self.logger.debug("> %s: %s", key, value) + if response.body is not None: + self.logger.debug("> [body] (%d bytes)", len(response.body)) + + self.writes.append(response.serialize()) + + if response.status_code == 101: + assert self.state is CONNECTING + self.state = OPEN + else: + self.send_eof() + self.parser = self.discard() + next(self.parser) # start coroutine + + def parse(self) -> Generator[None, None, None]: + if self.state is CONNECTING: + try: + request = yield from Request.parse( + self.reader.read_line, + ) + except Exception as exc: + self.handshake_exc = exc + self.send_eof() + self.parser = self.discard() + next(self.parser) # start coroutine + yield + + if self.debug: + self.logger.debug("< GET %s HTTP/1.1", request.path) + for key, value in request.headers.raw_items(): + self.logger.debug("< %s: %s", key, value) + + self.events.append(request) + + yield from super().parse() + + +class ServerConnection(ServerProtocol): + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "ServerConnection was renamed to ServerProtocol", + DeprecationWarning, + ) + super().__init__(*args, **kwargs) diff --git a/websockets/speedups.c b/websockets/speedups.c new file mode 100644 index 0000000..a195904 --- /dev/null +++ b/websockets/speedups.c @@ -0,0 +1,223 @@ +/* C implementation of performance sensitive functions. */ + +#define PY_SSIZE_T_CLEAN +#include +#include /* uint8_t, uint32_t, uint64_t */ + +#if __ARM_NEON +#include +#elif __SSE2__ +#include +#endif + +static const Py_ssize_t MASK_LEN = 4; + +/* Similar to PyBytes_AsStringAndSize, but accepts more types */ + +static int +_PyBytesLike_AsStringAndSize(PyObject *obj, PyObject **tmp, char **buffer, Py_ssize_t *length) +{ + // This supports bytes, bytearrays, and memoryview objects, + // which are common data structures for handling byte streams. + // websockets.framing.prepare_data() returns only these types. + // If *tmp isn't NULL, the caller gets a new reference. + if (PyBytes_Check(obj)) + { + *tmp = NULL; + *buffer = PyBytes_AS_STRING(obj); + *length = PyBytes_GET_SIZE(obj); + } + else if (PyByteArray_Check(obj)) + { + *tmp = NULL; + *buffer = PyByteArray_AS_STRING(obj); + *length = PyByteArray_GET_SIZE(obj); + } + else if (PyMemoryView_Check(obj)) + { + *tmp = PyMemoryView_GetContiguous(obj, PyBUF_READ, 'C'); + if (*tmp == NULL) + { + return -1; + } + Py_buffer *mv_buf; + mv_buf = PyMemoryView_GET_BUFFER(*tmp); + *buffer = mv_buf->buf; + *length = mv_buf->len; + } + else + { + PyErr_Format( + PyExc_TypeError, + "expected a bytes-like object, %.200s found", + Py_TYPE(obj)->tp_name); + return -1; + } + + return 0; +} + +/* C implementation of websockets.utils.apply_mask */ + +static PyObject * +apply_mask(PyObject *self, PyObject *args, PyObject *kwds) +{ + + // In order to support various bytes-like types, accept any Python object. + + static char *kwlist[] = {"data", "mask", NULL}; + PyObject *input_obj; + PyObject *mask_obj; + + // A pointer to a char * + length will be extracted from the data and mask + // arguments, possibly via a Py_buffer. + + PyObject *input_tmp = NULL; + char *input; + Py_ssize_t input_len; + PyObject *mask_tmp = NULL; + char *mask; + Py_ssize_t mask_len; + + // Initialize a PyBytesObject then get a pointer to the underlying char * + // in order to avoid an extra memory copy in PyBytes_FromStringAndSize. + + PyObject *result = NULL; + char *output; + + // Other variables. + + Py_ssize_t i = 0; + + // Parse inputs. + + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "OO", kwlist, &input_obj, &mask_obj)) + { + goto exit; + } + + if (_PyBytesLike_AsStringAndSize(input_obj, &input_tmp, &input, &input_len) == -1) + { + goto exit; + } + + if (_PyBytesLike_AsStringAndSize(mask_obj, &mask_tmp, &mask, &mask_len) == -1) + { + goto exit; + } + + if (mask_len != MASK_LEN) + { + PyErr_SetString(PyExc_ValueError, "mask must contain 4 bytes"); + goto exit; + } + + // Create output. + + result = PyBytes_FromStringAndSize(NULL, input_len); + if (result == NULL) + { + goto exit; + } + + // Since we just created result, we don't need error checks. + output = PyBytes_AS_STRING(result); + + // Perform the masking operation. + + // Apparently GCC cannot figure out the following optimizations by itself. + + // We need a new scope for MSVC 2010 (non C99 friendly) + { +#if __ARM_NEON + + // With NEON support, XOR by blocks of 16 bytes = 128 bits. + + Py_ssize_t input_len_128 = input_len & ~15; + uint8x16_t mask_128 = vreinterpretq_u8_u32(vdupq_n_u32(*(uint32_t *)mask)); + + for (; i < input_len_128; i += 16) + { + uint8x16_t in_128 = vld1q_u8((uint8_t *)(input + i)); + uint8x16_t out_128 = veorq_u8(in_128, mask_128); + vst1q_u8((uint8_t *)(output + i), out_128); + } + +#elif __SSE2__ + + // With SSE2 support, XOR by blocks of 16 bytes = 128 bits. + + // Since we cannot control the 16-bytes alignment of input and output + // buffers, we rely on loadu/storeu rather than load/store. + + Py_ssize_t input_len_128 = input_len & ~15; + __m128i mask_128 = _mm_set1_epi32(*(uint32_t *)mask); + + for (; i < input_len_128; i += 16) + { + __m128i in_128 = _mm_loadu_si128((__m128i *)(input + i)); + __m128i out_128 = _mm_xor_si128(in_128, mask_128); + _mm_storeu_si128((__m128i *)(output + i), out_128); + } + +#else + + // Without SSE2 support, XOR by blocks of 8 bytes = 64 bits. + + // We assume the memory allocator aligns everything on 8 bytes boundaries. + + Py_ssize_t input_len_64 = input_len & ~7; + uint32_t mask_32 = *(uint32_t *)mask; + uint64_t mask_64 = ((uint64_t)mask_32 << 32) | (uint64_t)mask_32; + + for (; i < input_len_64; i += 8) + { + *(uint64_t *)(output + i) = *(uint64_t *)(input + i) ^ mask_64; + } + +#endif + } + + // XOR the remainder of the input byte by byte. + + for (; i < input_len; i++) + { + output[i] = input[i] ^ mask[i & (MASK_LEN - 1)]; + } + +exit: + Py_XDECREF(input_tmp); + Py_XDECREF(mask_tmp); + return result; + +} + +static PyMethodDef speedups_methods[] = { + { + "apply_mask", + (PyCFunction)apply_mask, + METH_VARARGS | METH_KEYWORDS, + "Apply masking to the data of a WebSocket message.", + }, + {NULL, NULL, 0, NULL}, /* Sentinel */ +}; + +static struct PyModuleDef speedups_module = { + PyModuleDef_HEAD_INIT, + "websocket.speedups", /* m_name */ + "C implementation of performance sensitive functions.", + /* m_doc */ + -1, /* m_size */ + speedups_methods, /* m_methods */ + NULL, + NULL, + NULL, + NULL +}; + +PyMODINIT_FUNC +PyInit_speedups(void) +{ + return PyModule_Create(&speedups_module); +} diff --git a/websockets/speedups.pyi b/websockets/speedups.pyi new file mode 100644 index 0000000..821438a --- /dev/null +++ b/websockets/speedups.pyi @@ -0,0 +1 @@ +def apply_mask(data: bytes, mask: bytes) -> bytes: ... diff --git a/websockets/streams.py b/websockets/streams.py new file mode 100644 index 0000000..f861d4b --- /dev/null +++ b/websockets/streams.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +from typing import Generator + + +class StreamReader: + """ + Generator-based stream reader. + + This class doesn't support concurrent calls to :meth:`read_line`, + :meth:`read_exact`, or :meth:`read_to_eof`. Make sure calls are + serialized. + + """ + + def __init__(self) -> None: + self.buffer = bytearray() + self.eof = False + + def read_line(self, m: int) -> Generator[None, None, bytes]: + """ + Read a LF-terminated line from the stream. + + This is a generator-based coroutine. + + The return value includes the LF character. + + Args: + m: maximum number bytes to read; this is a security limit. + + Raises: + EOFError: if the stream ends without a LF. + RuntimeError: if the stream ends in more than ``m`` bytes. + + """ + n = 0 # number of bytes to read + p = 0 # number of bytes without a newline + while True: + n = self.buffer.find(b"\n", p) + 1 + if n > 0: + break + p = len(self.buffer) + if p > m: + raise RuntimeError(f"read {p} bytes, expected no more than {m} bytes") + if self.eof: + raise EOFError(f"stream ends after {p} bytes, before end of line") + yield + if n > m: + raise RuntimeError(f"read {n} bytes, expected no more than {m} bytes") + r = self.buffer[:n] + del self.buffer[:n] + return r + + def read_exact(self, n: int) -> Generator[None, None, bytes]: + """ + Read a given number of bytes from the stream. + + This is a generator-based coroutine. + + Args: + n: how many bytes to read. + + Raises: + EOFError: if the stream ends in less than ``n`` bytes. + + """ + assert n >= 0 + while len(self.buffer) < n: + if self.eof: + p = len(self.buffer) + raise EOFError(f"stream ends after {p} bytes, expected {n} bytes") + yield + r = self.buffer[:n] + del self.buffer[:n] + return r + + def read_to_eof(self, m: int) -> Generator[None, None, bytes]: + """ + Read all bytes from the stream. + + This is a generator-based coroutine. + + Args: + m: maximum number bytes to read; this is a security limit. + + Raises: + RuntimeError: if the stream ends in more than ``m`` bytes. + + """ + while not self.eof: + p = len(self.buffer) + if p > m: + raise RuntimeError(f"read {p} bytes, expected no more than {m} bytes") + yield + r = self.buffer[:] + del self.buffer[:] + return r + + def at_eof(self) -> Generator[None, None, bool]: + """ + Tell whether the stream has ended and all data was read. + + This is a generator-based coroutine. + + """ + while True: + if self.buffer: + return False + if self.eof: + return True + # When all data was read but the stream hasn't ended, we can't + # tell if until either feed_data() or feed_eof() is called. + yield + + def feed_data(self, data: bytes) -> None: + """ + Write data to the stream. + + :meth:`feed_data` cannot be called after :meth:`feed_eof`. + + Args: + data: data to write. + + Raises: + EOFError: if the stream has ended. + + """ + if self.eof: + raise EOFError("stream ended") + self.buffer += data + + def feed_eof(self) -> None: + """ + End the stream. + + :meth:`feed_eof` cannot be called more than once. + + Raises: + EOFError: if the stream has ended. + + """ + if self.eof: + raise EOFError("stream ended") + self.eof = True + + def discard(self) -> None: + """ + Discard all buffered data, but don't end the stream. + + """ + del self.buffer[:] diff --git a/websockets/sync/__init__.py b/websockets/sync/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/websockets/sync/client.py b/websockets/sync/client.py new file mode 100644 index 0000000..087ff5f --- /dev/null +++ b/websockets/sync/client.py @@ -0,0 +1,328 @@ +from __future__ import annotations + +import socket +import ssl +import threading +from typing import Any, Optional, Sequence, Type + +from ..client import ClientProtocol +from ..datastructures import HeadersLike +from ..extensions.base import ClientExtensionFactory +from ..extensions.permessage_deflate import enable_client_permessage_deflate +from ..headers import validate_subprotocols +from ..http import USER_AGENT +from ..http11 import Response +from ..protocol import CONNECTING, OPEN, Event +from ..typing import LoggerLike, Origin, Subprotocol +from ..uri import parse_uri +from .connection import Connection +from .utils import Deadline + + +__all__ = ["connect", "unix_connect", "ClientConnection"] + + +class ClientConnection(Connection): + """ + Threaded implementation of a WebSocket client connection. + + :class:`ClientConnection` provides :meth:`recv` and :meth:`send` methods for + receiving and sending messages. + + It supports iteration to receive messages:: + + for message in websocket: + process(message) + + The iterator exits normally when the connection is closed with close code + 1000 (OK) or 1001 (going away) or without a close code. It raises a + :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is + closed with any other code. + + Args: + socket: Socket connected to a WebSocket server. + protocol: Sans-I/O connection. + close_timeout: Timeout for closing the connection in seconds. + + """ + + def __init__( + self, + socket: socket.socket, + protocol: ClientProtocol, + *, + close_timeout: Optional[float] = 10, + ) -> None: + self.protocol: ClientProtocol + self.response_rcvd = threading.Event() + super().__init__( + socket, + protocol, + close_timeout=close_timeout, + ) + + def handshake( + self, + additional_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, + timeout: Optional[float] = None, + ) -> None: + """ + Perform the opening handshake. + + """ + with self.send_context(expected_state=CONNECTING): + self.request = self.protocol.connect() + if additional_headers is not None: + self.request.headers.update(additional_headers) + if user_agent_header is not None: + self.request.headers["User-Agent"] = user_agent_header + self.protocol.send_request(self.request) + + if not self.response_rcvd.wait(timeout): + self.close_socket() + self.recv_events_thread.join() + raise TimeoutError("timed out during handshake") + + if self.response is None: + self.close_socket() + self.recv_events_thread.join() + raise ConnectionError("connection closed during handshake") + + if self.protocol.state is not OPEN: + self.recv_events_thread.join(self.close_timeout) + self.close_socket() + self.recv_events_thread.join() + + if self.protocol.handshake_exc is not None: + raise self.protocol.handshake_exc + + def process_event(self, event: Event) -> None: + """ + Process one incoming event. + + """ + # First event - handshake response. + if self.response is None: + assert isinstance(event, Response) + self.response = event + self.response_rcvd.set() + # Later events - frames. + else: + super().process_event(event) + + def recv_events(self) -> None: + """ + Read incoming data from the socket and process events. + + """ + try: + super().recv_events() + finally: + # If the connection is closed during the handshake, unblock it. + self.response_rcvd.set() + + +def connect( + uri: str, + *, + # TCP/TLS — unix and path are only for unix_connect() + sock: Optional[socket.socket] = None, + ssl_context: Optional[ssl.SSLContext] = None, + server_hostname: Optional[str] = None, + unix: bool = False, + path: Optional[str] = None, + # WebSocket + origin: Optional[Origin] = None, + extensions: Optional[Sequence[ClientExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + additional_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, + compression: Optional[str] = "deflate", + # Timeouts + open_timeout: Optional[float] = 10, + close_timeout: Optional[float] = 10, + # Limits + max_size: Optional[int] = 2**20, + # Logging + logger: Optional[LoggerLike] = None, + # Escape hatch for advanced customization + create_connection: Optional[Type[ClientConnection]] = None, +) -> ClientConnection: + """ + Connect to the WebSocket server at ``uri``. + + This function returns a :class:`ClientConnection` instance, which you can + use to send and receive messages. + + :func:`connect` may be used as a context manager:: + + async with websockets.sync.client.connect(...) as websocket: + ... + + The connection is closed automatically when exiting the context. + + Args: + uri: URI of the WebSocket server. + sock: Preexisting TCP socket. ``sock`` overrides the host and port + from ``uri``. You may call :func:`socket.create_connection` to + create a suitable TCP socket. + ssl_context: Configuration for enabling TLS on the connection. + server_hostname: Host name for the TLS handshake. ``server_hostname`` + overrides the host name from ``uri``. + origin: Value of the ``Origin`` header, for servers that require it. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing + preference. + additional_headers (HeadersLike | None): Arbitrary HTTP headers to add + to the handshake request. + user_agent_header: Value of the ``User-Agent`` request header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. + Setting it to :obj:`None` removes the header. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + open_timeout: Timeout for opening the connection in seconds. + :obj:`None` disables the timeout. + close_timeout: Timeout for closing the connection in seconds. + :obj:`None` disables the timeout. + max_size: Maximum size of incoming messages in bytes. + :obj:`None` disables the limit. + logger: Logger for this client. + It defaults to ``logging.getLogger("websockets.client")``. + See the :doc:`logging guide <../../topics/logging>` for details. + create_connection: Factory for the :class:`ClientConnection` managing + the connection. Set it to a wrapper or a subclass to customize + connection handling. + + Raises: + InvalidURI: If ``uri`` isn't a valid WebSocket URI. + OSError: If the TCP connection fails. + InvalidHandshake: If the opening handshake fails. + TimeoutError: If the opening handshake times out. + + """ + + # Process parameters + + wsuri = parse_uri(uri) + if not wsuri.secure and ssl_context is not None: + raise TypeError("ssl_context argument is incompatible with a ws:// URI") + + if unix: + if path is None and sock is None: + raise TypeError("missing path argument") + elif path is not None and sock is not None: + raise TypeError("path and sock arguments are incompatible") + else: + assert path is None # private argument, only set by unix_connect() + + if subprotocols is not None: + validate_subprotocols(subprotocols) + + if compression == "deflate": + extensions = enable_client_permessage_deflate(extensions) + elif compression is not None: + raise ValueError(f"unsupported compression: {compression}") + + # Calculate timeouts on the TCP, TLS, and WebSocket handshakes. + # The TCP and TLS timeouts must be set on the socket, then removed + # to avoid conflicting with the WebSocket timeout in handshake(). + deadline = Deadline(open_timeout) + + if create_connection is None: + create_connection = ClientConnection + + try: + # Connect socket + + if sock is None: + if unix: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(deadline.timeout()) + assert path is not None # validated above -- this is for mpypy + sock.connect(path) + else: + sock = socket.create_connection( + (wsuri.host, wsuri.port), + deadline.timeout(), + ) + sock.settimeout(None) + + # Disable Nagle algorithm + + if not unix: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) + + # Initialize TLS wrapper and perform TLS handshake + + if wsuri.secure: + if ssl_context is None: + ssl_context = ssl.create_default_context() + if server_hostname is None: + server_hostname = wsuri.host + sock.settimeout(deadline.timeout()) + sock = ssl_context.wrap_socket(sock, server_hostname=server_hostname) + sock.settimeout(None) + + # Initialize WebSocket connection + + protocol = ClientProtocol( + wsuri, + origin=origin, + extensions=extensions, + subprotocols=subprotocols, + state=CONNECTING, + max_size=max_size, + logger=logger, + ) + + # Initialize WebSocket protocol + + connection = create_connection( + sock, + protocol, + close_timeout=close_timeout, + ) + # On failure, handshake() closes the socket and raises an exception. + connection.handshake( + additional_headers, + user_agent_header, + deadline.timeout(), + ) + + except Exception: + if sock is not None: + sock.close() + raise + + return connection + + +def unix_connect( + path: Optional[str] = None, + uri: Optional[str] = None, + **kwargs: Any, +) -> ClientConnection: + """ + Connect to a WebSocket server listening on a Unix socket. + + This function is identical to :func:`connect`, except for the additional + ``path`` argument. It's only available on Unix. + + It's mainly useful for debugging servers listening on Unix sockets. + + Args: + path: File system path to the Unix socket. + uri: URI of the WebSocket server. ``uri`` defaults to + ``ws://localhost/`` or, when a ``ssl_context`` is provided, to + ``wss://localhost/``. + + """ + if uri is None: + if kwargs.get("ssl_context") is None: + uri = "ws://localhost/" + else: + uri = "wss://localhost/" + return connect(uri=uri, unix=True, path=path, **kwargs) diff --git a/websockets/sync/connection.py b/websockets/sync/connection.py new file mode 100644 index 0000000..4a8879e --- /dev/null +++ b/websockets/sync/connection.py @@ -0,0 +1,773 @@ +from __future__ import annotations + +import contextlib +import logging +import random +import socket +import struct +import threading +import uuid +from types import TracebackType +from typing import Any, Dict, Iterable, Iterator, Mapping, Optional, Type, Union + +from ..exceptions import ConnectionClosed, ConnectionClosedOK, ProtocolError +from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode, prepare_ctrl +from ..http11 import Request, Response +from ..protocol import CLOSED, OPEN, Event, Protocol, State +from ..typing import Data, LoggerLike, Subprotocol +from .messages import Assembler +from .utils import Deadline + + +__all__ = ["Connection"] + +logger = logging.getLogger(__name__) + + +class Connection: + """ + Threaded implementation of a WebSocket connection. + + :class:`Connection` provides APIs shared between WebSocket servers and + clients. + + You shouldn't use it directly. Instead, use + :class:`~websockets.sync.client.ClientConnection` or + :class:`~websockets.sync.server.ServerConnection`. + + """ + + recv_bufsize = 65536 + + def __init__( + self, + socket: socket.socket, + protocol: Protocol, + *, + close_timeout: Optional[float] = 10, + ) -> None: + self.socket = socket + self.protocol = protocol + self.close_timeout = close_timeout + + # Inject reference to this instance in the protocol's logger. + self.protocol.logger = logging.LoggerAdapter( + self.protocol.logger, + {"websocket": self}, + ) + + # Copy attributes from the protocol for convenience. + self.id: uuid.UUID = self.protocol.id + """Unique identifier of the connection. Useful in logs.""" + self.logger: LoggerLike = self.protocol.logger + """Logger for this connection.""" + self.debug = self.protocol.debug + + # HTTP handshake request and response. + self.request: Optional[Request] = None + """Opening handshake request.""" + self.response: Optional[Response] = None + """Opening handshake response.""" + + # Mutex serializing interactions with the protocol. + self.protocol_mutex = threading.Lock() + + # Assembler turning frames into messages and serializing reads. + self.recv_messages = Assembler() + + # Whether we are busy sending a fragmented message. + self.send_in_progress = False + + # Deadline for the closing handshake. + self.close_deadline: Optional[Deadline] = None + + # Mapping of ping IDs to pong waiters, in chronological order. + self.pings: Dict[bytes, threading.Event] = {} + + # Receiving events from the socket. + self.recv_events_thread = threading.Thread(target=self.recv_events) + self.recv_events_thread.start() + + # Exception raised in recv_events, to be chained to ConnectionClosed + # in the user thread in order to show why the TCP connection dropped. + self.recv_events_exc: Optional[BaseException] = None + + # Public attributes + + @property + def local_address(self) -> Any: + """ + Local address of the connection. + + For IPv4 connections, this is a ``(host, port)`` tuple. + + The format of the address depends on the address family. + See :meth:`~socket.socket.getsockname`. + + """ + return self.socket.getsockname() + + @property + def remote_address(self) -> Any: + """ + Remote address of the connection. + + For IPv4 connections, this is a ``(host, port)`` tuple. + + The format of the address depends on the address family. + See :meth:`~socket.socket.getpeername`. + + """ + return self.socket.getpeername() + + @property + def subprotocol(self) -> Optional[Subprotocol]: + """ + Subprotocol negotiated during the opening handshake. + + :obj:`None` if no subprotocol was negotiated. + + """ + return self.protocol.subprotocol + + # Public methods + + def __enter__(self) -> Connection: + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + if exc_type is None: + self.close() + else: + self.close(CloseCode.INTERNAL_ERROR) + + def __iter__(self) -> Iterator[Data]: + """ + Iterate on incoming messages. + + The iterator calls :meth:`recv` and yields messages in an infinite loop. + + It exits when the connection is closed normally. It raises a + :exc:`~websockets.exceptions.ConnectionClosedError` exception after a + protocol error or a network failure. + + """ + try: + while True: + yield self.recv() + except ConnectionClosedOK: + return + + def recv(self, timeout: Optional[float] = None) -> Data: + """ + Receive the next message. + + When the connection is closed, :meth:`recv` raises + :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises + :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal closure + and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol + error or a network failure. This is how you detect the end of the + message stream. + + If ``timeout`` is :obj:`None`, block until a message is received. If + ``timeout`` is set and no message is received within ``timeout`` + seconds, raise :exc:`TimeoutError`. Set ``timeout`` to ``0`` to check if + a message was already received. + + If the message is fragmented, wait until all fragments are received, + reassemble them, and return the whole message. + + Returns: + A string (:class:`str`) for a Text_ frame or a bytestring + (:class:`bytes`) for a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If two threads call :meth:`recv` or + :meth:`recv_streaming` concurrently. + + """ + try: + return self.recv_messages.get(timeout) + except EOFError: + raise self.protocol.close_exc from self.recv_events_exc + except RuntimeError: + raise RuntimeError( + "cannot call recv while another thread " + "is already running recv or recv_streaming" + ) from None + + def recv_streaming(self) -> Iterator[Data]: + """ + Receive the next message frame by frame. + + If the message is fragmented, yield each fragment as it is received. + The iterator must be fully consumed, or else the connection will become + unusable. + + :meth:`recv_streaming` raises the same exceptions as :meth:`recv`. + + Returns: + An iterator of strings (:class:`str`) for a Text_ frame or + bytestrings (:class:`bytes`) for a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If two threads call :meth:`recv` or + :meth:`recv_streaming` concurrently. + + """ + try: + yield from self.recv_messages.get_iter() + except EOFError: + raise self.protocol.close_exc from self.recv_events_exc + except RuntimeError: + raise RuntimeError( + "cannot call recv_streaming while another thread " + "is already running recv or recv_streaming" + ) from None + + def send(self, message: Union[Data, Iterable[Data]]) -> None: + """ + Send a message. + + A string (:class:`str`) is sent as a Text_ frame. A bytestring or + bytes-like object (:class:`bytes`, :class:`bytearray`, or + :class:`memoryview`) is sent as a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + :meth:`send` also accepts an iterable of strings, bytestrings, or + bytes-like objects to enable fragmentation_. Each item is treated as a + message fragment and sent in its own frame. All items must be of the + same type, or else :meth:`send` will raise a :exc:`TypeError` and the + connection will be closed. + + .. _fragmentation: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.4 + + :meth:`send` rejects dict-like objects because this is often an error. + (If you really want to send the keys of a dict-like object as fragments, + call its :meth:`~dict.keys` method and pass the result to :meth:`send`.) + + When the connection is closed, :meth:`send` raises + :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it + raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal + connection closure and + :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol + error or a network failure. + + Args: + message: Message to send. + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If a connection is busy sending a fragmented message. + TypeError: If ``message`` doesn't have a supported type. + + """ + # Unfragmented message -- this case must be handled first because + # strings and bytes-like objects are iterable. + + if isinstance(message, str): + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.protocol.send_text(message.encode("utf-8")) + + elif isinstance(message, BytesLike): + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.protocol.send_binary(message) + + # Catch a common mistake -- passing a dict to send(). + + elif isinstance(message, Mapping): + raise TypeError("data is a dict-like object") + + # Fragmented message -- regular iterator. + + elif isinstance(message, Iterable): + chunks = iter(message) + try: + chunk = next(chunks) + except StopIteration: + return + + try: + # First fragment. + if isinstance(chunk, str): + text = True + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.send_in_progress = True + self.protocol.send_text( + chunk.encode("utf-8"), + fin=False, + ) + elif isinstance(chunk, BytesLike): + text = False + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.send_in_progress = True + self.protocol.send_binary( + chunk, + fin=False, + ) + else: + raise TypeError("data iterable must contain bytes or str") + + # Other fragments + for chunk in chunks: + if isinstance(chunk, str) and text: + with self.send_context(): + assert self.send_in_progress + self.protocol.send_continuation( + chunk.encode("utf-8"), + fin=False, + ) + elif isinstance(chunk, BytesLike) and not text: + with self.send_context(): + assert self.send_in_progress + self.protocol.send_continuation( + chunk, + fin=False, + ) + else: + raise TypeError("data iterable must contain uniform types") + + # Final fragment. + with self.send_context(): + self.protocol.send_continuation(b"", fin=True) + self.send_in_progress = False + + except RuntimeError: + # We didn't start sending a fragmented message. + raise + + except Exception: + # We're half-way through a fragmented message and we can't + # complete it. This makes the connection unusable. + with self.send_context(): + self.protocol.fail( + CloseCode.INTERNAL_ERROR, + "error in fragmented message", + ) + raise + + else: + raise TypeError("data must be bytes, str, or iterable") + + def close(self, code: int = CloseCode.NORMAL_CLOSURE, reason: str = "") -> None: + """ + Perform the closing handshake. + + :meth:`close` waits for the other end to complete the handshake, for the + TCP connection to terminate, and for all incoming messages to be read + with :meth:`recv`. + + :meth:`close` is idempotent: it doesn't do anything once the + connection is closed. + + Args: + code: WebSocket close code. + reason: WebSocket close reason. + + """ + try: + # The context manager takes care of waiting for the TCP connection + # to terminate after calling a method that sends a close frame. + with self.send_context(): + if self.send_in_progress: + self.protocol.fail( + CloseCode.INTERNAL_ERROR, + "close during fragmented message", + ) + else: + self.protocol.send_close(code, reason) + except ConnectionClosed: + # Ignore ConnectionClosed exceptions raised from send_context(). + # They mean that the connection is closed, which was the goal. + pass + + def ping(self, data: Optional[Data] = None) -> threading.Event: + """ + Send a Ping_. + + .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 + + A ping may serve as a keepalive or as a check that the remote endpoint + received all messages up to this point + + Args: + data: Payload of the ping. A :class:`str` will be encoded to UTF-8. + If ``data`` is :obj:`None`, the payload is four random bytes. + + Returns: + An event that will be set when the corresponding pong is received. + You can ignore it if you don't intend to wait. + + :: + + pong_event = ws.ping() + pong_event.wait() # only if you want to wait for the pong + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If another ping was sent with the same data and + the corresponding pong wasn't received yet. + + """ + if data is not None: + data = prepare_ctrl(data) + + with self.send_context(): + # Protect against duplicates if a payload is explicitly set. + if data in self.pings: + raise RuntimeError("already waiting for a pong with the same data") + + # Generate a unique random payload otherwise. + while data is None or data in self.pings: + data = struct.pack("!I", random.getrandbits(32)) + + pong_waiter = threading.Event() + self.pings[data] = pong_waiter + self.protocol.send_ping(data) + return pong_waiter + + def pong(self, data: Data = b"") -> None: + """ + Send a Pong_. + + .. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + + An unsolicited pong may serve as a unidirectional heartbeat. + + Args: + data: Payload of the pong. A :class:`str` will be encoded to UTF-8. + + Raises: + ConnectionClosed: When the connection is closed. + + """ + data = prepare_ctrl(data) + + with self.send_context(): + self.protocol.send_pong(data) + + # Private methods + + def process_event(self, event: Event) -> None: + """ + Process one incoming event. + + This method is overridden in subclasses to handle the handshake. + + """ + assert isinstance(event, Frame) + if event.opcode in DATA_OPCODES: + self.recv_messages.put(event) + + if event.opcode is Opcode.PONG: + self.acknowledge_pings(bytes(event.data)) + + def acknowledge_pings(self, data: bytes) -> None: + """ + Acknowledge pings when receiving a pong. + + """ + with self.protocol_mutex: + # Ignore unsolicited pong. + if data not in self.pings: + return + # Sending a pong for only the most recent ping is legal. + # Acknowledge all previous pings too in that case. + ping_id = None + ping_ids = [] + for ping_id, ping in self.pings.items(): + ping_ids.append(ping_id) + ping.set() + if ping_id == data: + break + else: + raise AssertionError("solicited pong not found in pings") + # Remove acknowledged pings from self.pings. + for ping_id in ping_ids: + del self.pings[ping_id] + + def recv_events(self) -> None: + """ + Read incoming data from the socket and process events. + + Run this method in a thread as long as the connection is alive. + + ``recv_events()`` exits immediately when the ``self.socket`` is closed. + + """ + try: + while True: + try: + if self.close_deadline is not None: + self.socket.settimeout(self.close_deadline.timeout()) + data = self.socket.recv(self.recv_bufsize) + except Exception as exc: + if self.debug: + self.logger.debug("error while receiving data", exc_info=True) + # When the closing handshake is initiated by our side, + # recv() may block until send_context() closes the socket. + # In that case, send_context() already set recv_events_exc. + # Calling set_recv_events_exc() avoids overwriting it. + with self.protocol_mutex: + self.set_recv_events_exc(exc) + break + + if data == b"": + break + + # Acquire the connection lock. + with self.protocol_mutex: + # Feed incoming data to the connection. + self.protocol.receive_data(data) + + # This isn't expected to raise an exception. + events = self.protocol.events_received() + + # Write outgoing data to the socket. + try: + self.send_data() + except Exception as exc: + if self.debug: + self.logger.debug("error while sending data", exc_info=True) + # Similarly to the above, avoid overriding an exception + # set by send_context(), in case of a race condition + # i.e. send_context() closes the socket after recv() + # returns above but before send_data() calls send(). + self.set_recv_events_exc(exc) + break + + if self.protocol.close_expected(): + # If the connection is expected to close soon, set the + # close deadline based on the close timeout. + if self.close_deadline is None: + self.close_deadline = Deadline(self.close_timeout) + + # Unlock conn_mutex before processing events. Else, the + # application can't send messages in response to events. + + # If self.send_data raised an exception, then events are lost. + # Given that automatic responses write small amounts of data, + # this should be uncommon, so we don't handle the edge case. + + try: + for event in events: + # This may raise EOFError if the closing handshake + # times out while a message is waiting to be read. + self.process_event(event) + except EOFError: + break + + # Breaking out of the while True: ... loop means that we believe + # that the socket doesn't work anymore. + with self.protocol_mutex: + # Feed the end of the data stream to the connection. + self.protocol.receive_eof() + + # This isn't expected to generate events. + assert not self.protocol.events_received() + + # There is no error handling because send_data() can only write + # the end of the data stream here and it handles errors itself. + self.send_data() + + except Exception as exc: + # This branch should never run. It's a safety net in case of bugs. + self.logger.error("unexpected internal error", exc_info=True) + with self.protocol_mutex: + self.set_recv_events_exc(exc) + # We don't know where we crashed. Force protocol state to CLOSED. + self.protocol.state = CLOSED + finally: + # This isn't expected to raise an exception. + self.close_socket() + + @contextlib.contextmanager + def send_context( + self, + *, + expected_state: State = OPEN, # CONNECTING during the opening handshake + ) -> Iterator[None]: + """ + Create a context for writing to the connection from user code. + + On entry, :meth:`send_context` acquires the connection lock and checks + that the connection is open; on exit, it writes outgoing data to the + socket:: + + with self.send_context(): + self.protocol.send_text(message.encode("utf-8")) + + When the connection isn't open on entry, when the connection is expected + to close on exit, or when an unexpected error happens, terminating the + connection, :meth:`send_context` waits until the connection is closed + then raises :exc:`~websockets.exceptions.ConnectionClosed`. + + """ + # Should we wait until the connection is closed? + wait_for_close = False + # Should we close the socket and raise ConnectionClosed? + raise_close_exc = False + # What exception should we chain ConnectionClosed to? + original_exc: Optional[BaseException] = None + + # Acquire the protocol lock. + with self.protocol_mutex: + if self.protocol.state is expected_state: + # Let the caller interact with the protocol. + try: + yield + except (ProtocolError, RuntimeError): + # The protocol state wasn't changed. Exit immediately. + raise + except Exception as exc: + self.logger.error("unexpected internal error", exc_info=True) + # This branch should never run. It's a safety net in case of + # bugs. Since we don't know what happened, we will close the + # connection and raise the exception to the caller. + wait_for_close = False + raise_close_exc = True + original_exc = exc + else: + # Check if the connection is expected to close soon. + if self.protocol.close_expected(): + wait_for_close = True + # If the connection is expected to close soon, set the + # close deadline based on the close timeout. + + # Since we tested earlier that protocol.state was OPEN + # (or CONNECTING) and we didn't release protocol_mutex, + # it is certain that self.close_deadline is still None. + assert self.close_deadline is None + self.close_deadline = Deadline(self.close_timeout) + # Write outgoing data to the socket. + try: + self.send_data() + except Exception as exc: + if self.debug: + self.logger.debug("error while sending data", exc_info=True) + # While the only expected exception here is OSError, + # other exceptions would be treated identically. + wait_for_close = False + raise_close_exc = True + original_exc = exc + + else: # self.protocol.state is not expected_state + # Minor layering violation: we assume that the connection + # will be closing soon if it isn't in the expected state. + wait_for_close = True + raise_close_exc = True + + # To avoid a deadlock, release the connection lock by exiting the + # context manager before waiting for recv_events() to terminate. + + # If the connection is expected to close soon and the close timeout + # elapses, close the socket to terminate the connection. + if wait_for_close: + if self.close_deadline is None: + timeout = self.close_timeout + else: + # Thread.join() returns immediately if timeout is negative. + timeout = self.close_deadline.timeout(raise_if_elapsed=False) + self.recv_events_thread.join(timeout) + + if self.recv_events_thread.is_alive(): + # There's no risk to overwrite another error because + # original_exc is never set when wait_for_close is True. + assert original_exc is None + original_exc = TimeoutError("timed out while closing connection") + # Set recv_events_exc before closing the socket in order to get + # proper exception reporting. + raise_close_exc = True + with self.protocol_mutex: + self.set_recv_events_exc(original_exc) + + # If an error occurred, close the socket to terminate the connection and + # raise an exception. + if raise_close_exc: + self.close_socket() + self.recv_events_thread.join() + raise self.protocol.close_exc from original_exc + + def send_data(self) -> None: + """ + Send outgoing data. + + This method requires holding protocol_mutex. + + Raises: + OSError: When a socket operations fails. + + """ + assert self.protocol_mutex.locked() + for data in self.protocol.data_to_send(): + if data: + if self.close_deadline is not None: + self.socket.settimeout(self.close_deadline.timeout()) + self.socket.sendall(data) + else: + try: + self.socket.shutdown(socket.SHUT_WR) + except OSError: # socket already closed + pass + + def set_recv_events_exc(self, exc: Optional[BaseException]) -> None: + """ + Set recv_events_exc, if not set yet. + + This method requires holding protocol_mutex. + + """ + assert self.protocol_mutex.locked() + if self.recv_events_exc is None: + self.recv_events_exc = exc + + def close_socket(self) -> None: + """ + Shutdown and close socket. Close message assembler. + + Calling close_socket() guarantees that recv_events() terminates. Indeed, + recv_events() may block only on socket.recv() or on recv_messages.put(). + + """ + # shutdown() is required to interrupt recv() on Linux. + try: + self.socket.shutdown(socket.SHUT_RDWR) + except OSError: + pass # socket is already closed + self.socket.close() + self.recv_messages.close() diff --git a/websockets/sync/messages.py b/websockets/sync/messages.py new file mode 100644 index 0000000..67a2231 --- /dev/null +++ b/websockets/sync/messages.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +import codecs +import queue +import threading +from typing import Iterator, List, Optional, cast + +from ..frames import Frame, Opcode +from ..typing import Data + + +__all__ = ["Assembler"] + +UTF8Decoder = codecs.getincrementaldecoder("utf-8") + + +class Assembler: + """ + Assemble messages from frames. + + """ + + def __init__(self) -> None: + # Serialize reads and writes -- except for reads via synchronization + # primitives provided by the threading and queue modules. + self.mutex = threading.Lock() + + # We create a latch with two events to ensure proper interleaving of + # writing and reading messages. + # put() sets this event to tell get() that a message can be fetched. + self.message_complete = threading.Event() + # get() sets this event to let put() that the message was fetched. + self.message_fetched = threading.Event() + + # This flag prevents concurrent calls to get() by user code. + self.get_in_progress = False + # This flag prevents concurrent calls to put() by library code. + self.put_in_progress = False + + # Decoder for text frames, None for binary frames. + self.decoder: Optional[codecs.IncrementalDecoder] = None + + # Buffer of frames belonging to the same message. + self.chunks: List[Data] = [] + + # When switching from "buffering" to "streaming", we use a thread-safe + # queue for transferring frames from the writing thread (library code) + # to the reading thread (user code). We're buffering when chunks_queue + # is None and streaming when it's a SimpleQueue. None is a sentinel + # value marking the end of the stream, superseding message_complete. + + # Stream data from frames belonging to the same message. + # Remove quotes around type when dropping Python < 3.9. + self.chunks_queue: Optional["queue.SimpleQueue[Optional[Data]]"] = None + + # This flag marks the end of the stream. + self.closed = False + + def get(self, timeout: Optional[float] = None) -> Data: + """ + Read the next message. + + :meth:`get` returns a single :class:`str` or :class:`bytes`. + + If the message is fragmented, :meth:`get` waits until the last frame is + received, then it reassembles the message and returns it. To receive + messages frame by frame, use :meth:`get_iter` instead. + + Args: + timeout: If a timeout is provided and elapses before a complete + message is received, :meth:`get` raises :exc:`TimeoutError`. + + Raises: + EOFError: If the stream of frames has ended. + RuntimeError: If two threads run :meth:`get` or :meth:``get_iter` + concurrently. + + """ + with self.mutex: + if self.closed: + raise EOFError("stream of frames ended") + + if self.get_in_progress: + raise RuntimeError("get or get_iter is already running") + + self.get_in_progress = True + + # If the message_complete event isn't set yet, release the lock to + # allow put() to run and eventually set it. + # Locking with get_in_progress ensures only one thread can get here. + completed = self.message_complete.wait(timeout) + + with self.mutex: + self.get_in_progress = False + + # Waiting for a complete message timed out. + if not completed: + raise TimeoutError(f"timed out in {timeout:.1f}s") + + # get() was unblocked by close() rather than put(). + if self.closed: + raise EOFError("stream of frames ended") + + assert self.message_complete.is_set() + self.message_complete.clear() + + joiner: Data = b"" if self.decoder is None else "" + # mypy cannot figure out that chunks have the proper type. + message: Data = joiner.join(self.chunks) # type: ignore + + assert not self.message_fetched.is_set() + self.message_fetched.set() + + self.chunks = [] + assert self.chunks_queue is None + + return message + + def get_iter(self) -> Iterator[Data]: + """ + Stream the next message. + + Iterating the return value of :meth:`get_iter` yields a :class:`str` or + :class:`bytes` for each frame in the message. + + The iterator must be fully consumed before calling :meth:`get_iter` or + :meth:`get` again. Else, :exc:`RuntimeError` is raised. + + This method only makes sense for fragmented messages. If messages aren't + fragmented, use :meth:`get` instead. + + Raises: + EOFError: If the stream of frames has ended. + RuntimeError: If two threads run :meth:`get` or :meth:``get_iter` + concurrently. + + """ + with self.mutex: + if self.closed: + raise EOFError("stream of frames ended") + + if self.get_in_progress: + raise RuntimeError("get or get_iter is already running") + + chunks = self.chunks + self.chunks = [] + self.chunks_queue = cast( + # Remove quotes around type when dropping Python < 3.9. + "queue.SimpleQueue[Optional[Data]]", + queue.SimpleQueue(), + ) + + # Sending None in chunk_queue supersedes setting message_complete + # when switching to "streaming". If message is already complete + # when the switch happens, put() didn't send None, so we have to. + if self.message_complete.is_set(): + self.chunks_queue.put(None) + + self.get_in_progress = True + + # Locking with get_in_progress ensures only one thread can get here. + yield from chunks + while True: + chunk = self.chunks_queue.get() + if chunk is None: + break + yield chunk + + with self.mutex: + self.get_in_progress = False + + assert self.message_complete.is_set() + self.message_complete.clear() + + # get_iter() was unblocked by close() rather than put(). + if self.closed: + raise EOFError("stream of frames ended") + + assert not self.message_fetched.is_set() + self.message_fetched.set() + + assert self.chunks == [] + self.chunks_queue = None + + def put(self, frame: Frame) -> None: + """ + Add ``frame`` to the next message. + + When ``frame`` is the final frame in a message, :meth:`put` waits until + the message is fetched, either by calling :meth:`get` or by fully + consuming the return value of :meth:`get_iter`. + + :meth:`put` assumes that the stream of frames respects the protocol. If + it doesn't, the behavior is undefined. + + Raises: + EOFError: If the stream of frames has ended. + RuntimeError: If two threads run :meth:`put` concurrently. + + """ + with self.mutex: + if self.closed: + raise EOFError("stream of frames ended") + + if self.put_in_progress: + raise RuntimeError("put is already running") + + if frame.opcode is Opcode.TEXT: + self.decoder = UTF8Decoder(errors="strict") + elif frame.opcode is Opcode.BINARY: + self.decoder = None + elif frame.opcode is Opcode.CONT: + pass + else: + # Ignore control frames. + return + + data: Data + if self.decoder is not None: + data = self.decoder.decode(frame.data, frame.fin) + else: + data = frame.data + + if self.chunks_queue is None: + self.chunks.append(data) + else: + self.chunks_queue.put(data) + + if not frame.fin: + return + + # Message is complete. Wait until it's fetched to return. + + assert not self.message_complete.is_set() + self.message_complete.set() + + if self.chunks_queue is not None: + self.chunks_queue.put(None) + + assert not self.message_fetched.is_set() + + self.put_in_progress = True + + # Release the lock to allow get() to run and eventually set the event. + self.message_fetched.wait() + + with self.mutex: + self.put_in_progress = False + + assert self.message_fetched.is_set() + self.message_fetched.clear() + + # put() was unblocked by close() rather than get() or get_iter(). + if self.closed: + raise EOFError("stream of frames ended") + + self.decoder = None + + def close(self) -> None: + """ + End the stream of frames. + + Callling :meth:`close` concurrently with :meth:`get`, :meth:`get_iter`, + or :meth:`put` is safe. They will raise :exc:`EOFError`. + + """ + with self.mutex: + if self.closed: + return + + self.closed = True + + # Unblock get or get_iter. + if self.get_in_progress: + self.message_complete.set() + if self.chunks_queue is not None: + self.chunks_queue.put(None) + + # Unblock put(). + if self.put_in_progress: + self.message_fetched.set() diff --git a/websockets/sync/server.py b/websockets/sync/server.py new file mode 100644 index 0000000..1476796 --- /dev/null +++ b/websockets/sync/server.py @@ -0,0 +1,530 @@ +from __future__ import annotations + +import http +import logging +import os +import selectors +import socket +import ssl +import sys +import threading +from types import TracebackType +from typing import Any, Callable, Optional, Sequence, Type + +from websockets.frames import CloseCode + +from ..extensions.base import ServerExtensionFactory +from ..extensions.permessage_deflate import enable_server_permessage_deflate +from ..headers import validate_subprotocols +from ..http import USER_AGENT +from ..http11 import Request, Response +from ..protocol import CONNECTING, OPEN, Event +from ..server import ServerProtocol +from ..typing import LoggerLike, Origin, Subprotocol +from .connection import Connection +from .utils import Deadline + + +__all__ = ["serve", "unix_serve", "ServerConnection", "WebSocketServer"] + + +class ServerConnection(Connection): + """ + Threaded implementation of a WebSocket server connection. + + :class:`ServerConnection` provides :meth:`recv` and :meth:`send` methods for + receiving and sending messages. + + It supports iteration to receive messages:: + + for message in websocket: + process(message) + + The iterator exits normally when the connection is closed with close code + 1000 (OK) or 1001 (going away) or without a close code. It raises a + :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is + closed with any other code. + + Args: + socket: Socket connected to a WebSocket client. + protocol: Sans-I/O connection. + close_timeout: Timeout for closing the connection in seconds. + + """ + + def __init__( + self, + socket: socket.socket, + protocol: ServerProtocol, + *, + close_timeout: Optional[float] = 10, + ) -> None: + self.protocol: ServerProtocol + self.request_rcvd = threading.Event() + super().__init__( + socket, + protocol, + close_timeout=close_timeout, + ) + + def handshake( + self, + process_request: Optional[ + Callable[ + [ServerConnection, Request], + Optional[Response], + ] + ] = None, + process_response: Optional[ + Callable[ + [ServerConnection, Request, Response], + Optional[Response], + ] + ] = None, + server_header: Optional[str] = USER_AGENT, + timeout: Optional[float] = None, + ) -> None: + """ + Perform the opening handshake. + + """ + if not self.request_rcvd.wait(timeout): + self.close_socket() + self.recv_events_thread.join() + raise TimeoutError("timed out during handshake") + + if self.request is None: + self.close_socket() + self.recv_events_thread.join() + raise ConnectionError("connection closed during handshake") + + with self.send_context(expected_state=CONNECTING): + self.response = None + + if process_request is not None: + try: + self.response = process_request(self, self.request) + except Exception as exc: + self.protocol.handshake_exc = exc + self.logger.error("opening handshake failed", exc_info=True) + self.response = self.protocol.reject( + http.HTTPStatus.INTERNAL_SERVER_ERROR, + ( + "Failed to open a WebSocket connection.\n" + "See server log for more information.\n" + ), + ) + + if self.response is None: + self.response = self.protocol.accept(self.request) + + if server_header is not None: + self.response.headers["Server"] = server_header + + if process_response is not None: + try: + response = process_response(self, self.request, self.response) + except Exception as exc: + self.protocol.handshake_exc = exc + self.logger.error("opening handshake failed", exc_info=True) + self.response = self.protocol.reject( + http.HTTPStatus.INTERNAL_SERVER_ERROR, + ( + "Failed to open a WebSocket connection.\n" + "See server log for more information.\n" + ), + ) + else: + if response is not None: + self.response = response + + self.protocol.send_response(self.response) + + if self.protocol.state is not OPEN: + self.recv_events_thread.join(self.close_timeout) + self.close_socket() + self.recv_events_thread.join() + + if self.protocol.handshake_exc is not None: + raise self.protocol.handshake_exc + + def process_event(self, event: Event) -> None: + """ + Process one incoming event. + + """ + # First event - handshake request. + if self.request is None: + assert isinstance(event, Request) + self.request = event + self.request_rcvd.set() + # Later events - frames. + else: + super().process_event(event) + + def recv_events(self) -> None: + """ + Read incoming data from the socket and process events. + + """ + try: + super().recv_events() + finally: + # If the connection is closed during the handshake, unblock it. + self.request_rcvd.set() + + +class WebSocketServer: + """ + WebSocket server returned by :func:`serve`. + + This class mirrors the API of :class:`~socketserver.BaseServer`, notably the + :meth:`~socketserver.BaseServer.serve_forever` and + :meth:`~socketserver.BaseServer.shutdown` methods, as well as the context + manager protocol. + + Args: + socket: Server socket listening for new connections. + handler: Handler for one connection. Receives the socket and address + returned by :meth:`~socket.socket.accept`. + logger: Logger for this server. + + """ + + def __init__( + self, + socket: socket.socket, + handler: Callable[[socket.socket, Any], None], + logger: Optional[LoggerLike] = None, + ): + self.socket = socket + self.handler = handler + if logger is None: + logger = logging.getLogger("websockets.server") + self.logger = logger + if sys.platform != "win32": + self.shutdown_watcher, self.shutdown_notifier = os.pipe() + + def serve_forever(self) -> None: + """ + See :meth:`socketserver.BaseServer.serve_forever`. + + This method doesn't return. Calling :meth:`shutdown` from another thread + stops the server. + + Typical use:: + + with serve(...) as server: + server.serve_forever() + + """ + poller = selectors.DefaultSelector() + poller.register(self.socket, selectors.EVENT_READ) + if sys.platform != "win32": + poller.register(self.shutdown_watcher, selectors.EVENT_READ) + + while True: + poller.select() + try: + # If the socket is closed, this will raise an exception and exit + # the loop. So we don't need to check the return value of select(). + sock, addr = self.socket.accept() + except OSError: + break + thread = threading.Thread(target=self.handler, args=(sock, addr)) + thread.start() + + def shutdown(self) -> None: + """ + See :meth:`socketserver.BaseServer.shutdown`. + + """ + self.socket.close() + if sys.platform != "win32": + os.write(self.shutdown_notifier, b"x") + + def fileno(self) -> int: + """ + See :meth:`socketserver.BaseServer.fileno`. + + """ + return self.socket.fileno() + + def __enter__(self) -> WebSocketServer: + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + self.shutdown() + + +def serve( + handler: Callable[[ServerConnection], None], + host: Optional[str] = None, + port: Optional[int] = None, + *, + # TCP/TLS — unix and path are only for unix_serve() + sock: Optional[socket.socket] = None, + ssl_context: Optional[ssl.SSLContext] = None, + unix: bool = False, + path: Optional[str] = None, + # WebSocket + origins: Optional[Sequence[Optional[Origin]]] = None, + extensions: Optional[Sequence[ServerExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + select_subprotocol: Optional[ + Callable[ + [ServerConnection, Sequence[Subprotocol]], + Optional[Subprotocol], + ] + ] = None, + process_request: Optional[ + Callable[ + [ServerConnection, Request], + Optional[Response], + ] + ] = None, + process_response: Optional[ + Callable[ + [ServerConnection, Request, Response], + Optional[Response], + ] + ] = None, + server_header: Optional[str] = USER_AGENT, + compression: Optional[str] = "deflate", + # Timeouts + open_timeout: Optional[float] = 10, + close_timeout: Optional[float] = 10, + # Limits + max_size: Optional[int] = 2**20, + # Logging + logger: Optional[LoggerLike] = None, + # Escape hatch for advanced customization + create_connection: Optional[Type[ServerConnection]] = None, +) -> WebSocketServer: + """ + Create a WebSocket server listening on ``host`` and ``port``. + + Whenever a client connects, the server creates a :class:`ServerConnection`, + performs the opening handshake, and delegates to the ``handler``. + + The handler receives a :class:`ServerConnection` instance, which you can use + to send and receive messages. + + Once the handler completes, either normally or with an exception, the server + performs the closing handshake and closes the connection. + + :class:`WebSocketServer` mirrors the API of + :class:`~socketserver.BaseServer`. Treat it as a context manager to ensure + that it will be closed and call the :meth:`~WebSocketServer.serve_forever` + method to serve requests:: + + def handler(websocket): + ... + + with websockets.sync.server.serve(handler, ...) as server: + server.serve_forever() + + Args: + handler: Connection handler. It receives the WebSocket connection, + which is a :class:`ServerConnection`, in argument. + host: Network interfaces the server binds to. + See :func:`~socket.create_server` for details. + port: TCP port the server listens on. + See :func:`~socket.create_server` for details. + sock: Preexisting TCP socket. ``sock`` replaces ``host`` and ``port``. + You may call :func:`socket.create_server` to create a suitable TCP + socket. + ssl_context: Configuration for enabling TLS on the connection. + origins: Acceptable values of the ``Origin`` header, for defending + against Cross-Site WebSocket Hijacking attacks. Include :obj:`None` + in the list if the lack of an origin is acceptable. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing + preference. + select_subprotocol: Callback for selecting a subprotocol among + those supported by the client and the server. It receives a + :class:`ServerConnection` (not a + :class:`~websockets.server.ServerProtocol`!) instance and a list of + subprotocols offered by the client. Other than the first argument, + it has the same behavior as the + :meth:`ServerProtocol.select_subprotocol + ` method. + process_request: Intercept the request during the opening handshake. + Return an HTTP response to force the response or :obj:`None` to + continue normally. When you force an HTTP 101 Continue response, + the handshake is successful. Else, the connection is aborted. + process_response: Intercept the response during the opening handshake. + Return an HTTP response to force the response or :obj:`None` to + continue normally. When you force an HTTP 101 Continue response, + the handshake is successful. Else, the connection is aborted. + server_header: Value of the ``Server`` response header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. Setting it to + :obj:`None` removes the header. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + open_timeout: Timeout for opening connections in seconds. + :obj:`None` disables the timeout. + close_timeout: Timeout for closing connections in seconds. + :obj:`None` disables the timeout. + max_size: Maximum size of incoming messages in bytes. + :obj:`None` disables the limit. + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.server")``. See the + :doc:`logging guide <../../topics/logging>` for details. + create_connection: Factory for the :class:`ServerConnection` managing + the connection. Set it to a wrapper or a subclass to customize + connection handling. + """ + + # Process parameters + + if subprotocols is not None: + validate_subprotocols(subprotocols) + + if compression == "deflate": + extensions = enable_server_permessage_deflate(extensions) + elif compression is not None: + raise ValueError(f"unsupported compression: {compression}") + + if create_connection is None: + create_connection = ServerConnection + + # Bind socket and listen + + if sock is None: + if unix: + if path is None: + raise TypeError("missing path argument") + sock = socket.create_server(path, family=socket.AF_UNIX) + else: + sock = socket.create_server((host, port)) + else: + if path is not None: + raise TypeError("path and sock arguments are incompatible") + + # Initialize TLS wrapper + + if ssl_context is not None: + sock = ssl_context.wrap_socket( + sock, + server_side=True, + # Delay TLS handshake until after we set a timeout on the socket. + do_handshake_on_connect=False, + ) + + # Define request handler + + def conn_handler(sock: socket.socket, addr: Any) -> None: + # Calculate timeouts on the TLS and WebSocket handshakes. + # The TLS timeout must be set on the socket, then removed + # to avoid conflicting with the WebSocket timeout in handshake(). + deadline = Deadline(open_timeout) + + try: + # Disable Nagle algorithm + + if not unix: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) + + # Perform TLS handshake + + if ssl_context is not None: + sock.settimeout(deadline.timeout()) + assert isinstance(sock, ssl.SSLSocket) # mypy cannot figure this out + sock.do_handshake() + sock.settimeout(None) + + # Create a closure so that select_subprotocol has access to self. + + protocol_select_subprotocol: Optional[ + Callable[ + [ServerProtocol, Sequence[Subprotocol]], + Optional[Subprotocol], + ] + ] = None + + if select_subprotocol is not None: + + def protocol_select_subprotocol( + protocol: ServerProtocol, + subprotocols: Sequence[Subprotocol], + ) -> Optional[Subprotocol]: + # mypy doesn't know that select_subprotocol is immutable. + assert select_subprotocol is not None + # Ensure this function is only used in the intended context. + assert protocol is connection.protocol + return select_subprotocol(connection, subprotocols) + + # Initialize WebSocket connection + + protocol = ServerProtocol( + origins=origins, + extensions=extensions, + subprotocols=subprotocols, + select_subprotocol=protocol_select_subprotocol, + state=CONNECTING, + max_size=max_size, + logger=logger, + ) + + # Initialize WebSocket protocol + + assert create_connection is not None # help mypy + connection = create_connection( + sock, + protocol, + close_timeout=close_timeout, + ) + # On failure, handshake() closes the socket, raises an exception, and + # logs it. + connection.handshake( + process_request, + process_response, + server_header, + deadline.timeout(), + ) + + except Exception: + sock.close() + return + + try: + handler(connection) + except Exception: + protocol.logger.error("connection handler failed", exc_info=True) + connection.close(CloseCode.INTERNAL_ERROR) + else: + connection.close() + + # Initialize server + + return WebSocketServer(sock, conn_handler, logger) + + +def unix_serve( + handler: Callable[[ServerConnection], Any], + path: Optional[str] = None, + **kwargs: Any, +) -> WebSocketServer: + """ + Create a WebSocket server listening on a Unix socket. + + This function is identical to :func:`serve`, except the ``host`` and + ``port`` arguments are replaced by ``path``. It's only available on Unix. + + It's useful for deploying a server behind a reverse proxy such as nginx. + + Args: + handler: Connection handler. It receives the WebSocket connection, + which is a :class:`ServerConnection`, in argument. + path: File system path to the Unix socket. + + """ + return serve(handler, path=path, unix=True, **kwargs) diff --git a/websockets/sync/utils.py b/websockets/sync/utils.py new file mode 100644 index 0000000..471f32e --- /dev/null +++ b/websockets/sync/utils.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import time +from typing import Optional + + +__all__ = ["Deadline"] + + +class Deadline: + """ + Manage timeouts across multiple steps. + + Args: + timeout: Time available in seconds or :obj:`None` if there is no limit. + + """ + + def __init__(self, timeout: Optional[float]) -> None: + self.deadline: Optional[float] + if timeout is None: + self.deadline = None + else: + self.deadline = time.monotonic() + timeout + + def timeout(self, *, raise_if_elapsed: bool = True) -> Optional[float]: + """ + Calculate a timeout from a deadline. + + Args: + raise_if_elapsed (bool): Whether to raise :exc:`TimeoutError` + if the deadline lapsed. + + Raises: + TimeoutError: If the deadline lapsed. + + Returns: + Time left in seconds or :obj:`None` if there is no limit. + + """ + if self.deadline is None: + return None + timeout = self.deadline - time.monotonic() + if raise_if_elapsed and timeout <= 0: + raise TimeoutError("timed out") + return timeout diff --git a/websockets/typing.py b/websockets/typing.py new file mode 100644 index 0000000..e672ba0 --- /dev/null +++ b/websockets/typing.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import logging +from typing import List, NewType, Optional, Tuple, Union + + +__all__ = [ + "Data", + "LoggerLike", + "Origin", + "Subprotocol", + "ExtensionName", + "ExtensionParameter", +] + + +# Public types used in the signature of public APIs + +Data = Union[str, bytes] +"""Types supported in a WebSocket message: +:class:`str` for a Text_ frame, :class:`bytes` for a Binary_. + +.. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 +.. _Binary : https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + +""" + + +LoggerLike = Union[logging.Logger, logging.LoggerAdapter] +"""Types accepted where a :class:`~logging.Logger` is expected.""" + + +Origin = NewType("Origin", str) +"""Value of a ``Origin`` header.""" + + +Subprotocol = NewType("Subprotocol", str) +"""Subprotocol in a ``Sec-WebSocket-Protocol`` header.""" + + +ExtensionName = NewType("ExtensionName", str) +"""Name of a WebSocket extension.""" + + +ExtensionParameter = Tuple[str, Optional[str]] +"""Parameter of a WebSocket extension.""" + + +# Private types + +ExtensionHeader = Tuple[ExtensionName, List[ExtensionParameter]] +"""Extension in a ``Sec-WebSocket-Extensions`` header.""" + + +ConnectionOption = NewType("ConnectionOption", str) +"""Connection option in a ``Connection`` header.""" + + +UpgradeProtocol = NewType("UpgradeProtocol", str) +"""Upgrade protocol in an ``Upgrade`` header.""" diff --git a/websockets/uri.py b/websockets/uri.py new file mode 100644 index 0000000..385090f --- /dev/null +++ b/websockets/uri.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import dataclasses +import urllib.parse +from typing import Optional, Tuple + +from . import exceptions + + +__all__ = ["parse_uri", "WebSocketURI"] + + +@dataclasses.dataclass +class WebSocketURI: + """ + WebSocket URI. + + Attributes: + secure: :obj:`True` for a ``wss`` URI, :obj:`False` for a ``ws`` URI. + host: Normalized to lower case. + port: Always set even if it's the default. + path: May be empty. + query: May be empty if the URI doesn't include a query component. + username: Available when the URI contains `User Information`_. + password: Available when the URI contains `User Information`_. + + .. _User Information: https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.1 + + """ + + secure: bool + host: str + port: int + path: str + query: str + username: Optional[str] = None + password: Optional[str] = None + + @property + def resource_name(self) -> str: + if self.path: + resource_name = self.path + else: + resource_name = "/" + if self.query: + resource_name += "?" + self.query + return resource_name + + @property + def user_info(self) -> Optional[Tuple[str, str]]: + if self.username is None: + return None + assert self.password is not None + return (self.username, self.password) + + +# All characters from the gen-delims and sub-delims sets in RFC 3987. +DELIMS = ":/?#[]@!$&'()*+,;=" + + +def parse_uri(uri: str) -> WebSocketURI: + """ + Parse and validate a WebSocket URI. + + Args: + uri: WebSocket URI. + + Returns: + WebSocketURI: Parsed WebSocket URI. + + Raises: + InvalidURI: if ``uri`` isn't a valid WebSocket URI. + + """ + parsed = urllib.parse.urlparse(uri) + if parsed.scheme not in ["ws", "wss"]: + raise exceptions.InvalidURI(uri, "scheme isn't ws or wss") + if parsed.hostname is None: + raise exceptions.InvalidURI(uri, "hostname isn't provided") + if parsed.fragment != "": + raise exceptions.InvalidURI(uri, "fragment identifier is meaningless") + + secure = parsed.scheme == "wss" + host = parsed.hostname + port = parsed.port or (443 if secure else 80) + path = parsed.path + query = parsed.query + username = parsed.username + password = parsed.password + # urllib.parse.urlparse accepts URLs with a username but without a + # password. This doesn't make sense for HTTP Basic Auth credentials. + if username is not None and password is None: + raise exceptions.InvalidURI(uri, "username provided without password") + + try: + uri.encode("ascii") + except UnicodeEncodeError: + # Input contains non-ASCII characters. + # It must be an IRI. Convert it to a URI. + host = host.encode("idna").decode() + path = urllib.parse.quote(path, safe=DELIMS) + query = urllib.parse.quote(query, safe=DELIMS) + if username is not None: + assert password is not None + username = urllib.parse.quote(username, safe=DELIMS) + password = urllib.parse.quote(password, safe=DELIMS) + + return WebSocketURI(secure, host, port, path, query, username, password) diff --git a/websockets/utils.py b/websockets/utils.py new file mode 100644 index 0000000..c404049 --- /dev/null +++ b/websockets/utils.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import base64 +import hashlib +import secrets +import sys + + +__all__ = ["accept_key", "apply_mask"] + + +GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + +def generate_key() -> str: + """ + Generate a random key for the Sec-WebSocket-Key header. + + """ + key = secrets.token_bytes(16) + return base64.b64encode(key).decode() + + +def accept_key(key: str) -> str: + """ + Compute the value of the Sec-WebSocket-Accept header. + + Args: + key: value of the Sec-WebSocket-Key header. + + """ + sha1 = hashlib.sha1((key + GUID).encode()).digest() + return base64.b64encode(sha1).decode() + + +def apply_mask(data: bytes, mask: bytes) -> bytes: + """ + Apply masking to the data of a WebSocket message. + + Args: + data: data to mask. + mask: 4-bytes mask. + + """ + if len(mask) != 4: + raise ValueError("mask must contain 4 bytes") + + data_int = int.from_bytes(data, sys.byteorder) + mask_repeated = mask * (len(data) // 4) + mask[: len(data) % 4] + mask_int = int.from_bytes(mask_repeated, sys.byteorder) + return (data_int ^ mask_int).to_bytes(len(data), sys.byteorder) diff --git a/websockets/version.py b/websockets/version.py new file mode 100644 index 0000000..3f171b3 --- /dev/null +++ b/websockets/version.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import importlib.metadata + + +__all__ = ["tag", "version", "commit"] + + +# ========= =========== =================== +# release development +# ========= =========== =================== +# tag X.Y X.Y (upcoming) +# version X.Y X.Y.dev1+g5678cde +# commit X.Y 5678cde +# ========= =========== =================== + + +# When tagging a release, set `released = True`. +# After tagging a release, set `released = False` and increment `tag`. + +released = False + +tag = version = commit = "12.0" + + +if not released: # pragma: no cover + import pathlib + import re + import subprocess + + def get_version(tag: str) -> str: + # Since setup.py executes the contents of src/websockets/version.py, + # __file__ can point to either of these two files. + file_path = pathlib.Path(__file__) + root_dir = file_path.parents[0 if file_path.name == "setup.py" else 2] + + # Read version from git if available. This prevents reading stale + # information from src/websockets.egg-info after building a sdist. + try: + description = subprocess.run( + ["git", "describe", "--dirty", "--tags", "--long"], + capture_output=True, + cwd=root_dir, + timeout=1, + check=True, + text=True, + ).stdout.strip() + # subprocess.run raises FileNotFoundError if git isn't on $PATH. + except ( + FileNotFoundError, + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + ): + pass + else: + description_re = r"[0-9.]+-([0-9]+)-(g[0-9a-f]{7,}(?:-dirty)?)" + match = re.fullmatch(description_re, description) + assert match is not None + distance, remainder = match.groups() + remainder = remainder.replace("-", ".") # required by PEP 440 + return f"{tag}.dev{distance}+{remainder}" + + # Read version from package metadata if it is installed. + try: + return importlib.metadata.version("websockets") + except ImportError: + pass + + # Avoid crashing if the development version cannot be determined. + return f"{tag}.dev0+gunknown" + + version = get_version(tag) + + def get_commit(tag: str, version: str) -> str: + # Extract commit from version, falling back to tag if not available. + version_re = r"[0-9.]+\.dev[0-9]+\+g([0-9a-f]{7,}|unknown)(?:\.dirty)?" + match = re.fullmatch(version_re, version) + assert match is not None + (commit,) = match.groups() + return tag if commit == "unknown" else commit + + commit = get_commit(tag, version) diff --git a/wecker.py b/wecker.py new file mode 100644 index 0000000..b350640 --- /dev/null +++ b/wecker.py @@ -0,0 +1,129 @@ +import sys +import mysql.connector as pymysql +from mysql.connector import connect, Error +import requests +import datetime +import time +import socket +import traceback +import konfig + +# MySQL-Datenbankverbindung. Zugangsdaten stehen in config.ini, siehe konfig.py. +DB_CONFIG = konfig.datenbank("alarm") + +wled_url = "http://192.168.179.139/json" # URL zur WLED JSON API +LOG_SERVER = ("localhost", 13377) # UDP Logserver + +# UDP Logging Funktion +def send_log(message): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + log_message = f"{datetime.datetime.now().strftime('%b %d %H:%M:%S')} alarm_scheduler: {message}" + sock.sendto(log_message.encode(), LOG_SERVER) + sock.close() + +def is_holiday(): + today_str = datetime.datetime.today().strftime("%Y-%m-%d") + + conn = pymysql.connect(**DB_CONFIG) + cursor = conn.cursor() + + query = "SELECT COUNT(*) FROM feiertage WHERE datetime = %s" + cursor.execute(query, (today_str,)) + result = cursor.fetchone() + conn.close() + + return result[0] > 0 + +def is_school_holiday(): + + today_str = datetime.datetime.today().strftime("%Y-%m-%d") + + conn = pymysql.connect(**DB_CONFIG) + cursor = conn.cursor() + + query = "SELECT COUNT(*) FROM ferien WHERE date_from <= %s AND date_to >= %s" + cursor.execute(query, (today_str, today_str)) + result = cursor.fetchone() + conn.close() + + return result[0] > 0 + +def get_active_alarms(): + today = datetime.datetime.today() + today_str = today.strftime("%Y-%m-%d") + weekday_map = {0: "mo", 1: "di", 2: "mi", 3: "do", 4: "fr", 5: "sa", 6: "so"} + weekday_col = weekday_map[today.weekday()] + holiday = is_holiday() + school_holiday = is_school_holiday() + + conn = pymysql.connect(**DB_CONFIG) + cursor = conn.cursor() + + query = f""" + SELECT id, TIME_FORMAT(time_1, '%H:%i:%s'), mode FROM alarmtime + WHERE onoff != -1 AND {weekday_col} != -1 AND (wecked IS NULL OR wecked != '{today_str}') + """ + + if holiday: + query += " AND mode NOT IN ('arbeit', 'schule')" + elif school_holiday: + query += " AND mode != 'schule'" + + cursor.execute(query) + alarms = cursor.fetchall() + conn.close() + + return [(alarm[0], alarm[1]) for alarm in alarms] + +def trigger_wled_playlist(): + data = {"ps": 5} # Beispiel: Playlist 1 aktivieren + headers = {"Content-Type": "application/json"} + + for attempt in range(5): + try: + response = requests.post(wled_url, json=data, headers=headers) + send_log(f"WLED Response (Attempt {attempt + 1}): {response.text}") + try: + if response.json().get("success"): + return + except ValueError: + send_log(f"Value error: {response.text}") # Falls die Antwort kein gültiges JSON ist, weitermachen + except: + send_log(f"No response (Attempt {attempt + 1}): {traceback.print_exc()}") + time.sleep(4) # Kurze Wartezeit zwischen Versuchen + +def mark_alarm_as_executed(alarm_id): + today_str = datetime.datetime.today().strftime("%Y-%m-%d") + conn = pymysql.connect(**DB_CONFIG) + cursor = conn.cursor() + + query = "UPDATE alarmtime SET wecked = %s WHERE id = %s" + cursor.execute(query, (today_str, alarm_id)) + conn.commit() + conn.close() + +def main(): + last_db_check = 0 + active_alarms = [] + + while True: + now = time.time() + + # Nur alle 5 Minuten die Datenbankabfrage ausführen + if now - last_db_check >= 300: + active_alarms = get_active_alarms() + last_db_check = now + + current_time = datetime.datetime.now().strftime("%H:%M:%S") + + for alarm_id, alarm_time in active_alarms: + if current_time >= alarm_time: + send_log(f"Triggering WLED Playlist at {alarm_time}") + trigger_wled_playlist() + mark_alarm_as_executed(alarm_id) + active_alarms = get_active_alarms() + + time.sleep(20) # Überprüfung alle 20 sec + +if __name__ == "__main__": + main() diff --git a/wsMQTTbridge.py b/wsMQTTbridge.py new file mode 100644 index 0000000..a3b478d --- /dev/null +++ b/wsMQTTbridge.py @@ -0,0 +1,139 @@ +import websocket +import time +import json +import dataclasses +import gc, logging as log +import threading +import paho.mqtt.client as mqtt +unacked_publish = set() + +mqttClient=mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) +#mqttClient.on_publish = on_publish +mqttClient.user_data_set(unacked_publish) + +def publish(dict,topic="weatherStation"): + if dataclasses.is_dataclass(dict): + for field in dataclasses.fields(dict): + key = field.name + value = getattr(dict, field.name) + if hasattr(value, '__len__') and (not isinstance(value, str)): + if dataclasses.is_dataclass(value): #make subtopics of dataclasses + publish(value,topic+"/"+key) + else: + #try: #make subtopics of dataclass arrays + if len(value): + if dataclasses.is_dataclass(value[0]): + i=0 + for d in value: + publish(d,topic+"/"+key+str(i)) + i=i+1 + #except: + #else: + #mqttClient.publish(topic+"/"+key,json.dumps(value, cls=EnhancedJSONEncoder),0,True) + #else: #output empty array anyway to keep data consistent + #mqttClient.publish(topic+"/"+key,json.dumps(value, cls=EnhancedJSONEncoder),0,True) + else: + try: + flt = float(value) + mqttClient.publish(topic+"/"+key,round(flt,2),0,True) + except ValueError: + mqttClient.publish(topic+"/"+key,value,0,True) + else: + for key, value in dict.items(): + if hasattr(value, '__len__') and (not isinstance(value, str)): + if dataclasses.is_dataclass(value): #make subtopics of dataclasses + publish(value,topic+"/"+key) + else: + #try: #make subtopics of dataclass arrays + if len(value): + if dataclasses.is_dataclass(value[0]): + i=0 + for d in value: + publish(d,topic+"/"+key+str(i)) + i=i+1 + #except: + #else: + #mqttClient.publish(topic+"/"+key,json.dumps(value, cls=EnhancedJSONEncoder),0,True) + #else: #output empty array anyway to keep data consistent + #mqttClient.publish(topic+"/"+key,json.dumps(value, cls=EnhancedJSONEncoder),0,True) + else: + try: + flt = float(value) + mqttClient.publish(topic+"/"+key,round(flt,2),0,True) + except ValueError: + mqttClient.publish(topic+"/"+key,value,0,True) + except: + log.error(f"value is no number") + + #print(message) + +def startMqttClient(): + mqttClient.connect("localhost", 1883, 60) + mqttClient.loop_start() + #await asyncio.Future() # run forever +def closeMqttClient(): + mqttClient.disconnect() + + + +def on_message(ws, message, msgCounter=[0]): + msgCounter[0] += 1 + data = json.loads(message) + if msgCounter[0] >1: + if "tempAmb" in data: + data.pop("tempAmb") + if "hum" in data: + data.pop("hum") + if "qff" in data: + data.pop("qff") + if "dewpt" in data: + data.pop("dewpt") + if "iaq" in data: + data.pop("iaq") + if "avgWindspeed" in data: + data.pop("avgWindspeed") + if "avgWindDeg" in data: + data.pop("avgWindDeg") + if msgCounter[0] > 20: + msgCounter[0] = 0 + + publish(data,topic="weatherStation") + +def on_error(ws, error): + #ws.web_socket_open = False + #ws.logged_in = False + #print("WebSocket Error ") + #print("Reconnect to the endpoint after 3 seconds... ") + #time.sleep(3) + #try: + # ws.run_forever() + #except Exception as error: + print(f"Encountered error: {error}") + +def on_close(ws, close_status_code, close_msg): + ws.web_socket_open = False + ws.logged_in = False + print("WebSocket Closed") + + +def on_open(ws): + print("Connection opened") + ws.send("Hello, Server!") + + +if __name__ == "__main__": + startMqttClient() + while True: + try: + ws = websocket.WebSocketApp("ws://192.168.179.42/ws", + on_message=on_message, + on_error=on_error, + on_close=on_close) + ws.on_open = on_open + ws.run_forever(skip_utf8_validation=True,ping_interval=10,ping_timeout=8) + except Exception as e: + gc.collect() + print("Websocket connection Error : {0}".format(e)) + print("Reconnecting websocket after 10 sec") + time.sleep(10) + #ws.run_forever() \ No newline at end of file diff --git a/yarl/__init__.py b/yarl/__init__.py new file mode 100644 index 0000000..b92ac79 --- /dev/null +++ b/yarl/__init__.py @@ -0,0 +1,5 @@ +from ._url import URL, cache_clear, cache_configure, cache_info + +__version__ = "1.9.2" + +__all__ = ("URL", "cache_clear", "cache_configure", "cache_info") diff --git a/yarl/__init__.pyi b/yarl/__init__.pyi new file mode 100644 index 0000000..5fd4bd0 --- /dev/null +++ b/yarl/__init__.pyi @@ -0,0 +1,121 @@ +import sys +from functools import _CacheInfo +from typing import Any, Mapping, Optional, Sequence, Tuple, Type, Union, overload + +import multidict + +if sys.version_info >= (3, 8): + from typing import Final, TypedDict, final +else: + from typing_extensions import Final, TypedDict, final + +_SimpleQuery = Union[str, int, float] +_QueryVariable = Union[_SimpleQuery, Sequence[_SimpleQuery]] +_Query = Union[ + None, str, Mapping[str, _QueryVariable], Sequence[Tuple[str, _QueryVariable]] +] + +@final +class URL: + scheme: Final[str] + raw_user: Final[str] + user: Final[Optional[str]] + raw_password: Final[Optional[str]] + password: Final[Optional[str]] + raw_host: Final[Optional[str]] + host: Final[Optional[str]] + port: Final[Optional[int]] + explicit_port: Final[Optional[int]] + raw_authority: Final[str] + authority: Final[str] + raw_path: Final[str] + path: Final[str] + raw_query_string: Final[str] + query_string: Final[str] + path_qs: Final[str] + raw_path_qs: Final[str] + raw_fragment: Final[str] + fragment: Final[str] + query: Final[multidict.MultiDict[str]] + raw_name: Final[str] + name: Final[str] + raw_suffix: Final[str] + suffix: Final[str] + raw_suffixes: Final[Tuple[str, ...]] + suffixes: Final[Tuple[str, ...]] + raw_parts: Final[Tuple[str, ...]] + parts: Final[Tuple[str, ...]] + parent: Final[URL] + def __init__( + self, val: Union[str, "URL"] = ..., *, encoded: bool = ... + ) -> None: ... + @classmethod + def build( + cls, + *, + scheme: str = ..., + authority: str = ..., + user: Optional[str] = ..., + password: Optional[str] = ..., + host: str = ..., + port: Optional[int] = ..., + path: str = ..., + query: Optional[_Query] = ..., + query_string: str = ..., + fragment: str = ..., + encoded: bool = ... + ) -> URL: ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + def __bytes__(self) -> bytes: ... + def __eq__(self, other: Any) -> bool: ... + def __le__(self, other: Any) -> bool: ... + def __lt__(self, other: Any) -> bool: ... + def __ge__(self, other: Any) -> bool: ... + def __gt__(self, other: Any) -> bool: ... + def __hash__(self) -> int: ... + def __truediv__(self, name: str) -> URL: ... + def __mod__(self, query: _Query) -> URL: ... + def is_absolute(self) -> bool: ... + def is_default_port(self) -> bool: ... + def origin(self) -> URL: ... + def relative(self) -> URL: ... + def with_scheme(self, scheme: str) -> URL: ... + def with_user(self, user: Optional[str]) -> URL: ... + def with_password(self, password: Optional[str]) -> URL: ... + def with_host(self, host: str) -> URL: ... + def with_port(self, port: Optional[int]) -> URL: ... + def with_path(self, path: str, *, encoded: bool = ...) -> URL: ... + @overload + def with_query(self, query: _Query) -> URL: ... + @overload + def with_query(self, **kwargs: _QueryVariable) -> URL: ... + @overload + def update_query(self, query: _Query) -> URL: ... + @overload + def update_query(self, **kwargs: _QueryVariable) -> URL: ... + def with_fragment(self, fragment: Optional[str]) -> URL: ... + def with_name(self, name: str) -> URL: ... + def with_suffix(self, suffix: str) -> URL: ... + def join(self, url: URL) -> URL: ... + def joinpath(self, *url: str, encoded: bool = ...) -> URL: ... + def human_repr(self) -> str: ... + # private API + @classmethod + def _normalize_path(cls, path: str) -> str: ... + +@final +class cached_property: + def __init__(self, wrapped: Any) -> None: ... + def __get__(self, inst: URL, owner: Type[URL]) -> Any: ... + def __set__(self, inst: URL, value: Any) -> None: ... + +class CacheInfo(TypedDict): + idna_encode: _CacheInfo + idna_decode: _CacheInfo + +def cache_clear() -> None: ... +def cache_info() -> CacheInfo: ... +def cache_configure( + *, idna_encode_size: Optional[int] = ..., idna_decode_size: Optional[int] = ... +) -> None: ... diff --git a/yarl/_quoting.py b/yarl/_quoting.py new file mode 100644 index 0000000..8d1c705 --- /dev/null +++ b/yarl/_quoting.py @@ -0,0 +1,18 @@ +import os +import sys + +__all__ = ("_Quoter", "_Unquoter") + + +NO_EXTENSIONS = bool(os.environ.get("YARL_NO_EXTENSIONS")) # type: bool +if sys.implementation.name != "cpython": + NO_EXTENSIONS = True + + +if not NO_EXTENSIONS: # pragma: no branch + try: + from ._quoting_c import _Quoter, _Unquoter # type: ignore[assignment] + except ImportError: # pragma: no cover + from ._quoting_py import _Quoter, _Unquoter # type: ignore[assignment] +else: + from ._quoting_py import _Quoter, _Unquoter # type: ignore[assignment] diff --git a/yarl/_quoting_c.pyi b/yarl/_quoting_c.pyi new file mode 100644 index 0000000..1c8fc24 --- /dev/null +++ b/yarl/_quoting_c.pyi @@ -0,0 +1,16 @@ +from typing import Optional + +class _Quoter: + def __init__( + self, + *, + safe: str = ..., + protected: str = ..., + qs: bool = ..., + requote: bool = ... + ) -> None: ... + def __call__(self, val: Optional[str] = ...) -> Optional[str]: ... + +class _Unquoter: + def __init__(self, *, unsafe: str = ..., qs: bool = ...) -> None: ... + def __call__(self, val: Optional[str] = ...) -> Optional[str]: ... diff --git a/yarl/_quoting_c.pyx b/yarl/_quoting_c.pyx new file mode 100644 index 0000000..5335d17 --- /dev/null +++ b/yarl/_quoting_c.pyx @@ -0,0 +1,371 @@ +# cython: language_level=3 + +from cpython.exc cimport PyErr_NoMemory +from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc +from cpython.unicode cimport PyUnicode_DecodeASCII, PyUnicode_DecodeUTF8Stateful +from libc.stdint cimport uint8_t, uint64_t +from libc.string cimport memcpy, memset + +from string import ascii_letters, digits + + +cdef str GEN_DELIMS = ":/?#[]@" +cdef str SUB_DELIMS_WITHOUT_QS = "!$'()*," +cdef str SUB_DELIMS = SUB_DELIMS_WITHOUT_QS + '+?=;' +cdef str RESERVED = GEN_DELIMS + SUB_DELIMS +cdef str UNRESERVED = ascii_letters + digits + '-._~' +cdef str ALLOWED = UNRESERVED + SUB_DELIMS_WITHOUT_QS +cdef str QS = '+&=;' + +DEF BUF_SIZE = 8 * 1024 # 8KiB +cdef char BUFFER[BUF_SIZE] + +cdef inline Py_UCS4 _to_hex(uint8_t v): + if v < 10: + return (v+0x30) # ord('0') == 0x30 + else: + return (v+0x41-10) # ord('A') == 0x41 + + +cdef inline int _from_hex(Py_UCS4 v): + if '0' <= v <= '9': + return (v) - 0x30 # ord('0') == 0x30 + elif 'A' <= v <= 'F': + return (v) - 0x41 + 10 # ord('A') == 0x41 + elif 'a' <= v <= 'f': + return (v) - 0x61 + 10 # ord('a') == 0x61 + else: + return -1 + + +cdef inline int _is_lower_hex(Py_UCS4 v): + return 'a' <= v <= 'f' + + +cdef inline Py_UCS4 _restore_ch(Py_UCS4 d1, Py_UCS4 d2): + cdef int digit1 = _from_hex(d1) + if digit1 < 0: + return -1 + cdef int digit2 = _from_hex(d2) + if digit2 < 0: + return -1 + return (digit1 << 4 | digit2) + + +cdef uint8_t ALLOWED_TABLE[16] +cdef uint8_t ALLOWED_NOTQS_TABLE[16] + + +cdef inline bint bit_at(uint8_t array[], uint64_t ch): + return array[ch >> 3] & (1 << (ch & 7)) + + +cdef inline void set_bit(uint8_t array[], uint64_t ch): + array[ch >> 3] |= (1 << (ch & 7)) + + +memset(ALLOWED_TABLE, 0, sizeof(ALLOWED_TABLE)) +memset(ALLOWED_NOTQS_TABLE, 0, sizeof(ALLOWED_NOTQS_TABLE)) + +for i in range(128): + if chr(i) in ALLOWED: + set_bit(ALLOWED_TABLE, i) + set_bit(ALLOWED_NOTQS_TABLE, i) + if chr(i) in QS: + set_bit(ALLOWED_NOTQS_TABLE, i) + +# ----------------- writer --------------------------- + +cdef struct Writer: + char *buf + Py_ssize_t size + Py_ssize_t pos + bint changed + + +cdef inline void _init_writer(Writer* writer): + writer.buf = &BUFFER[0] + writer.size = BUF_SIZE + writer.pos = 0 + writer.changed = 0 + + +cdef inline void _release_writer(Writer* writer): + if writer.buf != BUFFER: + PyMem_Free(writer.buf) + + +cdef inline int _write_char(Writer* writer, Py_UCS4 ch, bint changed): + cdef char * buf + cdef Py_ssize_t size + + if writer.pos == writer.size: + # reallocate + size = writer.size + BUF_SIZE + if writer.buf == BUFFER: + buf = PyMem_Malloc(size) + if buf == NULL: + PyErr_NoMemory() + return -1 + memcpy(buf, writer.buf, writer.size) + else: + buf = PyMem_Realloc(writer.buf, size) + if buf == NULL: + PyErr_NoMemory() + return -1 + writer.buf = buf + writer.size = size + writer.buf[writer.pos] = ch + writer.pos += 1 + writer.changed |= changed + return 0 + + +cdef inline int _write_pct(Writer* writer, uint8_t ch, bint changed): + if _write_char(writer, '%', changed) < 0: + return -1 + if _write_char(writer, _to_hex(ch >> 4), changed) < 0: + return -1 + return _write_char(writer, _to_hex(ch & 0x0f), changed) + + +cdef inline int _write_utf8(Writer* writer, Py_UCS4 symbol): + cdef uint64_t utf = symbol + + if utf < 0x80: + return _write_pct(writer, utf, True) + elif utf < 0x800: + if _write_pct(writer, (0xc0 | (utf >> 6)), True) < 0: + return -1 + return _write_pct(writer, (0x80 | (utf & 0x3f)), True) + elif 0xD800 <= utf <= 0xDFFF: + # surogate pair, ignored + return 0 + elif utf < 0x10000: + if _write_pct(writer, (0xe0 | (utf >> 12)), True) < 0: + return -1 + if _write_pct(writer, (0x80 | ((utf >> 6) & 0x3f)), + True) < 0: + return -1 + return _write_pct(writer, (0x80 | (utf & 0x3f)), True) + elif utf > 0x10FFFF: + # symbol is too large + return 0 + else: + if _write_pct(writer, (0xf0 | (utf >> 18)), True) < 0: + return -1 + if _write_pct(writer, (0x80 | ((utf >> 12) & 0x3f)), + True) < 0: + return -1 + if _write_pct(writer, (0x80 | ((utf >> 6) & 0x3f)), + True) < 0: + return -1 + return _write_pct(writer, (0x80 | (utf & 0x3f)), True) + + +# --------------------- end writer -------------------------- + + +cdef class _Quoter: + cdef bint _qs + cdef bint _requote + + cdef uint8_t _safe_table[16] + cdef uint8_t _protected_table[16] + + def __init__( + self, *, str safe='', str protected='', bint qs=False, bint requote=True, + ): + cdef Py_UCS4 ch + + self._qs = qs + self._requote = requote + + if not self._qs: + memcpy(self._safe_table, + ALLOWED_NOTQS_TABLE, + sizeof(self._safe_table)) + else: + memcpy(self._safe_table, + ALLOWED_TABLE, + sizeof(self._safe_table)) + for ch in safe: + if ord(ch) > 127: + raise ValueError("Only safe symbols with ORD < 128 are allowed") + set_bit(self._safe_table, ch) + + memset(self._protected_table, 0, sizeof(self._protected_table)) + for ch in protected: + if ord(ch) > 127: + raise ValueError("Only safe symbols with ORD < 128 are allowed") + set_bit(self._safe_table, ch) + set_bit(self._protected_table, ch) + + def __call__(self, val): + cdef Writer writer + if val is None: + return None + if type(val) is not str: + if isinstance(val, str): + # derived from str + val = str(val) + else: + raise TypeError("Argument should be str") + _init_writer(&writer) + try: + return self._do_quote(val, &writer) + finally: + _release_writer(&writer) + + cdef str _do_quote(self, str val, Writer *writer): + cdef Py_UCS4 ch + cdef int changed + cdef int idx = 0 + cdef int length = len(val) + + while idx < length: + ch = val[idx] + idx += 1 + if ch == '%' and self._requote and idx <= length - 2: + ch = _restore_ch(val[idx], val[idx + 1]) + if ch != -1: + idx += 2 + if ch < 128: + if bit_at(self._protected_table, ch): + if _write_pct(writer, ch, True) < 0: + raise + continue + + if bit_at(self._safe_table, ch): + if _write_char(writer, ch, True) < 0: + raise + continue + + changed = (_is_lower_hex(val[idx - 2]) or + _is_lower_hex(val[idx - 1])) + if _write_pct(writer, ch, changed) < 0: + raise + continue + else: + ch = '%' + + if self._write(writer, ch) < 0: + raise + + if not writer.changed: + return val + else: + return PyUnicode_DecodeASCII(writer.buf, writer.pos, "strict") + + cdef inline int _write(self, Writer *writer, Py_UCS4 ch): + if self._qs: + if ch == ' ': + return _write_char(writer, '+', True) + + if ch < 128 and bit_at(self._safe_table, ch): + return _write_char(writer, ch, False) + + return _write_utf8(writer, ch) + + +cdef class _Unquoter: + cdef str _unsafe + cdef bint _qs + cdef _Quoter _quoter + cdef _Quoter _qs_quoter + + def __init__(self, *, unsafe='', qs=False): + self._unsafe = unsafe + self._qs = qs + self._quoter = _Quoter() + self._qs_quoter = _Quoter(qs=True) + + def __call__(self, val): + if val is None: + return None + if type(val) is not str: + if isinstance(val, str): + # derived from str + val = str(val) + else: + raise TypeError("Argument should be str") + return self._do_unquote(val) + + cdef str _do_unquote(self, str val): + if len(val) == 0: + return val + cdef list ret = [] + cdef char buffer[4] + cdef Py_ssize_t buflen = 0 + cdef Py_ssize_t consumed + cdef str unquoted + cdef Py_UCS4 ch = 0 + cdef Py_ssize_t idx = 0 + cdef Py_ssize_t length = len(val) + cdef Py_ssize_t start_pct + + while idx < length: + ch = val[idx] + idx += 1 + if ch == '%' and idx <= length - 2: + ch = _restore_ch(val[idx], val[idx + 1]) + if ch != -1: + idx += 2 + assert buflen < 4 + buffer[buflen] = ch + buflen += 1 + try: + unquoted = PyUnicode_DecodeUTF8Stateful(buffer, buflen, + NULL, &consumed) + except UnicodeDecodeError: + start_pct = idx - buflen * 3 + buffer[0] = ch + buflen = 1 + ret.append(val[start_pct : idx - 3]) + try: + unquoted = PyUnicode_DecodeUTF8Stateful(buffer, buflen, + NULL, &consumed) + except UnicodeDecodeError: + buflen = 0 + ret.append(val[idx - 3 : idx]) + continue + if not unquoted: + assert consumed == 0 + continue + assert consumed == buflen + buflen = 0 + if self._qs and unquoted in '+=&;': + ret.append(self._qs_quoter(unquoted)) + elif unquoted in self._unsafe: + ret.append(self._quoter(unquoted)) + else: + ret.append(unquoted) + continue + else: + ch = '%' + + if buflen: + start_pct = idx - 1 - buflen * 3 + ret.append(val[start_pct : idx - 1]) + buflen = 0 + + if ch == '+': + if not self._qs or ch in self._unsafe: + ret.append('+') + else: + ret.append(' ') + continue + + if ch in self._unsafe: + ret.append('%') + h = hex(ord(ch)).upper()[2:] + for ch in h: + ret.append(ch) + continue + + ret.append(ch) + + if buflen: + ret.append(val[length - buflen * 3 : length]) + + return ''.join(ret) diff --git a/yarl/_quoting_py.py b/yarl/_quoting_py.py new file mode 100644 index 0000000..585a1da --- /dev/null +++ b/yarl/_quoting_py.py @@ -0,0 +1,197 @@ +import codecs +import re +from string import ascii_letters, ascii_lowercase, digits +from typing import Optional, cast + +BASCII_LOWERCASE = ascii_lowercase.encode("ascii") +BPCT_ALLOWED = {f"%{i:02X}".encode("ascii") for i in range(256)} +GEN_DELIMS = ":/?#[]@" +SUB_DELIMS_WITHOUT_QS = "!$'()*," +SUB_DELIMS = SUB_DELIMS_WITHOUT_QS + "+&=;" +RESERVED = GEN_DELIMS + SUB_DELIMS +UNRESERVED = ascii_letters + digits + "-._~" +ALLOWED = UNRESERVED + SUB_DELIMS_WITHOUT_QS + + +_IS_HEX = re.compile(b"[A-Z0-9][A-Z0-9]") +_IS_HEX_STR = re.compile("[A-Fa-f0-9][A-Fa-f0-9]") + +utf8_decoder = codecs.getincrementaldecoder("utf-8") + + +class _Quoter: + def __init__( + self, + *, + safe: str = "", + protected: str = "", + qs: bool = False, + requote: bool = True, + ) -> None: + self._safe = safe + self._protected = protected + self._qs = qs + self._requote = requote + + def __call__(self, val: Optional[str]) -> Optional[str]: + if val is None: + return None + if not isinstance(val, str): + raise TypeError("Argument should be str") + if not val: + return "" + bval = cast(str, val).encode("utf8", errors="ignore") + ret = bytearray() + pct = bytearray() + safe = self._safe + safe += ALLOWED + if not self._qs: + safe += "+&=;" + safe += self._protected + bsafe = safe.encode("ascii") + idx = 0 + while idx < len(bval): + ch = bval[idx] + idx += 1 + + if pct: + if ch in BASCII_LOWERCASE: + ch = ch - 32 # convert to uppercase + pct.append(ch) + if len(pct) == 3: # pragma: no branch # peephole optimizer + buf = pct[1:] + if not _IS_HEX.match(buf): + ret.extend(b"%25") + pct.clear() + idx -= 2 + continue + try: + unquoted = chr(int(pct[1:].decode("ascii"), base=16)) + except ValueError: + ret.extend(b"%25") + pct.clear() + idx -= 2 + continue + + if unquoted in self._protected: + ret.extend(pct) + elif unquoted in safe: + ret.append(ord(unquoted)) + else: + ret.extend(pct) + pct.clear() + + # special case, if we have only one char after "%" + elif len(pct) == 2 and idx == len(bval): + ret.extend(b"%25") + pct.clear() + idx -= 1 + + continue + + elif ch == ord("%") and self._requote: + pct.clear() + pct.append(ch) + + # special case if "%" is last char + if idx == len(bval): + ret.extend(b"%25") + + continue + + if self._qs: + if ch == ord(" "): + ret.append(ord("+")) + continue + if ch in bsafe: + ret.append(ch) + continue + + ret.extend((f"%{ch:02X}").encode("ascii")) + + ret2 = ret.decode("ascii") + if ret2 == val: + return val + return ret2 + + +class _Unquoter: + def __init__(self, *, unsafe: str = "", qs: bool = False) -> None: + self._unsafe = unsafe + self._qs = qs + self._quoter = _Quoter() + self._qs_quoter = _Quoter(qs=True) + + def __call__(self, val: Optional[str]) -> Optional[str]: + if val is None: + return None + if not isinstance(val, str): + raise TypeError("Argument should be str") + if not val: + return "" + decoder = cast(codecs.BufferedIncrementalDecoder, utf8_decoder()) + ret = [] + idx = 0 + while idx < len(val): + ch = val[idx] + idx += 1 + if ch == "%" and idx <= len(val) - 2: + pct = val[idx : idx + 2] + if _IS_HEX_STR.fullmatch(pct): + b = bytes([int(pct, base=16)]) + idx += 2 + try: + unquoted = decoder.decode(b) + except UnicodeDecodeError: + start_pct = idx - 3 - len(decoder.buffer) * 3 + ret.append(val[start_pct : idx - 3]) + decoder.reset() + try: + unquoted = decoder.decode(b) + except UnicodeDecodeError: + ret.append(val[idx - 3 : idx]) + continue + if not unquoted: + continue + if self._qs and unquoted in "+=&;": + to_add = self._qs_quoter(unquoted) + if to_add is None: # pragma: no cover + raise RuntimeError("Cannot quote None") + ret.append(to_add) + elif unquoted in self._unsafe: + to_add = self._quoter(unquoted) + if to_add is None: # pragma: no cover + raise RuntimeError("Cannot quote None") + ret.append(to_add) + else: + ret.append(unquoted) + continue + + if decoder.buffer: + start_pct = idx - 1 - len(decoder.buffer) * 3 + ret.append(val[start_pct : idx - 1]) + decoder.reset() + + if ch == "+": + if not self._qs or ch in self._unsafe: + ret.append("+") + else: + ret.append(" ") + continue + + if ch in self._unsafe: + ret.append("%") + h = hex(ord(ch)).upper()[2:] + for ch in h: + ret.append(ch) + continue + + ret.append(ch) + + if decoder.buffer: + ret.append(val[-len(decoder.buffer) * 3 :]) + + ret2 = "".join(ret) + if ret2 == val: + return val + return ret2 diff --git a/yarl/_url.py b/yarl/_url.py new file mode 100644 index 0000000..c8f2acb --- /dev/null +++ b/yarl/_url.py @@ -0,0 +1,1198 @@ +import functools +import math +import warnings +from collections.abc import Mapping, Sequence +from contextlib import suppress +from ipaddress import ip_address +from urllib.parse import SplitResult, parse_qsl, quote, urljoin, urlsplit, urlunsplit + +import idna +from multidict import MultiDict, MultiDictProxy + +from ._quoting import _Quoter, _Unquoter + +DEFAULT_PORTS = {"http": 80, "https": 443, "ws": 80, "wss": 443} + +sentinel = object() + + +def rewrite_module(obj: object) -> object: + obj.__module__ = "yarl" + return obj + + +class cached_property: + """Use as a class method decorator. It operates almost exactly like + the Python `@property` decorator, but it puts the result of the + method it decorates into the instance dict after the first call, + effectively replacing the function it decorates with an instance + variable. It is, in Python parlance, a data descriptor. + + """ + + def __init__(self, wrapped): + self.wrapped = wrapped + try: + self.__doc__ = wrapped.__doc__ + except AttributeError: # pragma: no cover + self.__doc__ = "" + self.name = wrapped.__name__ + + def __get__(self, inst, owner, _sentinel=sentinel): + if inst is None: + return self + val = inst._cache.get(self.name, _sentinel) + if val is not _sentinel: + return val + val = self.wrapped(inst) + inst._cache[self.name] = val + return val + + def __set__(self, inst, value): + raise AttributeError("cached property is read-only") + + +def _normalize_path_segments(segments): + """Drop '.' and '..' from a sequence of str segments""" + + resolved_path = [] + + for seg in segments: + if seg == "..": + # ignore any .. segments that would otherwise cause an + # IndexError when popped from resolved_path if + # resolving for rfc3986 + with suppress(IndexError): + resolved_path.pop() + elif seg != ".": + resolved_path.append(seg) + + if segments and segments[-1] in (".", ".."): + # do some post-processing here. + # if the last segment was a relative dir, + # then we need to append the trailing '/' + resolved_path.append("") + + return resolved_path + + +@rewrite_module +class URL: + # Don't derive from str + # follow pathlib.Path design + # probably URL will not suffer from pathlib problems: + # it's intended for libraries like aiohttp, + # not to be passed into standard library functions like os.open etc. + + # URL grammar (RFC 3986) + # pct-encoded = "%" HEXDIG HEXDIG + # reserved = gen-delims / sub-delims + # gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" + # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + # / "*" / "+" / "," / ";" / "=" + # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + # URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + # hier-part = "//" authority path-abempty + # / path-absolute + # / path-rootless + # / path-empty + # scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + # authority = [ userinfo "@" ] host [ ":" port ] + # userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) + # host = IP-literal / IPv4address / reg-name + # IP-literal = "[" ( IPv6address / IPvFuture ) "]" + # IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) + # IPv6address = 6( h16 ":" ) ls32 + # / "::" 5( h16 ":" ) ls32 + # / [ h16 ] "::" 4( h16 ":" ) ls32 + # / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + # / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + # / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + # / [ *4( h16 ":" ) h16 ] "::" ls32 + # / [ *5( h16 ":" ) h16 ] "::" h16 + # / [ *6( h16 ":" ) h16 ] "::" + # ls32 = ( h16 ":" h16 ) / IPv4address + # ; least-significant 32 bits of address + # h16 = 1*4HEXDIG + # ; 16 bits of address represented in hexadecimal + # IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet + # dec-octet = DIGIT ; 0-9 + # / %x31-39 DIGIT ; 10-99 + # / "1" 2DIGIT ; 100-199 + # / "2" %x30-34 DIGIT ; 200-249 + # / "25" %x30-35 ; 250-255 + # reg-name = *( unreserved / pct-encoded / sub-delims ) + # port = *DIGIT + # path = path-abempty ; begins with "/" or is empty + # / path-absolute ; begins with "/" but not "//" + # / path-noscheme ; begins with a non-colon segment + # / path-rootless ; begins with a segment + # / path-empty ; zero characters + # path-abempty = *( "/" segment ) + # path-absolute = "/" [ segment-nz *( "/" segment ) ] + # path-noscheme = segment-nz-nc *( "/" segment ) + # path-rootless = segment-nz *( "/" segment ) + # path-empty = 0 + # segment = *pchar + # segment-nz = 1*pchar + # segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) + # ; non-zero-length segment without any colon ":" + # pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + # query = *( pchar / "/" / "?" ) + # fragment = *( pchar / "/" / "?" ) + # URI-reference = URI / relative-ref + # relative-ref = relative-part [ "?" query ] [ "#" fragment ] + # relative-part = "//" authority path-abempty + # / path-absolute + # / path-noscheme + # / path-empty + # absolute-URI = scheme ":" hier-part [ "?" query ] + __slots__ = ("_cache", "_val") + + _QUOTER = _Quoter(requote=False) + _REQUOTER = _Quoter() + _PATH_QUOTER = _Quoter(safe="@:", protected="/+", requote=False) + _PATH_REQUOTER = _Quoter(safe="@:", protected="/+") + _QUERY_QUOTER = _Quoter(safe="?/:@", protected="=+&;", qs=True, requote=False) + _QUERY_REQUOTER = _Quoter(safe="?/:@", protected="=+&;", qs=True) + _QUERY_PART_QUOTER = _Quoter(safe="?/:@", qs=True, requote=False) + _FRAGMENT_QUOTER = _Quoter(safe="?/:@", requote=False) + _FRAGMENT_REQUOTER = _Quoter(safe="?/:@") + + _UNQUOTER = _Unquoter() + _PATH_UNQUOTER = _Unquoter(unsafe="+") + _QS_UNQUOTER = _Unquoter(qs=True) + + def __new__(cls, val="", *, encoded=False, strict=None): + if strict is not None: # pragma: no cover + warnings.warn("strict parameter is ignored") + if type(val) is cls: + return val + if type(val) is str: + val = urlsplit(val) + elif type(val) is SplitResult: + if not encoded: + raise ValueError("Cannot apply decoding to SplitResult") + elif isinstance(val, str): + val = urlsplit(str(val)) + else: + raise TypeError("Constructor parameter should be str") + + if not encoded: + if not val[1]: # netloc + netloc = "" + host = "" + else: + host = val.hostname + if host is None: + raise ValueError("Invalid URL: host is required for absolute urls") + + try: + port = val.port + except ValueError as e: + raise ValueError( + "Invalid URL: port can't be converted to integer" + ) from e + + netloc = cls._make_netloc( + val.username, val.password, host, port, encode=True, requote=True + ) + path = cls._PATH_REQUOTER(val[2]) + if netloc: + path = cls._normalize_path(path) + + cls._validate_authority_uri_abs_path(host=host, path=path) + query = cls._QUERY_REQUOTER(val[3]) + fragment = cls._FRAGMENT_REQUOTER(val[4]) + val = SplitResult(val[0], netloc, path, query, fragment) + + self = object.__new__(cls) + self._val = val + self._cache = {} + return self + + @classmethod + def build( + cls, + *, + scheme="", + authority="", + user=None, + password=None, + host="", + port=None, + path="", + query=None, + query_string="", + fragment="", + encoded=False, + ): + """Creates and returns a new URL""" + + if authority and (user or password or host or port): + raise ValueError( + 'Can\'t mix "authority" with "user", "password", "host" or "port".' + ) + if port and not host: + raise ValueError('Can\'t build URL with "port" but without "host".') + if query and query_string: + raise ValueError('Only one of "query" or "query_string" should be passed') + if ( + scheme is None + or authority is None + or host is None + or path is None + or query_string is None + or fragment is None + ): + raise TypeError( + 'NoneType is illegal for "scheme", "authority", "host", "path", ' + '"query_string", and "fragment" args, use empty string instead.' + ) + + if authority: + if encoded: + netloc = authority + else: + tmp = SplitResult("", authority, "", "", "") + netloc = cls._make_netloc( + tmp.username, tmp.password, tmp.hostname, tmp.port, encode=True + ) + elif not user and not password and not host and not port: + netloc = "" + else: + netloc = cls._make_netloc( + user, password, host, port, encode=not encoded, encode_host=not encoded + ) + if not encoded: + path = cls._PATH_QUOTER(path) + if netloc: + path = cls._normalize_path(path) + + cls._validate_authority_uri_abs_path(host=host, path=path) + query_string = cls._QUERY_QUOTER(query_string) + fragment = cls._FRAGMENT_QUOTER(fragment) + + url = cls( + SplitResult(scheme, netloc, path, query_string, fragment), encoded=True + ) + + if query: + return url.with_query(query) + else: + return url + + def __init_subclass__(cls): + raise TypeError(f"Inheriting a class {cls!r} from URL is forbidden") + + def __str__(self): + val = self._val + if not val.path and self.is_absolute() and (val.query or val.fragment): + val = val._replace(path="/") + return urlunsplit(val) + + def __repr__(self): + return f"{self.__class__.__name__}('{str(self)}')" + + def __bytes__(self): + return str(self).encode("ascii") + + def __eq__(self, other): + if not type(other) is URL: + return NotImplemented + + val1 = self._val + if not val1.path and self.is_absolute(): + val1 = val1._replace(path="/") + + val2 = other._val + if not val2.path and other.is_absolute(): + val2 = val2._replace(path="/") + + return val1 == val2 + + def __hash__(self): + ret = self._cache.get("hash") + if ret is None: + val = self._val + if not val.path and self.is_absolute(): + val = val._replace(path="/") + ret = self._cache["hash"] = hash(val) + return ret + + def __le__(self, other): + if not type(other) is URL: + return NotImplemented + return self._val <= other._val + + def __lt__(self, other): + if not type(other) is URL: + return NotImplemented + return self._val < other._val + + def __ge__(self, other): + if not type(other) is URL: + return NotImplemented + return self._val >= other._val + + def __gt__(self, other): + if not type(other) is URL: + return NotImplemented + return self._val > other._val + + def __truediv__(self, name): + if not isinstance(name, str): + return NotImplemented + return self._make_child((str(name),)) + + def __mod__(self, query): + return self.update_query(query) + + def __bool__(self) -> bool: + return bool( + self._val.netloc or self._val.path or self._val.query or self._val.fragment + ) + + def __getstate__(self): + return (self._val,) + + def __setstate__(self, state): + if state[0] is None and isinstance(state[1], dict): + # default style pickle + self._val = state[1]["_val"] + else: + self._val, *unused = state + self._cache = {} + + def is_absolute(self): + """A check for absolute URLs. + + Return True for absolute ones (having scheme or starting + with //), False otherwise. + + """ + return self.raw_host is not None + + def is_default_port(self): + """A check for default port. + + Return True if port is default for specified scheme, + e.g. 'http://python.org' or 'http://python.org:80', False + otherwise. + + """ + if self.port is None: + return False + default = DEFAULT_PORTS.get(self.scheme) + if default is None: + return False + return self.port == default + + def origin(self): + """Return an URL with scheme, host and port parts only. + + user, password, path, query and fragment are removed. + + """ + # TODO: add a keyword-only option for keeping user/pass maybe? + if not self.is_absolute(): + raise ValueError("URL should be absolute") + if not self._val.scheme: + raise ValueError("URL should have scheme") + v = self._val + netloc = self._make_netloc(None, None, v.hostname, v.port) + val = v._replace(netloc=netloc, path="", query="", fragment="") + return URL(val, encoded=True) + + def relative(self): + """Return a relative part of the URL. + + scheme, user, password, host and port are removed. + + """ + if not self.is_absolute(): + raise ValueError("URL should be absolute") + val = self._val._replace(scheme="", netloc="") + return URL(val, encoded=True) + + @property + def scheme(self): + """Scheme for absolute URLs. + + Empty string for relative URLs or URLs starting with // + + """ + return self._val.scheme + + @property + def raw_authority(self): + """Encoded authority part of URL. + + Empty string for relative URLs. + + """ + return self._val.netloc + + @cached_property + def authority(self): + """Decoded authority part of URL. + + Empty string for relative URLs. + + """ + return self._make_netloc( + self.user, self.password, self.host, self.port, encode_host=False + ) + + @property + def raw_user(self): + """Encoded user part of URL. + + None if user is missing. + + """ + # not .username + ret = self._val.username + if not ret: + return None + return ret + + @cached_property + def user(self): + """Decoded user part of URL. + + None if user is missing. + + """ + return self._UNQUOTER(self.raw_user) + + @property + def raw_password(self): + """Encoded password part of URL. + + None if password is missing. + + """ + return self._val.password + + @cached_property + def password(self): + """Decoded password part of URL. + + None if password is missing. + + """ + return self._UNQUOTER(self.raw_password) + + @property + def raw_host(self): + """Encoded host part of URL. + + None for relative URLs. + + """ + # Use host instead of hostname for sake of shortness + # May add .hostname prop later + return self._val.hostname + + @cached_property + def host(self): + """Decoded host part of URL. + + None for relative URLs. + + """ + raw = self.raw_host + if raw is None: + return None + if "%" in raw: + # Hack for scoped IPv6 addresses like + # fe80::2%Проверка + # presence of '%' sign means only IPv6 address, so idna is useless. + return raw + return _idna_decode(raw) + + @property + def port(self): + """Port part of URL, with scheme-based fallback. + + None for relative URLs or URLs without explicit port and + scheme without default port substitution. + + """ + return self._val.port or DEFAULT_PORTS.get(self._val.scheme) + + @property + def explicit_port(self): + """Port part of URL, without scheme-based fallback. + + None for relative URLs or URLs without explicit port. + + """ + return self._val.port + + @property + def raw_path(self): + """Encoded path of URL. + + / for absolute URLs without path part. + + """ + ret = self._val.path + if not ret and self.is_absolute(): + ret = "/" + return ret + + @cached_property + def path(self): + """Decoded path of URL. + + / for absolute URLs without path part. + + """ + return self._PATH_UNQUOTER(self.raw_path) + + @cached_property + def query(self): + """A MultiDictProxy representing parsed query parameters in decoded + representation. + + Empty value if URL has no query part. + + """ + ret = MultiDict(parse_qsl(self.raw_query_string, keep_blank_values=True)) + return MultiDictProxy(ret) + + @property + def raw_query_string(self): + """Encoded query part of URL. + + Empty string if query is missing. + + """ + return self._val.query + + @cached_property + def query_string(self): + """Decoded query part of URL. + + Empty string if query is missing. + + """ + return self._QS_UNQUOTER(self.raw_query_string) + + @cached_property + def path_qs(self): + """Decoded path of URL with query.""" + if not self.query_string: + return self.path + return f"{self.path}?{self.query_string}" + + @cached_property + def raw_path_qs(self): + """Encoded path of URL with query.""" + if not self.raw_query_string: + return self.raw_path + return f"{self.raw_path}?{self.raw_query_string}" + + @property + def raw_fragment(self): + """Encoded fragment part of URL. + + Empty string if fragment is missing. + + """ + return self._val.fragment + + @cached_property + def fragment(self): + """Decoded fragment part of URL. + + Empty string if fragment is missing. + + """ + return self._UNQUOTER(self.raw_fragment) + + @cached_property + def raw_parts(self): + """A tuple containing encoded *path* parts. + + ('/',) for absolute URLs if *path* is missing. + + """ + path = self._val.path + if self.is_absolute(): + if not path: + parts = ["/"] + else: + parts = ["/"] + path[1:].split("/") + else: + if path.startswith("/"): + parts = ["/"] + path[1:].split("/") + else: + parts = path.split("/") + return tuple(parts) + + @cached_property + def parts(self): + """A tuple containing decoded *path* parts. + + ('/',) for absolute URLs if *path* is missing. + + """ + return tuple(self._UNQUOTER(part) for part in self.raw_parts) + + @cached_property + def parent(self): + """A new URL with last part of path removed and cleaned up query and + fragment. + + """ + path = self.raw_path + if not path or path == "/": + if self.raw_fragment or self.raw_query_string: + return URL(self._val._replace(query="", fragment=""), encoded=True) + return self + parts = path.split("/") + val = self._val._replace(path="/".join(parts[:-1]), query="", fragment="") + return URL(val, encoded=True) + + @cached_property + def raw_name(self): + """The last part of raw_parts.""" + parts = self.raw_parts + if self.is_absolute(): + parts = parts[1:] + if not parts: + return "" + else: + return parts[-1] + else: + return parts[-1] + + @cached_property + def name(self): + """The last part of parts.""" + return self._UNQUOTER(self.raw_name) + + @cached_property + def raw_suffix(self): + name = self.raw_name + i = name.rfind(".") + if 0 < i < len(name) - 1: + return name[i:] + else: + return "" + + @cached_property + def suffix(self): + return self._UNQUOTER(self.raw_suffix) + + @cached_property + def raw_suffixes(self): + name = self.raw_name + if name.endswith("."): + return () + name = name.lstrip(".") + return tuple("." + suffix for suffix in name.split(".")[1:]) + + @cached_property + def suffixes(self): + return tuple(self._UNQUOTER(suffix) for suffix in self.raw_suffixes) + + @staticmethod + def _validate_authority_uri_abs_path(host, path): + """Ensure that path in URL with authority starts with a leading slash. + + Raise ValueError if not. + """ + if len(host) > 0 and len(path) > 0 and not path.startswith("/"): + raise ValueError( + "Path in a URL with authority should start with a slash ('/') if set" + ) + + def _make_child(self, segments, encoded=False): + """add segments to self._val.path, accounting for absolute vs relative paths""" + # keep the trailing slash if the last segment ends with / + parsed = [""] if segments and segments[-1][-1:] == "/" else [] + for seg in reversed(segments): + if not seg: + continue + if seg[0] == "/": + raise ValueError( + f"Appending path {seg!r} starting from slash is forbidden" + ) + seg = seg if encoded else self._PATH_QUOTER(seg) + if "/" in seg: + parsed += ( + sub for sub in reversed(seg.split("/")) if sub and sub != "." + ) + elif seg != ".": + parsed.append(seg) + parsed.reverse() + old_path = self._val.path + if old_path: + parsed = [*old_path.rstrip("/").split("/"), *parsed] + if self.is_absolute(): + parsed = _normalize_path_segments(parsed) + if parsed and parsed[0] != "": + # inject a leading slash when adding a path to an absolute URL + # where there was none before + parsed = ["", *parsed] + new_path = "/".join(parsed) + return URL( + self._val._replace(path=new_path, query="", fragment=""), encoded=True + ) + + @classmethod + def _normalize_path(cls, path): + # Drop '.' and '..' from str path + + prefix = "" + if path.startswith("/"): + # preserve the "/" root element of absolute paths, copying it to the + # normalised output as per sections 5.2.4 and 6.2.2.3 of rfc3986. + prefix = "/" + path = path[1:] + + segments = path.split("/") + return prefix + "/".join(_normalize_path_segments(segments)) + + @classmethod + def _encode_host(cls, host, human=False): + try: + ip, sep, zone = host.partition("%") + ip = ip_address(ip) + except ValueError: + host = host.lower() + # IDNA encoding is slow, + # skip it for ASCII-only strings + # Don't move the check into _idna_encode() helper + # to reduce the cache size + if human or host.isascii(): + return host + host = _idna_encode(host) + else: + host = ip.compressed + if sep: + host += "%" + zone + if ip.version == 6: + host = "[" + host + "]" + return host + + @classmethod + def _make_netloc( + cls, user, password, host, port, encode=False, encode_host=True, requote=False + ): + quoter = cls._REQUOTER if requote else cls._QUOTER + if encode_host: + ret = cls._encode_host(host) + else: + ret = host + if port is not None: + ret = ret + ":" + str(port) + if password is not None: + if not user: + user = "" + else: + if encode: + user = quoter(user) + if encode: + password = quoter(password) + user = user + ":" + password + elif user and encode: + user = quoter(user) + if user: + ret = user + "@" + ret + return ret + + def with_scheme(self, scheme): + """Return a new URL with scheme replaced.""" + # N.B. doesn't cleanup query/fragment + if not isinstance(scheme, str): + raise TypeError("Invalid scheme type") + if not self.is_absolute(): + raise ValueError("scheme replacement is not allowed for relative URLs") + return URL(self._val._replace(scheme=scheme.lower()), encoded=True) + + def with_user(self, user): + """Return a new URL with user replaced. + + Autoencode user if needed. + + Clear user/password if user is None. + + """ + # N.B. doesn't cleanup query/fragment + val = self._val + if user is None: + password = None + elif isinstance(user, str): + user = self._QUOTER(user) + password = val.password + else: + raise TypeError("Invalid user type") + if not self.is_absolute(): + raise ValueError("user replacement is not allowed for relative URLs") + return URL( + self._val._replace( + netloc=self._make_netloc(user, password, val.hostname, val.port) + ), + encoded=True, + ) + + def with_password(self, password): + """Return a new URL with password replaced. + + Autoencode password if needed. + + Clear password if argument is None. + + """ + # N.B. doesn't cleanup query/fragment + if password is None: + pass + elif isinstance(password, str): + password = self._QUOTER(password) + else: + raise TypeError("Invalid password type") + if not self.is_absolute(): + raise ValueError("password replacement is not allowed for relative URLs") + val = self._val + return URL( + self._val._replace( + netloc=self._make_netloc(val.username, password, val.hostname, val.port) + ), + encoded=True, + ) + + def with_host(self, host): + """Return a new URL with host replaced. + + Autoencode host if needed. + + Changing host for relative URLs is not allowed, use .join() + instead. + + """ + # N.B. doesn't cleanup query/fragment + if not isinstance(host, str): + raise TypeError("Invalid host type") + if not self.is_absolute(): + raise ValueError("host replacement is not allowed for relative URLs") + if not host: + raise ValueError("host removing is not allowed") + val = self._val + return URL( + self._val._replace( + netloc=self._make_netloc(val.username, val.password, host, val.port) + ), + encoded=True, + ) + + def with_port(self, port): + """Return a new URL with port replaced. + + Clear port to default if None is passed. + + """ + # N.B. doesn't cleanup query/fragment + if port is not None: + if isinstance(port, bool) or not isinstance(port, int): + raise TypeError(f"port should be int or None, got {type(port)}") + if port < 0 or port > 65535: + raise ValueError(f"port must be between 0 and 65535, got {port}") + if not self.is_absolute(): + raise ValueError("port replacement is not allowed for relative URLs") + val = self._val + return URL( + self._val._replace( + netloc=self._make_netloc(val.username, val.password, val.hostname, port) + ), + encoded=True, + ) + + def with_path(self, path, *, encoded=False): + """Return a new URL with path replaced.""" + if not encoded: + path = self._PATH_QUOTER(path) + if self.is_absolute(): + path = self._normalize_path(path) + if len(path) > 0 and path[0] != "/": + path = "/" + path + return URL(self._val._replace(path=path, query="", fragment=""), encoded=True) + + @classmethod + def _query_seq_pairs(cls, quoter, pairs): + for key, val in pairs: + if isinstance(val, (list, tuple)): + for v in val: + yield quoter(key) + "=" + quoter(cls._query_var(v)) + else: + yield quoter(key) + "=" + quoter(cls._query_var(val)) + + @staticmethod + def _query_var(v): + cls = type(v) + if issubclass(cls, str): + return v + if issubclass(cls, float): + if math.isinf(v): + raise ValueError("float('inf') is not supported") + if math.isnan(v): + raise ValueError("float('nan') is not supported") + return str(float(v)) + if issubclass(cls, int) and cls is not bool: + return str(int(v)) + raise TypeError( + "Invalid variable type: value " + "should be str, int or float, got {!r} " + "of type {}".format(v, cls) + ) + + def _get_str_query(self, *args, **kwargs): + if kwargs: + if len(args) > 0: + raise ValueError( + "Either kwargs or single query parameter must be present" + ) + query = kwargs + elif len(args) == 1: + query = args[0] + else: + raise ValueError("Either kwargs or single query parameter must be present") + + if query is None: + query = None + elif isinstance(query, Mapping): + quoter = self._QUERY_PART_QUOTER + query = "&".join(self._query_seq_pairs(quoter, query.items())) + elif isinstance(query, str): + query = self._QUERY_QUOTER(query) + elif isinstance(query, (bytes, bytearray, memoryview)): + raise TypeError( + "Invalid query type: bytes, bytearray and memoryview are forbidden" + ) + elif isinstance(query, Sequence): + quoter = self._QUERY_PART_QUOTER + # We don't expect sequence values if we're given a list of pairs + # already; only mappings like builtin `dict` which can't have the + # same key pointing to multiple values are allowed to use + # `_query_seq_pairs`. + query = "&".join( + quoter(k) + "=" + quoter(self._query_var(v)) for k, v in query + ) + else: + raise TypeError( + "Invalid query type: only str, mapping or " + "sequence of (key, value) pairs is allowed" + ) + + return query + + def with_query(self, *args, **kwargs): + """Return a new URL with query part replaced. + + Accepts any Mapping (e.g. dict, multidict.MultiDict instances) + or str, autoencode the argument if needed. + + A sequence of (key, value) pairs is supported as well. + + It also can take an arbitrary number of keyword arguments. + + Clear query if None is passed. + + """ + # N.B. doesn't cleanup query/fragment + + new_query = self._get_str_query(*args, **kwargs) or "" + return URL( + self._val._replace(path=self._val.path, query=new_query), encoded=True + ) + + def update_query(self, *args, **kwargs): + """Return a new URL with query part updated.""" + s = self._get_str_query(*args, **kwargs) + query = None + if s is not None: + new_query = MultiDict(parse_qsl(s, keep_blank_values=True)) + query = MultiDict(self.query) + query.update(new_query) + + return URL( + self._val._replace(query=self._get_str_query(query) or ""), encoded=True + ) + + def with_fragment(self, fragment): + """Return a new URL with fragment replaced. + + Autoencode fragment if needed. + + Clear fragment to default if None is passed. + + """ + # N.B. doesn't cleanup query/fragment + if fragment is None: + raw_fragment = "" + elif not isinstance(fragment, str): + raise TypeError("Invalid fragment type") + else: + raw_fragment = self._FRAGMENT_QUOTER(fragment) + if self.raw_fragment == raw_fragment: + return self + return URL(self._val._replace(fragment=raw_fragment), encoded=True) + + def with_name(self, name): + """Return a new URL with name (last part of path) replaced. + + Query and fragment parts are cleaned up. + + Name is encoded if needed. + + """ + # N.B. DOES cleanup query/fragment + if not isinstance(name, str): + raise TypeError("Invalid name type") + if "/" in name: + raise ValueError("Slash in name is not allowed") + name = self._PATH_QUOTER(name) + if name in (".", ".."): + raise ValueError(". and .. values are forbidden") + parts = list(self.raw_parts) + if self.is_absolute(): + if len(parts) == 1: + parts.append(name) + else: + parts[-1] = name + parts[0] = "" # replace leading '/' + else: + parts[-1] = name + if parts[0] == "/": + parts[0] = "" # replace leading '/' + return URL( + self._val._replace(path="/".join(parts), query="", fragment=""), + encoded=True, + ) + + def with_suffix(self, suffix): + """Return a new URL with suffix (file extension of name) replaced. + + Query and fragment parts are cleaned up. + + suffix is encoded if needed. + """ + if not isinstance(suffix, str): + raise TypeError("Invalid suffix type") + if suffix and not suffix.startswith(".") or suffix == ".": + raise ValueError(f"Invalid suffix {suffix!r}") + name = self.raw_name + if not name: + raise ValueError(f"{self!r} has an empty name") + old_suffix = self.raw_suffix + if not old_suffix: + name = name + suffix + else: + name = name[: -len(old_suffix)] + suffix + return self.with_name(name) + + def join(self, url): + """Join URLs + + Construct a full (“absolute”) URL by combining a “base URL” + (self) with another URL (url). + + Informally, this uses components of the base URL, in + particular the addressing scheme, the network location and + (part of) the path, to provide missing components in the + relative URL. + + """ + # See docs for urllib.parse.urljoin + if not isinstance(url, URL): + raise TypeError("url should be URL") + return URL(urljoin(str(self), str(url)), encoded=True) + + def joinpath(self, *other, encoded=False): + """Return a new URL with the elements in other appended to the path.""" + return self._make_child(other, encoded=encoded) + + def human_repr(self): + """Return decoded human readable string for URL representation.""" + user = _human_quote(self.user, "#/:?@[]") + password = _human_quote(self.password, "#/:?@[]") + host = self.host + if host: + host = self._encode_host(self.host, human=True) + path = _human_quote(self.path, "#?") + query_string = "&".join( + "{}={}".format(_human_quote(k, "#&+;="), _human_quote(v, "#&+;=")) + for k, v in self.query.items() + ) + fragment = _human_quote(self.fragment, "") + return urlunsplit( + SplitResult( + self.scheme, + self._make_netloc( + user, + password, + host, + self._val.port, + encode_host=False, + ), + path, + query_string, + fragment, + ) + ) + + +def _human_quote(s, unsafe): + if not s: + return s + for c in "%" + unsafe: + if c in s: + s = s.replace(c, f"%{ord(c):02X}") + if s.isprintable(): + return s + return "".join(c if c.isprintable() else quote(c) for c in s) + + +_MAXCACHE = 256 + + +@functools.lru_cache(_MAXCACHE) +def _idna_decode(raw): + try: + return idna.decode(raw.encode("ascii")) + except UnicodeError: # e.g. '::1' + return raw.encode("ascii").decode("idna") + + +@functools.lru_cache(_MAXCACHE) +def _idna_encode(host): + try: + return idna.encode(host, uts46=True).decode("ascii") + except UnicodeError: + return host.encode("idna").decode("ascii") + + +@rewrite_module +def cache_clear(): + _idna_decode.cache_clear() + _idna_encode.cache_clear() + + +@rewrite_module +def cache_info(): + return { + "idna_encode": _idna_encode.cache_info(), + "idna_decode": _idna_decode.cache_info(), + } + + +@rewrite_module +def cache_configure(*, idna_encode_size=_MAXCACHE, idna_decode_size=_MAXCACHE): + global _idna_decode, _idna_encode + + _idna_encode = functools.lru_cache(idna_encode_size)(_idna_encode.__wrapped__) + _idna_decode = functools.lru_cache(idna_decode_size)(_idna_decode.__wrapped__) diff --git a/yarl/py.typed b/yarl/py.typed new file mode 100644 index 0000000..dcf2c80 --- /dev/null +++ b/yarl/py.typed @@ -0,0 +1 @@ +# Placeholder diff --git a/zeit.py b/zeit.py new file mode 100644 index 0000000..534058f --- /dev/null +++ b/zeit.py @@ -0,0 +1,98 @@ +import sys +import serial +import logging +import konfig +import time +from mysql.connector import connect, Error +from time import sleep + +Log_Format = "%(levelname)s %(module)s:%(lineno)d %(asctime)s - %(message)s" +logging.basicConfig(stream=sys.stdout, format = Log_Format, level=logging.INFO) +logger = logging.getLogger() +comeTimeID = 0 +def checksum(data): + checksum = 0; + len = int(data[1:3]) + if(len < 99): + for i in range(1, len+3): + checksum = checksum ^ ord(data[i:i+1]) + #print(str(i)+":"+str(data[i:i+1])+" "+hex(ord(data[i:i+1]))) + return checksum & 0xFF; + + +def writeCmd(cmd,data): + length = str(len(data)+1).zfill(2) + msg = '#'+ length + cmd + data + cs = str(checksum(str.encode(msg))).zfill(3) + #print("Len: "+str(len(data)+1)) + #print("MEssage: "+msg+" CS: "+cs) + con.write(str.encode(msg+cs+"\n")) + +def getMaID(id): + comeTimeID = 0 + try: + with connect(**konfig.datenbank()) as connection: + with connection.cursor() as cursor: + cursor.execute("SELECT id FROM user WHERE rfid = '"+str(id[0:8])+"';") + maid = cursor.fetchone() + connection.commit() + if maid == None: + writeCmd('M',"NeuerTag "+str(id[0:8])) + logger.warning("Employee not found rfID: %s",str(id[0:8])) + else: + cursor.execute("SELECT id FROM zeiten WHERE ma_id = "+maid[0]+" AND DATE(kommen) = DATE(NOW()) AND gehen = '1990-01-01 00:00:00';") + cometime = cursor.fetchone() + connection.commit() + if cometime == None: + writeCmd('C',"") + cursor.execute("INSERT INTO zeiten (kommen, gehen, ma_id) VALUES (NOW(), '1990-01-01 00:00:00', "+maid[0]+")") + connection.commit() + else: + writeCmd('G',"") + return str(cometime[0]) + + except: + writeCmd('M',"Datenbankverbindung\n\rfehlgeschlagen") + logger.warning("DB connection failed!") + + +#con = serial.Serial("COM7",115200,timeout=2,parity=serial.PARITY_NONE, dsrdtr=False) +#con.write('#') +#con.write('\n') +#sleep(2) +#writeCmd("T",str(int(time.time()))) +#print(con.read_all()) +con = None +while True: + try: + if(con == None): + con = serial.Serial("COM7",115200,timeout=2,parity=serial.PARITY_NONE, dsrdtr=False) + print("Reconnecting") + sleep(2) + writeCmd("T",str(int(time.time()))) + line=con.readline() + if len(line) > 10: + logger.info("MSG: "+str(line)+" "+str(line[3:4])) + if(line[3:4] == b'I'): + logger.info("TAG!") + cometimeID = getMaID(line[4:4+8]) + elif(line[3:4] == b'P'): + logger.info("PAUSE!") + pause = str(line(4,4+3)) + if(cometimeID != None): + try: + with connect(**konfig.datenbank()) as connection: + with connection.cursor() as cursor: + cursor.execute("UPDATE zeiten SET gehen = NOW(), pause_mins="+pause+" WHERE id = "+cometimeID) + maid = cursor.fetchone() + connection.commit() + writeCmd('P',"") + except: + writeCmd('M',"Eintragen\n\rfehlgeschlagen") + logger.warning("Could not update entry with Go time") + except: + if(not(con == None)): + con.close() + con = None + logger.warning("Disconnecting") + time.sleep(2)

    +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 literal 0 HcmV?d00001 diff --git a/dateutil/zoneinfo/rebuild.py b/dateutil/zoneinfo/rebuild.py new file mode 100644 index 0000000..684c658 --- /dev/null +++ b/dateutil/zoneinfo/rebuild.py @@ -0,0 +1,75 @@ +import logging +import os +import tempfile +import shutil +import json +from subprocess import check_call, check_output +from tarfile import TarFile + +from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME + + +def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None): + """Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar* + + filename is the timezone tarball from ``ftp.iana.org/tz``. + + """ + tmpdir = tempfile.mkdtemp() + zonedir = os.path.join(tmpdir, "zoneinfo") + moduledir = os.path.dirname(__file__) + try: + with TarFile.open(filename) as tf: + for name in zonegroups: + tf.extract(name, tmpdir) + filepaths = [os.path.join(tmpdir, n) for n in zonegroups] + + _run_zic(zonedir, filepaths) + + # write metadata file + with open(os.path.join(zonedir, METADATA_FN), 'w') as f: + json.dump(metadata, f, indent=4, sort_keys=True) + target = os.path.join(moduledir, ZONEFILENAME) + with TarFile.open(target, "w:%s" % format) as tf: + for entry in os.listdir(zonedir): + entrypath = os.path.join(zonedir, entry) + tf.add(entrypath, entry) + finally: + shutil.rmtree(tmpdir) + + +def _run_zic(zonedir, filepaths): + """Calls the ``zic`` compiler in a compatible way to get a "fat" binary. + + Recent versions of ``zic`` default to ``-b slim``, while older versions + don't even have the ``-b`` option (but default to "fat" binaries). The + current version of dateutil does not support Version 2+ TZif files, which + causes problems when used in conjunction with "slim" binaries, so this + function is used to ensure that we always get a "fat" binary. + """ + + try: + help_text = check_output(["zic", "--help"]) + except OSError as e: + _print_on_nosuchfile(e) + raise + + if b"-b " in help_text: + bloat_args = ["-b", "fat"] + else: + bloat_args = [] + + check_call(["zic"] + bloat_args + ["-d", zonedir] + filepaths) + + +def _print_on_nosuchfile(e): + """Print helpful troubleshooting message + + e is an exception raised by subprocess.check_call() + + """ + if e.errno == 2: + logging.error( + "Could not find zic. Perhaps you need to install " + "libc-bin or some other package that provides it, " + "or it's not in your PATH?") diff --git a/frozenlist/__init__.py b/frozenlist/__init__.py new file mode 100644 index 0000000..1523565 --- /dev/null +++ b/frozenlist/__init__.py @@ -0,0 +1,95 @@ +import os +import sys +import types +from collections.abc import MutableSequence +from functools import total_ordering +from typing import Type + +__version__ = "1.4.0" + +__all__ = ("FrozenList", "PyFrozenList") # type: Tuple[str, ...] + + +NO_EXTENSIONS = bool(os.environ.get("FROZENLIST_NO_EXTENSIONS")) # type: bool + + +@total_ordering +class FrozenList(MutableSequence): + __slots__ = ("_frozen", "_items") + + if sys.version_info >= (3, 9): + __class_getitem__ = classmethod(types.GenericAlias) + else: + + @classmethod + def __class_getitem__(cls: Type["FrozenList"]) -> Type["FrozenList"]: + return cls + + def __init__(self, items=None): + self._frozen = False + if items is not None: + items = list(items) + else: + items = [] + self._items = items + + @property + def frozen(self): + return self._frozen + + def freeze(self): + self._frozen = True + + def __getitem__(self, index): + return self._items[index] + + def __setitem__(self, index, value): + if self._frozen: + raise RuntimeError("Cannot modify frozen list.") + self._items[index] = value + + def __delitem__(self, index): + if self._frozen: + raise RuntimeError("Cannot modify frozen list.") + del self._items[index] + + def __len__(self): + return self._items.__len__() + + def __iter__(self): + return self._items.__iter__() + + def __reversed__(self): + return self._items.__reversed__() + + def __eq__(self, other): + return list(self) == other + + def __le__(self, other): + return list(self) <= other + + def insert(self, pos, item): + if self._frozen: + raise RuntimeError("Cannot modify frozen list.") + self._items.insert(pos, item) + + def __repr__(self): + return f"" + + def __hash__(self): + if self._frozen: + return hash(tuple(self)) + else: + raise RuntimeError("Cannot hash unfrozen list.") + + +PyFrozenList = FrozenList + + +try: + from ._frozenlist import FrozenList as CFrozenList # type: ignore + + if not NO_EXTENSIONS: # pragma: no cover + FrozenList = CFrozenList # type: ignore +except ImportError: # pragma: no cover + pass diff --git a/frozenlist/__init__.pyi b/frozenlist/__init__.pyi new file mode 100644 index 0000000..ae803ef --- /dev/null +++ b/frozenlist/__init__.pyi @@ -0,0 +1,47 @@ +from typing import ( + Generic, + Iterable, + Iterator, + List, + MutableSequence, + Optional, + TypeVar, + Union, + overload, +) + +_T = TypeVar("_T") +_Arg = Union[List[_T], Iterable[_T]] + +class FrozenList(MutableSequence[_T], Generic[_T]): + def __init__(self, items: Optional[_Arg[_T]] = None) -> None: ... + @property + def frozen(self) -> bool: ... + def freeze(self) -> None: ... + @overload + def __getitem__(self, i: int) -> _T: ... + @overload + def __getitem__(self, s: slice) -> FrozenList[_T]: ... + @overload + def __setitem__(self, i: int, o: _T) -> None: ... + @overload + def __setitem__(self, s: slice, o: Iterable[_T]) -> None: ... + @overload + def __delitem__(self, i: int) -> None: ... + @overload + def __delitem__(self, i: slice) -> None: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[_T]: ... + def __reversed__(self) -> Iterator[_T]: ... + def __eq__(self, other: object) -> bool: ... + def __le__(self, other: FrozenList[_T]) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __lt__(self, other: FrozenList[_T]) -> bool: ... + def __ge__(self, other: FrozenList[_T]) -> bool: ... + def __gt__(self, other: FrozenList[_T]) -> bool: ... + def insert(self, pos: int, item: _T) -> None: ... + def __repr__(self) -> str: ... + def __hash__(self) -> int: ... + +# types for C accelerators are the same +CFrozenList = PyFrozenList = FrozenList diff --git a/frozenlist/_frozenlist.pyx b/frozenlist/_frozenlist.pyx new file mode 100644 index 0000000..9ee846c --- /dev/null +++ b/frozenlist/_frozenlist.pyx @@ -0,0 +1,123 @@ +import sys +import types +from collections.abc import MutableSequence + + +cdef class FrozenList: + + if sys.version_info >= (3, 9): + __class_getitem__ = classmethod(types.GenericAlias) + else: + @classmethod + def __class_getitem__(cls): + return cls + + cdef readonly bint frozen + cdef list _items + + def __init__(self, items=None): + self.frozen = False + if items is not None: + items = list(items) + else: + items = [] + self._items = items + + cdef object _check_frozen(self): + if self.frozen: + raise RuntimeError("Cannot modify frozen list.") + + cdef inline object _fast_len(self): + return len(self._items) + + def freeze(self): + self.frozen = True + + def __getitem__(self, index): + return self._items[index] + + def __setitem__(self, index, value): + self._check_frozen() + self._items[index] = value + + def __delitem__(self, index): + self._check_frozen() + del self._items[index] + + def __len__(self): + return self._fast_len() + + def __iter__(self): + return self._items.__iter__() + + def __reversed__(self): + return self._items.__reversed__() + + def __richcmp__(self, other, op): + if op == 0: # < + return list(self) < other + if op == 1: # <= + return list(self) <= other + if op == 2: # == + return list(self) == other + if op == 3: # != + return list(self) != other + if op == 4: # > + return list(self) > other + if op == 5: # => + return list(self) >= other + + def insert(self, pos, item): + self._check_frozen() + self._items.insert(pos, item) + + def __contains__(self, item): + return item in self._items + + def __iadd__(self, items): + self._check_frozen() + self._items += list(items) + return self + + def index(self, item): + return self._items.index(item) + + def remove(self, item): + self._check_frozen() + self._items.remove(item) + + def clear(self): + self._check_frozen() + self._items.clear() + + def extend(self, items): + self._check_frozen() + self._items += list(items) + + def reverse(self): + self._check_frozen() + self._items.reverse() + + def pop(self, index=-1): + self._check_frozen() + return self._items.pop(index) + + def append(self, item): + self._check_frozen() + return self._items.append(item) + + def count(self, item): + return self._items.count(item) + + def __repr__(self): + return ''.format(self.frozen, + self._items) + + def __hash__(self): + if self.frozen: + return hash(tuple(self._items)) + else: + raise RuntimeError("Cannot hash unfrozen list.") + + +MutableSequence.register(FrozenList) diff --git a/frozenlist/py.typed b/frozenlist/py.typed new file mode 100644 index 0000000..f5642f7 --- /dev/null +++ b/frozenlist/py.typed @@ -0,0 +1 @@ +Marker diff --git a/gatherDTUBIData.py b/gatherDTUBIData.py new file mode 100644 index 0000000..d498d2c --- /dev/null +++ b/gatherDTUBIData.py @@ -0,0 +1,59 @@ +import json +from hoymiles_wifi.dtu import DTU +import asyncio +import aiohttp +import logging +import math +import time +from typing import List +from dataclasses import dataclass +from dataclasses import field +UPDATE_INTERVAL = 31 +_LOGGER = logging.getLogger(__name__) + +dtu = DTU("192.168.179.184") + +@dataclass +class DTUBIInvData: + p_PV:List[float] = field(default_factory=lambda: [0.0,0.0]) + p_AC:float = 0.0 + temp:float = 0.0 + error:int = 0 + name:str = "" + +@dataclass +class DTUBIData: + inverter:List[DTUBIInvData] = field(default_factory=lambda: [DTUBIInvData()])#default_factory=lambda: [DTUInvData(), DTUInvData(),DTUInvData(),DTUInvData(),DTUInvData(),DTUInvData()]) + p_AC_sum:float = 0 + error = 0 + + +ret = DTUBIData() + +ret.lastUpdate = time.clock_gettime(0)-UPDATE_INTERVAL +ret.inverter[0].name = "Veranda UG2" +async def gatherData() -> DTUBIData: + if(time.clock_gettime(0) >= ret.lastUpdate + UPDATE_INTERVAL): + ret.lastUpdate = time.clock_gettime(0) + #print("get charger status..."); + try: + state = await dtu.async_get_real_data_new() + #logging.warning(html) + #state = json.load(html) + ret.inverter[0].error = 0 + ret.inverter[0].p_PV[0] = state.pv_data[0].power/10 + ret.inverter[0].p_PV[1] = state.pv_data[1].power/10 + ret.inverter[0].p_AC = state.sgs_data[0].active_power/10 + ret.p_AC_sum = ret.inverter[0].p_AC + ret.inverter[0].temp = state.sgs_data[0].temperature/10 + + except: + if(ret.inverter[0].error > 1): + ret.p_AC_sum = 0 + ret.inverter[0].p_PV[0] = 0 + ret.inverter[0].p_PV[1] = 0 + ret.inverter[0].temp = 0 + ret.inverter[0].p_AC = 0 + else: + ret.inverter[0].error += 1 + return ret \ No newline at end of file diff --git a/gatherHeaterData.py b/gatherHeaterData.py new file mode 100644 index 0000000..bf4170e --- /dev/null +++ b/gatherHeaterData.py @@ -0,0 +1,294 @@ +import json +import asyncio +import aiohttp +import logging +import math +from typing import List +from dataclasses import dataclass +from dataclasses import field + +_LOGGER = logging.getLogger(__name__) + +@dataclass +class HeaterData: + t_buffT:float = 0 + t_buffM:float = 0 + t_buffB:float = 0 + t_heatVL:float = 0 + t_heatRL:float = 0 + t_gasVLu:float = 0 + t_gasVLo:float = 0 + t_gasRL:float = 0 + t_fbVL:float = 0 + t_fbRL:float = 0 + t_triac:float = 0 + p_heat:float = 0 + error:int = 0 + mode:str = "" + +ret = HeaterData() + +async def gatherData() -> HeaterData: + + #print("get charger status..."); + timeout = aiohttp.ClientTimeout(total=3) + try: + async with aiohttp.ClientSession() as session: + async with session.get('http://192.168.179.169/data',timeout=timeout) as response: #?filter=psm,fup,amp,frc,nrg,lmo,car + if response.status == 200: + html = await response.text() + #logging.warning(html) + state = json.loads(html) + ret.error = 0 + ret.t_buffT = state["PufferO"] + ret.t_buffM = state["PufferM"] + ret.t_buffB = state["PufferU"] + ret.t_heatVL = state["HeaterVL"] + ret.t_heatRL = state["HeaterRL"] + ret.t_gasVLu = state["ThermeVLu"] + ret.t_gasVLo = state["ThermeVLo"] + ret.t_gasRL = state["ThermeRL"] + ret.t_triac = state["Triac"] + ret.t_fbVL = state["fbVL"] + ret.t_fbRL = state["fbRL"] + ret.p_heat = state["heatPower"] + ret.mode = state["mode"] + except: + if(ret.error > 5): + ret.p_heat = 0 + _LOGGER.warning("heater data could not be fetched") + else: + ret.error += 1 + return ret + +#async def main(): +# await gatherNeededStatus() + +#loop = asyncio.get_event_loop() +#loop.run_until_complete(main()) + + +""" +{ + "inverters":[ + { + "serial":"116491626890", + "name":"Terrassendach", + "order":0, + "data_age":1, + "poll_enabled":true, + "reachable":true, + "producing":true, + "limit_relative":100, + "limit_absolute":1600, + "AC":{ + "0":{ + "Power":{ + "v":74.09999847, + "u":"W", + "d":1 + }, + "Voltage":{ + "v":235.3000031, + "u":"V", + "d":1 + }, + "Current":{ + "v":0.310000002, + "u":"A", + "d":2 + }, + "Power DC":{ + "v":77.80000305, + "u":"W", + "d":1 + }, + "YieldDay":{ + "v":179, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":8.732000351, + "u":"kWh", + "d":3 + }, + "Frequency":{ + "v":50.00999832, + "u":"Hz", + "d":2 + }, + "PowerFactor":{ + "v":1, + "u":"", + "d":3 + }, + "ReactivePower":{ + "v":0, + "u":"var", + "d":1 + }, + "Efficiency":{ + "v":95.24420929, + "u":"%", + "d":3 + } + } + }, + "DC":{ + "0":{ + "name":{ + "u":"" + }, + "Power":{ + "v":12.80000019, + "u":"W", + "d":1 + }, + "Voltage":{ + "v":21.20000076, + "u":"V", + "d":1 + }, + "Current":{ + "v":0.610000014, + "u":"A", + "d":2 + }, + "YieldDay":{ + "v":31, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":1.710000038, + "u":"kWh", + "d":3 + } + }, + "1":{ + "name":{ + "u":"" + }, + "Power":{ + "v":26.60000038, + "u":"W", + "d":1 + }, + "Voltage":{ + "v":43.20000076, + "u":"V", + "d":1 + }, + "Current":{ + "v":0.610000014, + "u":"A", + "d":2 + }, + "YieldDay":{ + "v":62, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":2.496000051, + "u":"kWh", + "d":3 + } + }, + "2":{ + "name":{ + "u":"" + }, + "Power":{ + "v":12.5, + "u":"W", + "d":1 + }, + "Voltage":{ + "v":21.20000076, + "u":"V", + "d":1 + }, + "Current":{ + "v":0.589999974, + "u":"A", + "d":2 + }, + "YieldDay":{ + "v":26, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":1.179999948, + "u":"kWh", + "d":3 + } + }, + "3":{ + "name":{ + "u":"" + }, + "Power":{ + "v":25.89999962, + "u":"W", + "d":1 + }, + "Voltage":{ + "v":43, + "u":"V", + "d":1 + }, + "Current":{ + "v":0.600000024, + "u":"A", + "d":2 + }, + "YieldDay":{ + "v":60, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":3.345999956, + "u":"kWh", + "d":3 + } + } + }, + "INV":{ + "0":{ + "Temperature":{ + "v":12.5, + "u":"°C", + "d":1 + } + } + }, + "events":1 + } + ], + "total":{ + "Power":{ + "v":74.09999847, + "u":"W", + "d":1 + }, + "YieldDay":{ + "v":179, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":8.732000351, + "u":"kWh", + "d":3 + } + }, + "hints":{ + "time_sync":false, + "radio_problem":false, + "default_password":true + } +}""" \ No newline at end of file diff --git a/gatherModbusData.py b/gatherModbusData.py new file mode 100644 index 0000000..f29ac94 --- /dev/null +++ b/gatherModbusData.py @@ -0,0 +1,561 @@ +import asyncio +import json +import aiohttp +import sys +import logging +import sunspec2.modbus.client as client +import datetime +from typing import List +from dataclasses import dataclass +from dataclasses import field +from operator import attrgetter +from time import sleep +import traceback + +_LOGGER = logging.getLogger(__name__) + + +batStat = { + 1: "OFF", + 2: "EMPTY", + 3: "DISCHARGING", + 4: "CHARGING", + 5: "FULL", + 6: "HOLDING", + 7: "TESTING", +} +wrStat = { + 1: "OFF", + 2: "SLEEPING", + 3: "STARTING", + 4: "MPPT", + 5: "THROTTLED", + 6: "SHUTTING_DOWN", + 7: "FAULT", +} + +@dataclass +class ModbusRegisters: + ppv0_reg = "" + ppv1_reg = "" + pbat_crg_reg = "" + pbat_discrg_reg = "" + ubat_reg = "" + ibat_crg_reg = "" + ibat_discrg_reg = "" + pACinv_reg = "" + soc_reg = "" + battStat_reg = "" + wrStat_reg = "" + + pACevu_reg = "" + pL1ACevu_reg = "" + pL2ACevu_reg = "" + pL3ACevu_reg = "" + iL1evu_reg = "" + iL2evu_reg = "" + iL3evu_reg = "" + + pACog_reg = "" + pL1ACog_reg = "" + pL2ACog_reg = "" + pL3ACog_reg = "" + iL1og_reg = "" + iL2og_reg = "" + iL3og_reg = "" + + limitChrgRate = "" + setChargeLimits = "" + + setPowerLimitPercent = "" + setPowerLimitEN = "" + + + +@dataclass +class InverterData: + p_PV:List[float] = field(default_factory=lambda: [0.0,0.0]) + error:int = 0 + load: float = 0.0 + soc: float = 0.0 + pbat: float = 0.0 + ppv: float = 0.0 + ppv2: float = 0.0 + aut: float = 0.0 + pgrid: float = 0.0 + ibatt: float = 0.0 + ubatt: float = 0.0 + temp: float = 0.0 + p_l1evu: float = 0.0 + p_l2evu: float = 0.0 + p_l3evu: float = 0.0 + p_l1og: float = 0.0 + p_l2og: float = 0.0 + p_l3og: float = 0.0 + i_l1evu: float = 0.0 + i_l2evu: float = 0.0 + i_l3evu: float = 0.0 + i_l1og: float = 0.0 + i_l2og: float = 0.0 + i_l3og : float = 0.0 + p_wr : float = 0.0 + crgMaxPct : float = 0.0 + wr_status : str = "" + batt_status : str = "" + pwrMaxPct : float = 0.0 + p_AC : float = 0.0 + +@dataclass +class estimation: + estProd = 0.1 + increaseTmr = 0 + +try: + inv = client.SunSpecModbusClientDeviceTCP(1, "192.168.179.155", 1502) + meter0 = client.SunSpecModbusClientDeviceTCP(200, "192.168.179.155", 1502) + meter1 = client.SunSpecModbusClientDeviceTCP(201, "192.168.179.155", 1502) + regs = ModbusRegisters() + estimations = estimation() +except client.SunSpecModbusClientError as e: + _LOGGER.error('Modbus TCP Error: %s' % e) + _LOGGER.erorr('trace: %s' % traceback.print_exc()) + sys.exit(1) + + + +async def connect(): + if inv is not None and meter0 is not None and meter1 is not None: + print('Scanning: ') + # print( '\nTimestamp: %s' % (time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()))) + + # read all models in the device + inv.scan() + meter0.scan() + meter1.scan() + #_LOGGER.debug(inv.get_text()) + if(inv.common[0].Md.value == "Symo GEN24 10.0"): + regs.ppv0_reg = "inv.mppt[0].module[0].DCW" + regs.ppv1_reg = "inv.mppt[0].module[1].DCW" + regs.pbat_crg_reg = "inv.mppt[0].module[2].DCW" + regs.pbat_discrg_reg = "inv.mppt[0].module[3].DCW" + regs.ubat_reg = "inv.mppt[0].module[3].DCV" + regs.ibat_crg_reg = "inv.mppt[0].module[2].DCA" + regs.ibat_discrg_reg = "inv.mppt[0].module[3].DCA" + regs.pACinv_reg = "inv.inverter_three_phase[0].W" + regs.tmpCab_reg = "inv.inverter_three_phase[0].TmpCab" + regs.soc_reg = "inv.storage_basic[0].ChaState" + regs.battStat_reg = "inv.storage_basic[0].ChaSt" + regs.wrStat_reg = "inv.inverter_three_phase[0].St" + + regs.limitChrgRate = "inv.storage_basic[0].InWRte" + regs.setChargeLimits = "inv.storage_basic[0].StorCtl_Mod" + + regs.setPowerLimitPercent = "inv.controls[0].WMaxLimPct" + regs.setPowerLimitEN = "inv.controls[0].WMaxLim_Ena" + + regs.pACevu_reg = "meter0.ac_meter_abcn[0].W" + regs.pL1ACevu_reg = "meter0.ac_meter_abcn[0].WphA" + regs.pL2ACevu_reg = "meter0.ac_meter_abcn[0].WphB" + regs.pL3ACevu_reg = "meter0.ac_meter_abcn[0].WphC" + regs.iL1evu_reg = "meter0.ac_meter_abcn[0].AphA" + regs.iL2evu_reg = "meter0.ac_meter_abcn[0].AphB" + regs.iL3evu_reg = "meter0.ac_meter_abcn[0].AphC" + + regs.pACog_reg = "meter1.ac_meter_abcn[0].W" + regs.pL1ACog_reg = "meter1.ac_meter_abcn[0].WphA" + regs.pL2ACog_reg = "meter1.ac_meter_abcn[0].WphB" + regs.pL3ACog_reg = "meter1.ac_meter_abcn[0].WphC" + regs.iL1og_reg = "meter1.ac_meter_abcn[0].AphA" + regs.iL2og_reg = "meter1.ac_meter_abcn[0].AphB" + regs.iL3og_reg = "meter1.ac_meter_abcn[0].AphC" + else: + _LOGGER.error('undefined Inverter, please define inverter registers!') + sys.exit(1) + else: + _LOGGER.error('One of the expected modbus devices was unreachable') + sys.exit(1) + #while True: + +async def get_runtime_data(estProduction, sunset) -> InverterData: + ret = InverterData() + try: + inv.mppt[0].read() + inv.inverter_three_phase[0].read() + inv.storage_basic[0].read() + meter0.ac_meter_abcn[0].read() + meter1.ac_meter_abcn[0].read() + #print(inv.controls[0].__dir__()) + ret.p_PV[0] = eval(regs.ppv0_reg).cvalue + ret.p_PV[1] = eval(regs.ppv1_reg).cvalue + ret.error = 0 + ret.load = eval(regs.pACevu_reg).cvalue + eval(regs.pbat_discrg_reg).cvalue + eval(regs.ppv0_reg).cvalue + eval(regs.ppv1_reg).cvalue + ret.soc = eval(regs.soc_reg).cvalue + ret.pbat = eval(regs.pbat_discrg_reg).cvalue + if(ret.pbat == 0): + ret.pbat = -eval(regs.pbat_crg_reg).cvalue + ret.crgMaxPct = (eval(regs.limitChrgRate).cvalue)*2.04 #references max charge of batt. (20,48kw) instead of INV (10kw) + ret.pwrMaxPct = eval(regs.setPowerLimitPercent).cvalue #references max charge of batt. (20,48kw) instead of INV (10kw) + ret.ppv = ret.p_PV[0] + ret.ppv2 = ret.p_PV[1] + ret.pgrid = eval(regs.pACevu_reg).cvalue + if(ret.pgrid < 0): + ret.aut = 100 + else: + ret.aut = 100/ret.load * ret.pgrid + if(ret.aut < 0): + ret.aut=0 + elif(ret.aut > 100): + ret.aut = 100 + ret.ibatt = eval(regs.ibat_discrg_reg).cvalue + if(ret.ibatt == 0): + ret.ibatt = -eval(regs.ibat_crg_reg).cvalue + ret.ubatt = eval(regs.ubat_reg).cvalue + ret.p_l1evu = eval(regs.pL1ACevu_reg).cvalue + ret.p_l2evu = eval(regs.pL2ACevu_reg).cvalue + ret.p_l3evu = eval(regs.pL3ACevu_reg).cvalue + ret.p_l1og = eval(regs.pL1ACog_reg).cvalue + ret.p_l2og = eval(regs.pL2ACog_reg).cvalue + ret.p_l3og = eval(regs.pL3ACog_reg).cvalue + ret.i_l1evu = eval(regs.iL1evu_reg).cvalue + ret.i_l2evu = eval(regs.iL2evu_reg).cvalue + ret.i_l3evu = eval(regs.iL3evu_reg).cvalue + ret.i_l1og = eval(regs.iL1og_reg).cvalue + ret.i_l2og = eval(regs.iL2og_reg).cvalue + ret.i_l3og = eval(regs.iL3og_reg).cvalue + ret.p_AC = eval(regs.pACinv_reg).cvalue + ret.p_wr = ret.ppv + ret.ppv2 + ret.pbat - ret.p_AC + ret.temp = 0#no need to read reg, reads always null ... eval(regs.tmpCab_reg).cvalue + ret.batt_status = batStat[eval(regs.battStat_reg).cvalue] + ret.wr_status = wrStat[eval(regs.wrStat_reg).cvalue] + except Exception as error: + _LOGGER.warning(f"No modbus data received from Inverter: %s",error) + #_LOGGER.erorr('trace: %s' % traceback.print_exc()) + try: + inv.connect() #reconnect the modbus client + meter0.connect() #reconnect the modbus client + meter1.connect() #reconnect the modbus client + _LOGGER.warning(f"Tried to reconnect") + except: + _LOGGER.warning(f"reconnect failed...") + +# if(ret.soc > 90 and (ret.i_l1evu > 22 or ret.i_l2evu > 22 or ret.i_l3evu > 22)): +# eval(regs.setPowerLimitPercent).value = ret.pwrMaxPct*100 - 500; #divide by two because this references 20.5kw max charge of battery instead of 10kw from INV +# eval(regs.setPowerLimitEN).value = 1 #set charge rate: bit1 set discharge rate: bit 2 +# eval(regs.limitChrgRate).write() +# eval(regs.setChargeLimits).write() +# elif (ret.i_l1evu < 18 and ret.i_l2evu < 18 and ret.i_l3evu < 18 and ret.pwrMaxPct < 100): +# eval(regs.setPowerLimitPercent).value = ret.pwrMaxPct*100 + 500; #divide by two because this references 20.5kw max charge of battery instead of 10kw from INV +# eval(regs.setPowerLimitEN).value = 1 #set charge rate: bit1 set discharge rate: bit 2 +# eval(regs.limitChrgRate).write() +# eval(regs.setChargeLimits).write() +# if datetime.datetime.now().hour == 23 and ret.pwrMaxPct < 99: #reset power limit every night +# eval(regs.setPowerLimitPercent).value = 10000; #divide by two because this references 20.5kw max charge of battery instead of 10kw from INV +# eval(regs.setPowerLimitEN).value = 1 #set charge rate: bit1 set discharge rate: bit 2 +# eval(regs.limitChrgRate).write() +# eval(regs.setChargeLimits).write() + try: + if(estProduction != estimations.estProd and datetime.datetime.now().hour < 9): + estimations.estProd = estProduction + estimations.increaseTmr = 0 + if(estimations.estProd > 35): + #_LOGGER.debug("High yeld predicted, reducing battery charge current") + if ret.soc > 45: #if SOC in the morning is above 45% wait with charging until midday + eval(regs.limitChrgRate).value = int(500/2.04) #divide by two because this references 20.5kw max charge of battery instead of 10kw from INV + else: #if less than 45% SOC in the morning charge slow (SOC dependant) until midday + eval(regs.limitChrgRate).value = int((5000 - ret.soc*100)/2.04) #divide by two because this references 20.5kw max charge of battery instead of 10kw from INV + eval(regs.setChargeLimits).value = 1 #set charge rate: bit1 set discharge rate: bit 2 + eval(regs.limitChrgRate).write() + eval(regs.setChargeLimits).write() + elif ret.crgMaxPct < 70: + eval(regs.limitChrgRate).value = int(8000/2.04) #set charge rate to 80% if no high yeld is predicted + eval(regs.setChargeLimits).value = 1 #set charge rate: bit1 set discharge rate: bit 2 + eval(regs.limitChrgRate).write() + eval(regs.setChargeLimits).write() + elif datetime.datetime.now().hour == 11 or datetime.datetime.now().hour == 12: #inclease charge rate at midday + estimations.increaseTmr = estimations.increaseTmr + 1 + if estimations.increaseTmr > 300: #slowdown rate change to 300 * 3sec (15 min.) + estimations.increaseTmr = 0 + maxMiddDayChrg = 30 + if(int(sunset.strftime('%H')) < 18): #inclrease max charge rate during wintertime, where time to full SOC is less. + maxMiddDayChrg = 70 + if ret.crgMaxPct < maxMiddDayChrg: + estimations.increaseTmr = 0 + eval(regs.limitChrgRate).value = int((ret.crgMaxPct*100+500)/2.04) #increase by 5% every 15 min --> reach 30% after 5*15min max + eval(regs.setChargeLimits).value = 1 #set charge rate: bit1 set discharge rate: bit 2 + eval(regs.limitChrgRate).write() + eval(regs.setChargeLimits).write() + _LOGGER.debug("Increasing charge rate to buffer feed peaks to " + str(int((ret.crgMaxPct*100+200)/2.04)) + "%") + if datetime.datetime.now().hour >= int(sunset.strftime('%H'))-4 and ret.soc < 70 and ret.crgMaxPct < 90: + eval(regs.limitChrgRate).value = int((10000)/2.04) + eval(regs.setChargeLimits).value = 1 #set charge rate: bit1 set discharge rate: bit 2 + eval(regs.limitChrgRate).write() + eval(regs.setChargeLimits).write() + _LOGGER.debug("Increasing charge rate because of too low SOC " + str(int((10000)/2.04)) + "%") + return ret + #print(ret.p_wr) + #print("-----") + #sleep(1) + except Exception as error: + _LOGGER.warning(f"No modbus data received from Inverter: %s",error) + +""" +Model: common (1) + + ID 1 + L 65 + Mn Fronius + Md Symo GEN24 10.0 + Opt None + Vr 1.34.2-1 + SN 33095286 + DA 1 + +Model: inverter_three_phase (103) + + ID 103 + L 50 + A 22300 A + AphA 8115 A + AphB 7527 A + AphC 6658 A + A_SF -5 + PPVphAB 4000 V + PPVphBC 4015 V + PPVphCA 4033 V + PhVphA 2310 V + PhVphB 2317 V + PhVphC 2328 V + V_SF -1 + W -5145 W + W_SF -2 + Hz 4998 Hz + Hz_SF -2 + VA 5145 VA + VA_SF -2 + VAr 31275 var + VAr_SF -5 + PF 1000 Pct + PF_SF -1 + WH 3939319098 Wh + WH_SF -2 + DCA None A + DCA_SF None + DCV None V + DCV_SF None + DCW 25076 W + DCW_SF -5 + TmpCab 526 C + TmpSnk None C + TmpTrns None C + TmpOt None C + Tmp_SF -1 + St 4 + StVnd 4 + Evt1 0 + Evt2 0 + EvtVnd1 0 + EvtVnd2 0 + EvtVnd3 None + EvtVnd4 None + +Model: nameplate (120) + + ID 120 + L 26 + DERTyp 82 + WRtg 1000 W + WRtg_SF 1 + VARtg 1000 VA + VARtg_SF 1 + VArRtgQ1 714 var + VArRtgQ2 714 var + VArRtgQ3 -714 var + VArRtgQ4 -714 var + VArRtg_SF 1 + ARtg 1443 A + ARtg_SF -2 + PFRtgQ1 -700 cos() + PFRtgQ2 700 cos() + PFRtgQ3 -700 cos() + PFRtgQ4 700 cos() + PFRtg_SF -3 + WHRtg 22118 Wh + WHRtg_SF 0 + AhrRtg None AH + AhrRtg_SF None + MaxChaRte 20480 W + MaxChaRte_SF 0 + MaxDisChaRte 20480 W + MaxDisChaRte_SF 0 + Pad 32768 + +Model: settings (121) + + ID 121 + L 30 + WMax 1000 W + VRef 231 V + VRefOfs 0 V + VMax None V + VMin None V + VAMax 1000 VA + VArMaxQ1 714 var + VArMaxQ2 714 var + VArMaxQ3 -714 var + VArMaxQ4 -714 var + WGra None % WMax/sec + PFMinQ1 -700 cos() + PFMinQ2 700 cos() + PFMinQ3 -700 cos() + PFMinQ4 700 cos() + VArAct None + ClcTotVA None + MaxRmpRte None % WGra + ECPNomHz None Hz + ConnPh None + WMax_SF 1 + VRef_SF 0 + VRefOfs_SF 0 + VMinMax_SF None + VAMax_SF 1 + VArMax_SF 1 + WGra_SF None + PFMin_SF -3 + MaxRmpRte_SF None + ECPNomHz_SF None + +Model: status (122) + + ID 122 + L 44 + PVConn 7 + StorConn 7 + ECPConn 1 + ActWh 39393190 Wh + ActVAh None VAh + ActVArhQ1 None varh + ActVArhQ2 None varh + ActVArhQ3 None varh + ActVArhQ4 None varh + VArAval None var + VArAval_SF None + WAval None var + WAval_SF None + StSetLimMsk None + StActCtl 0 + TmSrc RTC + Tms 791196551 Secs + RtSt None + Ris None ohms + Ris_SF None + +Model: controls (123) + + ID 123 + L 24 + Conn_WinTms 0 Secs + Conn_RvrtTms 0 Secs + Conn 1 + WMaxLimPct 10000 % WMax + WMaxLimPct_WinTms 0 Secs + WMaxLimPct_RvrtTms 0 Secs + WMaxLimPct_RmpTms 0 Secs + WMaxLim_Ena 0 + OutPFSet 1000 cos() + OutPFSet_WinTms 0 Secs + OutPFSet_RvrtTms 0 Secs + OutPFSet_RmpTms 0 Secs + OutPFSet_Ena 0 + VArWMaxPct None % WMax + VArMaxPct 100 % VArMax + VArAvalPct None % VArAval + VArPct_WinTms 0 Secs + VArPct_RvrtTms 0 Secs + VArPct_RmpTms 0 Secs + VArPct_Mod 2 + VArPct_Ena 0 + WMaxLimPct_SF -2 + OutPFSet_SF -3 + VArPct_SF 0 + +Model: mppt (160) + + ID 160 + L 88 + DCA_SF -4 + DCV_SF -2 + DCW_SF -1 + DCWH_SF -2 + Evt None + N 4 + TmsPer None + 01:ID 1 + 01:IDStr MPPT 1 + 01:DCA 22696 A + 01:DCV 55400 V + 01:DCW 12574 W + 01:DCWH 3156686802 Wh + 01:Tms 791196551 Secs + 01:Tmp None C + 01:DCSt None + 01:DCEvt None + 02:ID 2 + 02:IDStr MPPT 2 + 02:DCA 15510 A + 02:DCV 32689 V + 02:DCW 5070 W + 02:DCWH 989224180 Wh + 02:Tms 791196551 Secs + 02:Tmp None C + 02:DCSt None + 02:DCEvt None + 03:ID 3 + 03:IDStr StCha 3 + 03:DCA 42263 A + 03:DCV 41721 V + 03:DCW 17641 W + 03:DCWH 877835025 Wh + 03:Tms 791196551 Secs + 03:Tmp None C + 03:DCSt None + 03:DCEvt None + 04:ID 4 + 04:IDStr StDisCha 4 + 04:DCA 0 A + 04:DCV 41721 V + 04:DCW 0 W + 04:DCWH 824803735 Wh + 04:Tms 791196551 Secs + 04:Tmp None C + 04:DCSt None + 04:DCEvt None + +Model: storage_basic (124) + + ID 124 + L 24 + WChaMax 20480 W + WChaGra 100 % WChaMax/sec + WDisChaGra 100 % WChaMax/sec + StorCtl_Mod 0 + VAChaMax None VA + MinRsvPct 0 % WChaMax + ChaState 1240 % AhrRtg + StorAval None AH + InBatV None V + ChaSt 4 + OutWRte 10000 % WDisChaMax + InWRte 10000 % WChaMax + InOutWRte_WinTms None Secs + InOutWRte_RvrtTms 0 Secs + InOutWRte_RmpTms None Secs + ChaGriSet 1 + WChaMax_SF 0 + WChaDisChaGra_SF 0 + VAChaMax_SF None + MinRsvPct_SF -2 + ChaState_SF -2 + StorAval_SF None + InBatV_SF None + InOutWRte_SF -2 +""" \ No newline at end of file diff --git a/gatherOpenDTUData.py b/gatherOpenDTUData.py new file mode 100644 index 0000000..1c462b9 --- /dev/null +++ b/gatherOpenDTUData.py @@ -0,0 +1,371 @@ +import json +import asyncio +import aiohttp +import sys +import logging +import math +from typing import List +from dataclasses import dataclass +from dataclasses import field +import copy + + +_LOGGER = logging.getLogger(__name__) + +@dataclass +class DTUInvData: + p_PV:List[float] = field(default_factory=list) + p_AC:float = 0.0 + temp:float = 0.0 + error:int = 0 + name:str = "" + estimated:bool = False # Werte hochgerechnet statt gemessen + p_PV_est:float = 0.0 # davon geschaetzte DC-Leistung in W + limit:float = 0.0 # limit_absolute der DTU, W - Deckel der Schaetzung + +@dataclass +class DTUData: + inverter:List[DTUInvData] = field(default_factory=list)#default_factory=lambda: [DTUInvData(), DTUInvData(),DTUInvData(),DTUInvData(),DTUInvData(),DTUInvData()]) + p_AC_sum:float = 0 + + +ret = DTUData() + +# Stand aus dem letzten Moment, in dem ALLE Wechselrichter frische Daten +# geliefert haben. Faellt spaeter einer aus, laesst sich sein Beitrag daraus +# hochrechnen: die uebrigen stehen daneben und sehen dieselbe Sonne, ihr +# Verhaeltnis zu diesem Stand traegt Sonnenstand und Bewoelkung mit. +_ref = {"p_AC": {}, "p_PV": {}} + +def schaetzeAusfaelle(stale): + """Beitrag ausgefallener Wechselrichter aus den gesunden hochrechnen. + + Tritt an die Stelle der frueheren _CP-Kopien: die galten nur fuer das Paar + 1/2 und uebernahmen den Nachbarwert unskaliert. Grundlage ist _ref, der + letzte Moment mit lueckenlosen Daten. Der Faktor ist das Verhaeltnis, in dem + die weiterhin gesunden Geraete seither gestiegen oder gefallen sind - damit + traegt die Schaetzung Sonnenstand und Bewoelkung mit. Ohne Referenz oder + ohne gesunden Nachbarn wird wie frueher genullt. Gedeckelt wird auf + limit_absolute, die von der DTU gemeldete Wirkleistungsgrenze des Geraets - + die steht auch dann noch in der Uebersichtsantwort, wenn die Detailabfrage + schon ins Leere laeuft. + """ + for i in range(len(ret.inverter)): + ret.inverter[i].estimated = False + ret.inverter[i].p_PV_est = 0.0 + gesund = [i for i in range(len(ret.inverter)) if i not in stale] + if not stale: + for i in gesund: # Referenz nachfuehren + _ref["p_AC"][i] = ret.inverter[i].p_AC + _ref["p_PV"][i] = list(ret.inverter[i].p_PV) + return + refSum = sum(_ref["p_AC"].get(i,0.0) for i in gesund) + nowSum = sum(ret.inverter[i].p_AC for i in gesund) + faktor = nowSum/refSum if refSum > 0 else 0.0 + for i in stale: + if _ref["p_AC"].get(i,0.0) > 0 and faktor > 0: + pAC = _ref["p_AC"][i]*faktor + if ret.inverter[i].limit > 0: + pAC = min(pAC, ret.inverter[i].limit) + skala = pAC/_ref["p_AC"][i] + ret.inverter[i].p_AC = round(pAC,2) + ret.inverter[i].p_PV = [round(v*skala,2) for v in _ref["p_PV"].get(i,[])] + ret.inverter[i].p_PV_est = round(sum(ret.inverter[i].p_PV),2) + ret.inverter[i].estimated = True + else: # keine Grundlage - wie bisher nullen + ret.inverter[i].p_AC = 0 + ret.inverter[i].p_PV = [0]*len(ret.inverter[i].p_PV) + + +async def gatherData() -> DTUData: + + #print("get charger status..."); + stale = set() + timeout = aiohttp.ClientTimeout(total=3) + try: + async with aiohttp.ClientSession() as session: + async with session.get('http://192.168.179.36/api/livedata/status',timeout=timeout) as response: #?filter=psm,fup,amp,frc,nrg,lmo,car + if response.status == 200: + html = await response.text() + #logging.warning(html) + state = json.loads(html) + ret.p_AC_sum = state["total"]["Power"]["v"] + invNum = 0 + for inverter in state["inverters"]: + if len(ret.inverter) <= invNum: + ret.inverter.append(DTUInvData()) + ret.inverter[invNum].name = inverter["name"] + ret.inverter[invNum].limit = inverter.get("limit_absolute",0) or 0 + try: + async with session.get('http://192.168.179.36/api/livedata/status?inv='+inverter["serial"],timeout=timeout) as response: #?filter=psm,fup,amp,frc,nrg,lmo,car + html = await response.text() + invState = json.loads(html) + if(invState["inverters"][0]["data_age"] < 60): + ret.inverter[invNum].p_AC = invState["inverters"][0]["AC"]["0"]["Power"]["v"] + ret.inverter[invNum].temp = invState["inverters"][0]["INV"]["0"]["Temperature"]["v"] + ret.inverter[invNum].error = 0 + pvNum = 0 + for key, pvs in invState["inverters"][0]["DC"].items(): + if len(ret.inverter[invNum].p_PV) <= pvNum: + ret.inverter[invNum].p_PV.append(pvs["Power"]["v"]) + else: + ret.inverter[invNum].p_PV[pvNum] = pvs["Power"]["v"] + pvNum = pvNum + 1 + else: + ret.inverter[invNum].error += 1 + stale.add(invNum) + except: + ret.inverter[invNum].error += 1 + stale.add(invNum) + _LOGGER.warning("Inverter data error: "+inverter["serial"]) + invNum = invNum + 1 + schaetzeAusfaelle(stale) + + except: + # Die DTU selbst antwortet nicht - es gibt keinen gesunden Nachbarn, aus + # dem sich etwas hochrechnen liesse. Beide Seiten werden genullt, damit + # p_AC und p_PV zueinander passen; frueher blieb p_AC auf dem alten Wert + # stehen, waehrend p_PV auf 0 fiel. Der error-Zaehler traegt es nach aussen. + for inv in ret.inverter: + inv.error += 1 + inv.estimated = False + inv.p_PV_est = 0.0 + inv.p_AC = 0 + try: + inv.p_PV = [0]*len(inv.p_PV) + except: + pass + _LOGGER.debug("OpenDTU not working") + return ret + +#async def main(): +# await gatherNeededStatus() + +#loop = asyncio.get_event_loop() +#loop.run_until_complete(main()) + + +""" +{ + "inverters":[ + { + "serial":"116491626890", + "name":"Terrassendach", + "order":0, + "data_age":1, + "poll_enabled":true, + "reachable":true, + "producing":true, + "limit_relative":100, + "limit_absolute":1600, + "AC":{ + "0":{ + "Power":{ + "v":74.09999847, + "u":"W", + "d":1 + }, + "Voltage":{ + "v":235.3000031, + "u":"V", + "d":1 + }, + "Current":{ + "v":0.310000002, + "u":"A", + "d":2 + }, + "Power DC":{ + "v":77.80000305, + "u":"W", + "d":1 + }, + "YieldDay":{ + "v":179, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":8.732000351, + "u":"kWh", + "d":3 + }, + "Frequency":{ + "v":50.00999832, + "u":"Hz", + "d":2 + }, + "PowerFactor":{ + "v":1, + "u":"", + "d":3 + }, + "ReactivePower":{ + "v":0, + "u":"var", + "d":1 + }, + "Efficiency":{ + "v":95.24420929, + "u":"%", + "d":3 + } + } + }, + "DC":{ + "0":{ + "name":{ + "u":"" + }, + "Power":{ + "v":12.80000019, + "u":"W", + "d":1 + }, + "Voltage":{ + "v":21.20000076, + "u":"V", + "d":1 + }, + "Current":{ + "v":0.610000014, + "u":"A", + "d":2 + }, + "YieldDay":{ + "v":31, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":1.710000038, + "u":"kWh", + "d":3 + } + }, + "1":{ + "name":{ + "u":"" + }, + "Power":{ + "v":26.60000038, + "u":"W", + "d":1 + }, + "Voltage":{ + "v":43.20000076, + "u":"V", + "d":1 + }, + "Current":{ + "v":0.610000014, + "u":"A", + "d":2 + }, + "YieldDay":{ + "v":62, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":2.496000051, + "u":"kWh", + "d":3 + } + }, + "2":{ + "name":{ + "u":"" + }, + "Power":{ + "v":12.5, + "u":"W", + "d":1 + }, + "Voltage":{ + "v":21.20000076, + "u":"V", + "d":1 + }, + "Current":{ + "v":0.589999974, + "u":"A", + "d":2 + }, + "YieldDay":{ + "v":26, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":1.179999948, + "u":"kWh", + "d":3 + } + }, + "3":{ + "name":{ + "u":"" + }, + "Power":{ + "v":25.89999962, + "u":"W", + "d":1 + }, + "Voltage":{ + "v":43, + "u":"V", + "d":1 + }, + "Current":{ + "v":0.600000024, + "u":"A", + "d":2 + }, + "YieldDay":{ + "v":60, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":3.345999956, + "u":"kWh", + "d":3 + } + } + }, + "INV":{ + "0":{ + "Temperature":{ + "v":12.5, + "u":"°C", + "d":1 + } + } + }, + "events":1 + } + ], + "total":{ + "Power":{ + "v":74.09999847, + "u":"W", + "d":1 + }, + "YieldDay":{ + "v":179, + "u":"Wh", + "d":0 + }, + "YieldTotal":{ + "v":8.732000351, + "u":"kWh", + "d":3 + } + }, + "hints":{ + "time_sync":false, + "radio_problem":false, + "default_password":true + } +}""" \ No newline at end of file diff --git a/gatherShellyEM3DataEG.py b/gatherShellyEM3DataEG.py new file mode 100644 index 0000000..d1dd55a --- /dev/null +++ b/gatherShellyEM3DataEG.py @@ -0,0 +1,44 @@ +import json +import asyncio +import aiohttp +import logging +import math +from typing import List +from dataclasses import dataclass +from dataclasses import field + +_LOGGER = logging.getLogger(__name__) + +@dataclass +class PowerEGData: + P_L1:float = 0 + P_L2:float = 0 + P_L3:float = 0 + error:int = 0 +ret = PowerEGData() + +async def gatherData() -> PowerEGData: + + #print("get power EG status..."); + timeout = aiohttp.ClientTimeout(total=3) + try: + async with aiohttp.ClientSession() as session: + #_LOGGER.warning("fetching Power EG") + async with session.get('http://192.168.179.113/rpc/EM.GetStatus?id=0',timeout=timeout) as response: #?filter=psm,fup,amp,frc,nrg,lmo,car + if response.status == 200: + html = await response.text() + #print(html) + state = json.loads(html) + ret.error = 0 + ret.P_L1 = state["a_act_power"] + ret.P_L2 = state["b_act_power"] + ret.P_L3 = state["c_act_power"] + except: + if(ret.error > 5): + ret.P_L1 = 0 + ret.P_L2 = 0 + ret.P_L3 = 0 + _LOGGER.warning("Power EG could not be fetched") + else: + ret.error += 1 + return ret diff --git a/gatherShellyEM3DataUG.py b/gatherShellyEM3DataUG.py new file mode 100644 index 0000000..4642a7b --- /dev/null +++ b/gatherShellyEM3DataUG.py @@ -0,0 +1,43 @@ +import json +import asyncio +import aiohttp +import logging +import math +from typing import List +from dataclasses import dataclass +from dataclasses import field + +_LOGGER = logging.getLogger(__name__) + +@dataclass +class PowerUGData: + P_L1:float = 0 + P_L2:float = 0 + P_L3:float = 0 + error:int = 0 +ret = PowerUGData() + +async def gatherData() -> PowerUGData: + + #print("get charger status..."); + timeout = aiohttp.ClientTimeout(total=3) + try: + async with aiohttp.ClientSession() as session: + async with session.get('http://192.168.179.161/rpc/EM.GetStatus?id=0',timeout=timeout) as response: #?filter=psm,fup,amp,frc,nrg,lmo,car + if response.status == 200: + html = await response.text() + #logging.warning(html) + state = json.loads(html) + ret.error = 0 + ret.P_L1 = state["a_act_power"] + ret.P_L2 = state["b_act_power"] + ret.P_L3 = state["c_act_power"] + except: + if(ret.error > 5): + ret.P_L1 = 0 + ret.P_L2 = 0 + ret.P_L3 = 0 + _LOGGER.warning("Power UG could not be fetched") + else: + ret.error += 1 + return ret diff --git a/gatherSkodaData.py b/gatherSkodaData.py new file mode 100644 index 0000000..7d9e453 --- /dev/null +++ b/gatherSkodaData.py @@ -0,0 +1,839 @@ +"""Anbindung der oeffentlichen MySkoda-API. + +Loest den Weg ueber den Kia ab, dessen Dateien mit dem Fahrzeug weg sind. Der +lief ueber einen Cron-Job, der die Tabelle car fuellte, waehrend der Manager +daraus nur die jeweils letzte Zeile wieder herauslas - Daten also durch die +Datenbank hindurch von einem Prozess zum anderen. Hier laeuft alles im Manager +selbst: der Abruf fuellt rtData direkt und schreibt seine eigene, +ausfuehrliche Historie. + +Die API ist gegenueber dem frueheren Weg deutlich schlichter. Ein Header +X-API-Key genuegt, kein Login, kein Token-Refresh. Der Schluessel wird in der +MySkoda-App unter https://go.skoda.eu/api-keys erzeugt, ist an die dort +ausgewaehlten Fahrzeuge gebunden und laeuft ab - jede erfolgreiche Antwort +traegt X-API-Key-Expires-At mit, das Modul warnt rechtzeitig vorher. + +Ein einziger GET liefert Ladezustand, Verriegelung, Kilometerstand, +Parkposition und Klima. Spec: https://public.api.connect.skoda-auto.cz/docs + +Zwei Dinge unterscheiden das Modul von den uebrigen Sammlern: + + Es ist eine Cloud-API mit knappem Kontingent, kein Geraet im Haus: laut + Doku 20 Anfragen je Stunde und Schluessel, ausdruecklich vorlaeufig. Der + Manager ruft gatherData() im 3-Sekunden-Takt auf, angefragt wird aber nur, + wenn das eigene Intervall abgelaufen ist - alle vier Minuten waehrend des + Ladens, stuendlich beim Parken. Darueber liegen eine eigene Stundenbilanz + und die RateLimit-Header der Antwort, die als massgebliche Quelle gelten. + Zieht Skoda das Kontingent enger, folgt das Modul von selbst. + + Ladebeginn und Ladeende stossen einen Abruf ausser der Reihe an. Die + Wallbox merkt beides sofort, das Fahrzeug erst beim naechsten Abruf - und + bei so wenigen Anfragen sind genau diese beiden Augenblicke die + wertvollsten: der Ladestand davor und danach traegt die + Kapazitaetsrechnung. + + Die Antwort kann lange dauern. Deshalb blockiert gatherData() nie: der + Abruf laeuft als Hintergrund-Task, zurueckgegeben wird immer sofort der + zuletzt bekannte Stand. Eine haengende Cloud-Verbindung kann den + 3-Sekunden-Takt des Managers damit nicht ausbremsen. +""" + +import json +import asyncio +import aiohttp +import logging +import os +import re +import collections +import time +import datetime +from typing import List, Optional +from dataclasses import dataclass, field + +import mysql.connector as mc + + +_LOGGER = logging.getLogger(__name__) + +_URL = "https://public.api.connect.skoda-auto.cz/api/v1/vehicles/" + +# Der include-Parameter bleibt bewusst weg. Ohne ihn liefert die API alles, +# was das Fahrzeug unterstuetzt, und meldet UNSUPPORTED nur fuer Teile, die +# ausdruecklich angefordert wurden - eine Liste haette also bei jedem Abruf +# Fehler fuer alles erzeugt, was dieses Modell nicht kann. So passt sich der +# Abruf von selbst an, ob ein reiner Stromer oder ein Plug-in-Hybrid ankommt, +# und die Ladeprofile kommen ohne Zutun in skoda_raw mit. + +# Schluessel und VIN stehen bewusst nicht im Quelltext: der Schluessel laeuft +# ab und muss dann getauscht werden. Die Datei wird bei jeder Aenderung neu +# gelesen, ein Neustart des Managers ist dafuer nicht noetig. +_KONFIG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "skoda.conf") + +# Das Kontingent ist der enge Punkt dieser Schnittstelle: laut Doku +# 20 Anfragen je Stunde und Schluessel, ausdruecklich nicht endgueltig. Ein +# Abruf alle drei Minuten ist damit schon das Aeusserste. Alle Intervalle +# unten sind daran bemessen; _BUDGET zieht zusaetzlich eine eigene Grenze +# knapp darunter, damit fuer einen Fehlversuch noch Luft bleibt. +# +# Fehlerantworten ab 500 zaehlen laut Doku mit, 401 und 403 nicht. +_LIMIT = 20 # bekanntes Kontingent je Stunde + +# Die API begrenzt je Schluessel. Steht in skoda.conf ein eigener CMD_API_KEY, +# schickt die Weboberflaeche ihre Befehle darueber und nimmt diesem Modul +# nichts weg - dann darf es fast das ganze Kontingent nutzen. Ohne zweiten +# Schluessel teilen sich beide eines, und weil sie in getrennten Prozessen +# laufen und keinen gemeinsamen Zaehler haben, ist es fest aufgeteilt: +# +# eigener CMD_API_KEY nur API_KEY +# dieses Modul 18 / Stunde 14 / Stunde +# Befehle 15 / Stunde 4 / Stunde (ajax/skodaCmd.php) +# Reserve 2 / Stunde 2 / Stunde +# +# Umgestellt wird von selbst, sobald der zweite Schluessel auftaucht - von +# Hand ist hier nichts nachzuziehen. +_BUDGET_ALLEIN = 18 +_BUDGET_GETEILT = 14 +_FENSTER = 3600 # Bezugszeitraum des Kontingents + +# Abrufintervalle in Sekunden. +_I_MIN = 200 # harte Untergrenze, egal was sonst gilt +_I_LADEN = 240 # laedt gerade - 15 Abrufe je Stunde +_I_FAHRT = 300 # unterwegs +_I_GESTECKT = 900 # Kabel steckt, laedt aber nicht +_I_AKTIV = 1800 # steht, hat sich zuletzt aber noch geruehrt +_I_RUHE = 3600 # seit Stunden unveraendert + +_HEARTBEAT = 3600 # auch ohne Aenderung so oft eine Zeile schreiben +_RUHE_AB = 7200 # ab so langer Unveraendertheit gilt _I_RUHE +_WARN_KEY = 14 # Tage vor Ablauf des Schluessels warnen + +_TIMEOUT = aiohttp.ClientTimeout(total=20) + +# Ladezustaende, in denen das Kabel steckt. +_GESTECKT = ("CHARGING", "CONSERVING", "READY_FOR_CHARGING", + "CHARGING_INTERRUPTED", "DISCHARGING") + + +@dataclass +class SkodaData: + # --- Identitaet ------------------------------------------------------- + vin:str = "" + name:str = "" + plate:str = "" + + # --- Batterie und Laden ---------------------------------------------- + soc:int = 0 # Prozent + range_m:int = 0 # Restreichweite, Einheit wie geliefert + range_km:float = 0.0 # daraus abgeleitet, immer Kilometer + chgState:str = "" # CHARGING, CONSERVING, CONNECT_CABLE, ... + chgType:str = "" # AC, DC, OFF + chgKw:float = 0.0 # Ladeleistung laut Fahrzeug + chgKmh:float = 0.0 # Ladegeschwindigkeit in km/h + chgRemMin:int = 0 # Restladezeit in Minuten + chgFullAt:Optional[datetime.datetime] = None + savedLoc:bool = False # steht an einem gespeicherten Ladeort + + # --- Ladeeinstellungen ------------------------------------------------ + targetSoc:int = 0 + careTargetSoc:int = 0 + careMode:bool = False # Batterieschonung aktiv + chgMode:str = "" # MANUAL, TIMER, ... + maxAc:str = "" # REDUCED, MAXIMUM + maxAcA:int = 0 # Ampere-Grenze + autoUnlock:bool = False + + # --- Zustand ---------------------------------------------------------- + locked:Optional[bool] = None # None, wenn das Fahrzeug UNKNOWN meldet + doorsLocked:str = "" + doors:str = "" + windows:str = "" + lights:str = "" + sunroof:str = "" + trunk:str = "" + bonnet:str = "" + + odoKm:int = 0 + + # --- Verbrenner, nur bei Hybrid oder Verbrenner besetzt --------------- + carType:str = "" # HYBRID, GASOLINE, DIESEL, CNG, LPG + totalRangeKm:float = 0.0 # Gesamtreichweite ueber alle Antriebe + adBlueKm:float = 0.0 + eng1Type:str = "" # ELECTRIC, GASOLINE, DIESEL, ... + eng1Soc:int = 0 + eng1FuelPct:int = 0 + eng1RangeKm:float = 0.0 + eng2Type:str = "" + eng2Soc:int = 0 + eng2FuelPct:int = 0 + eng2RangeKm:float = 0.0 + fuelPct:int = 0 # Tankfuellung, unabhaengig davon, an + # welcher der beiden Motorstellen der + # Verbrenner gemeldet wird + + # --- Position --------------------------------------------------------- + parkState:str = "" # PARKED, IN_MOTION + lat:Optional[float] = None + lon:Optional[float] = None + address:str = "" + + # --- Klima ------------------------------------------------------------ + acState:str = "" + acTargetC:Optional[float] = None + acWinFront:Optional[bool] = None + acWinRear:Optional[bool] = None + auxState:str = "" + ventState:str = "" + + # --- Zeitstempel des Fahrzeugs --------------------------------------- + # Die Antwort setzt sich aus mehreren Quellen zusammen, jede mit eigenem + # Stand. Alle vier mitzufuehren zeigt spaeter, wie alt ein Wert war. + capChg:Optional[datetime.datetime] = None + capStatus:Optional[datetime.datetime] = None + capOdo:Optional[datetime.datetime] = None + capFuel:Optional[datetime.datetime] = None + capAc:Optional[datetime.datetime] = None + + # --- Betrieb des Moduls ---------------------------------------------- + error:int = 0 # aufeinanderfolgende Fehlversuche + httpStatus:int = 0 + apiErrors:str = "" # Fehlerliste der Antwort, kommagetrennt + rlRemaining:int = -1 # Restkontingent laut RateLimit-Header + keyExpires:Optional[datetime.datetime] = None + lastOk:float = 0.0 # Zeitpunkt der letzten guten Antwort + alter:float = 0.0 # Sekunden seit der letzten guten Antwort + + +ret = SkodaData() # Modul-Singleton, ueberlebt zwischen Aufrufen + +# Alles, was nur den Ablauf steuert und nicht nach aussen gehoert. +_st = { + "naechster": 0.0, # fruehester naechster Abruf + "laeuft": False, # ein Abruf ist unterwegs + "letzteAend": 0.0, # wann sich zuletzt etwas am Fahrzeug ruehrte + "letzteZeile": 0.0, # wann zuletzt eine Zeile geschrieben wurde + "signatur": None, # Fingerabdruck der zuletzt geschriebenen Zeile + "keyGewarnt": False, + "konfMtime": 0.0, + "apiKey": "", + "vin": "", + "ladenVorher": False, # Hausseite lieferte beim letzten Aufruf Strom + "eigenerCmdKey": False, # Steuerung hat einen eigenen Schluessel +} + +# Zeitpunkte der Anfragen der letzten Stunde. Eigene Buchfuehrung neben den +# RateLimit-Headern: die kommen erst mit der Antwort, und ihr Reset-Wert +# schrumpft ueber das Fenster, sodass sich gegen Ende ein Schwall erlauben +# liesse, der zu Beginn des naechsten Fensters sofort auflaeuft. Die eigene +# Liste haelt den Abstand ueber jede Fenstergrenze hinweg. +_verbrauch = collections.deque() + +# Hausseitige Werte im Moment des Abrufs. Der Manager reicht sie bei jedem +# Aufruf herein; der Hintergrund-Task greift auf den letzten Stand zu. +_haus = {"wbKw":0.0, "wbPlug":False, "wbogKw":0.0, "wbogPlug":False, + "pvKw":0.0, "gridKw":0.0, "wbWh":0, "wbogWh":0} + +_db = {"host":"localhost", "port":3310, "user":"solarLog", + "passwd":"", "database":"solarLog"} + + +# --------------------------------------------------------------------------- +# Kleinkram +# --------------------------------------------------------------------------- + +_ISO = re.compile(r"(\d{4})-(\d\d)-(\d\d)[T ](\d\d):(\d\d):(\d\d)" + r"(?:\.\d+)?(Z|[+-]\d\d:?\d\d)?") + +def _zeit(s) -> Optional[datetime.datetime]: + """ISO-8601 der API in lokale, naive Zeit fuer MySQL DATETIME. + + Die API liefert UTC mit Z. Der Rest der Datenbank steht in Ortszeit, also + wird hier umgerechnet - sonst laegen Ladevorgaenge im Sommer zwei Stunden + neben den Zaehlerwerten in EnergyFlow, mit denen sie verglichen werden + sollen. datetime.fromisoformat kann in Python 3.8 weder Z noch beliebige + Bruchteile, daher der eigene Ausdruck. + """ + if not s: + return None + m = _ISO.match(str(s).strip()) + if not m: + return None + y, mo, d, h, mi, se, off = m.groups() + dt = datetime.datetime(int(y), int(mo), int(d), int(h), int(mi), int(se)) + if not off: + return dt # ohne Zone: schon lokal + if off != "Z": + vz = 1 if off[0] == "+" else -1 + off = off[1:].replace(":", "") + dt -= vz*datetime.timedelta(hours=int(off[:2]), minutes=int(off[2:])) + return dt.replace(tzinfo=datetime.timezone.utc).astimezone().replace(tzinfo=None) + + +def _janein(v) -> Optional[bool]: + """YES/NO/ON/OFF/ACTIVATED der API in bool, UNKNOWN in None. + + Ein fehlender Wert und ein ausdrueckliches UNKNOWN sind nicht dasselbe wie + ein Nein. Beim Kia wurde beides zu 0 und war hinterher nicht mehr zu + unterscheiden; deshalb hier None. + """ + if v is None: + return None + v = str(v).upper() + if v in ("YES", "ON", "TRUE", "ACTIVATED", "LOCKED", "PERMANENT"): + return True + if v in ("NO", "OFF", "FALSE", "DEACTIVATED", "UNLOCKED"): + return False + return None + + +def _konfig() -> bool: + """Schluessel und VIN aus skoda.conf lesen, wenn die Datei sich geaendert hat. + + Format, eine Zuweisung je Zeile: API_KEY=... und VIN=... + Der Schluessel laeuft ab. Weil die Datei bei jeder Aenderung neu gelesen + wird, genuegt zum Tausch das Ueberschreiben - der Manager laeuft weiter. + """ + try: + mtime = os.path.getmtime(_KONFIG) + except OSError: + if _st["apiKey"]: + _LOGGER.warning("skoda.conf nicht mehr lesbar, benutze den letzten Stand.") + return True + return False + if mtime == _st["konfMtime"]: + return bool(_st["apiKey"] and _st["vin"]) + werte = {} + try: + with open(_KONFIG, "r") as f: + for zeile in f: + zeile = zeile.strip() + if not zeile or zeile.startswith("#") or "=" not in zeile: + continue + k, v = zeile.split("=", 1) + werte[k.strip().upper()] = v.strip().strip('"').strip("'") + except OSError as e: + _LOGGER.error("skoda.conf nicht lesbar: "+str(e)) + return bool(_st["apiKey"] and _st["vin"]) + _st["konfMtime"] = mtime + _st["apiKey"] = werte.get("API_KEY", "") + _st["vin"] = werte.get("VIN", "") + _st["eigenerCmdKey"] = bool(werte.get("CMD_API_KEY")) + _db["passwd"] = werte.get("DB_PASSWORD", _db["passwd"]) + _st["keyGewarnt"] = False + if not _st["apiKey"] or not _st["vin"]: + _LOGGER.error("skoda.conf braucht API_KEY und VIN.") + return False + ret.vin = _st["vin"] + _LOGGER.info("skoda.conf gelesen, VIN endet auf "+_st["vin"][-4:] + +(", eigener Schluessel fuer die Steuerung" + if _st["eigenerCmdKey"] else ", ein Schluessel fuer alles")) + return True + + +def setDbPasswort(pw:str): + """Datenbank-Passwort vom Manager uebernehmen. + + Alternativ steht DB_PASSWORD in skoda.conf. So oder so taucht es hier + nicht im Quelltext auf. + """ + _db["passwd"] = pw + + +# --------------------------------------------------------------------------- +# Antwort auswerten +# --------------------------------------------------------------------------- + +def uebernehmen(antwort:dict): + """Eine Antwort des Fahrzeugs in ret uebertragen. + + Bewusst eine eigene Funktion ohne Netz und ohne Datenbank: so laesst sich + die Zuordnung mit einer gespeicherten Antwort pruefen, bevor das Auto da + ist (siehe __main__ am Dateiende). + + Fehlende Teile werden uebersprungen statt genullt. Die API laesst einen + Teil weg, wenn sie ihn gerade nicht bekommt, und legt dann einen Eintrag + in errors ab - der alte Wert ist dann die bessere Auskunft als eine Null. + """ + v = antwort.get("vehicle") or {} + + fehler = [] + for e in antwort.get("errors") or []: + if e.get("type"): + fehler.append(e["type"]) + ret.apiErrors = ",".join(fehler)[:255] + + if v.get("vin"): + ret.vin = v["vin"] + if v.get("name"): + ret.name = v["name"] + if v.get("licensePlate"): + ret.plate = v["licensePlate"] + + # --- Laden ------------------------------------------------------------ + chg = v.get("charging") + if chg: + ret.savedLoc = bool(chg.get("isVehicleInSavedLocation", False)) + ret.capChg = _zeit(chg.get("carCapturedTimestamp")) or ret.capChg + s = chg.get("status") or {} + ret.chgState = s.get("state", ret.chgState) or "" + ret.chgType = s.get("chargeType", ret.chgType) or "" + if s.get("chargePowerInKw") is not None: + ret.chgKw = round(float(s["chargePowerInKw"]), 3) + elif ret.chgState not in _GESTECKT: + ret.chgKw = 0.0 + if s.get("chargingRateInKilometersPerHour") is not None: + ret.chgKmh = round(float(s["chargingRateInKilometersPerHour"]), 2) + if s.get("remainingTimeToFullyChargedInMinutes") is not None: + ret.chgRemMin = int(s["remainingTimeToFullyChargedInMinutes"]) + ret.chgFullAt = _zeit(s.get("fullyChargedAt")) or ret.chgFullAt + b = s.get("battery") or {} + if b.get("stateOfChargeInPercent") is not None: + ret.soc = int(b["stateOfChargeInPercent"]) + if b.get("remainingCruisingRangeInMeters") is not None: + ret.range_m = int(b["remainingCruisingRangeInMeters"]) + # Das Feld heisst Meter, das Beispiel der Spec (249) sieht nach + # Kilometern aus. Der Rohwert wird unveraendert mitgeschrieben, + # abgeleitet wird nur diese Anzeige: ueber 1500 kann nur Meter + # gemeint sein, ein Akku traegt keine 1500 Kilometer. + ret.range_km = round(ret.range_m/1000.0, 1) if ret.range_m > 1500 else float(ret.range_m) + cs = chg.get("settings") or {} + if cs.get("targetStateOfChargeInPercent") is not None: + ret.targetSoc = int(cs["targetStateOfChargeInPercent"]) + if cs.get("batteryCareModeTargetValueInPercent") is not None: + ret.careTargetSoc = int(cs["batteryCareModeTargetValueInPercent"]) + cm = _janein(cs.get("chargingCareMode")) + if cm is not None: + ret.careMode = cm + ret.chgMode = cs.get("preferredChargeMode", ret.chgMode) or "" + ret.maxAc = cs.get("maxChargeCurrentAc", ret.maxAc) or "" + if cs.get("maxChargeCurrentAcAmpere") is not None: + ret.maxAcA = int(cs["maxChargeCurrentAcAmpere"]) + au = _janein(cs.get("autoUnlockPlugWhenCharged")) + if au is not None: + ret.autoUnlock = au + + # --- Tueren, Fenster, Licht ------------------------------------------- + st = v.get("status") + if st: + ret.capStatus = _zeit(st.get("carCapturedTimestamp")) or ret.capStatus + o = st.get("overall") or {} + ret.locked = _janein(o.get("locked")) + ret.doorsLocked = o.get("doorsLocked", "") or "" + ret.doors = o.get("doors", "") or "" + ret.windows = o.get("windows", "") or "" + ret.lights = o.get("lights", "") or "" + d = st.get("detail") or {} + ret.sunroof = d.get("sunroof", "") or "" + ret.trunk = d.get("trunk", "") or "" + ret.bonnet = d.get("bonnet", "") or "" + + # --- Kilometerstand --------------------------------------------------- + od = v.get("odometer") + if od and od.get("mileageInKm") is not None: + ret.odoKm = int(od["mileageInKm"]) + ret.capOdo = _zeit(od.get("carCapturedTimestamp")) or ret.capOdo + + # --- Verbrenner ------------------------------------------------------- + # Ein reiner Stromer laesst diesen Block weg, dann bleiben die Spalten + # NULL. Ein Hybrid liesse sich sonst nachtraeglich nicht auswerten - was + # hier nicht mitgeschrieben wird, ist fuer immer fort. + fs = v.get("fuelStatus") + if fs: + ret.carType = fs.get("carType", ret.carType) or "" + ret.capFuel = _zeit(fs.get("carCapturedTimestamp")) or ret.capFuel + if fs.get("totalRangeInKm") is not None: + ret.totalRangeKm = round(float(fs["totalRangeInKm"]), 1) + if fs.get("adBlueRange") is not None: + ret.adBlueKm = round(float(fs["adBlueRange"]), 1) + for nr, schluessel in ((1, "primaryEngineRange"), (2, "secondaryEngineRange")): + er = fs.get(schluessel) or {} + if not er: + continue + setattr(ret, "eng"+str(nr)+"Type", er.get("engineType", "") or "") + if er.get("currentSoCInPercent") is not None: + setattr(ret, "eng"+str(nr)+"Soc", int(er["currentSoCInPercent"])) + if er.get("currentFuelLevelInPercent") is not None: + setattr(ret, "eng"+str(nr)+"FuelPct", int(er["currentFuelLevelInPercent"])) + if er.get("remainingRangeInKm") is not None: + setattr(ret, "eng"+str(nr)+"RangeKm", round(float(er["remainingRangeInKm"]), 1)) + if (er.get("engineType") not in (None, "ELECTRIC") + and er.get("currentFuelLevelInPercent") is not None): + ret.fuelPct = int(er["currentFuelLevelInPercent"]) + + # --- Position --------------------------------------------------------- + pp = v.get("parkingPosition") + if pp: + ret.parkState = pp.get("state", ret.parkState) or "" + g = pp.get("gpsCoordinates") or {} + if g.get("latitude") is not None: + ret.lat = round(float(g["latitude"]), 6) + ret.lon = round(float(g["longitude"]), 6) + ret.address = (pp.get("formattedAddress") or ret.address)[:160] + + # --- Klima ------------------------------------------------------------ + ac = v.get("airConditioning") + if ac: + ret.acState = ac.get("state", ret.acState) or "" + ret.capAc = _zeit(ac.get("carCapturedTimestamp")) or ret.capAc + tt = ac.get("targetTemperature") or {} + if tt.get("value") is not None: + t = float(tt["value"]) + if str(tt.get("unit", "CELSIUS")).upper() == "FAHRENHEIT": + t = (t-32.0)*5.0/9.0 + ret.acTargetC = round(t, 1) + wh = ac.get("windowHeating") or {} + ret.acWinFront = _janein(wh.get("front")) + ret.acWinRear = _janein(wh.get("rear")) + aux = v.get("auxiliaryHeating") + if aux: + ret.auxState = aux.get("state", ret.auxState) or "" + vt = v.get("activeVentilation") + if vt: + ret.ventState = vt.get("state", ret.ventState) or "" + + +def _signatur() -> tuple: + """Fingerabdruck der Werte, deren Aenderung eine neue Zeile rechtfertigt. + + Das Fahrzeug meldet sich nur, wenn es etwas zu melden hat; zwischendurch + liefert die Cloud denselben Stand erneut. Ohne diesen Vergleich stuenden + in der Tabelle vor allem Wiederholungen. Die hausseitigen Werte gehen + absichtlich nicht ein - die stehen ohnehin alle 300 s in EnergyFlow. + + chgKw gehoert dagegen hinein, obwohl es waehrend des Ladens fast jeden + Abruf veraendert: genau diese Punkte sind die Ladekurve. Ohne den Wert + entstuende eine Zeile erst, wenn der Ladestand um einen Prozentpunkt + weiterspringt - der Knick, an dem das Fahrzeug abregelt, faende sich + hinterher nicht wieder. + """ + return (ret.capChg, ret.capStatus, ret.capOdo, ret.capFuel, ret.capAc, + ret.soc, ret.range_m, ret.chgState, ret.chgType, ret.chgRemMin, + ret.chgKw, + ret.locked, ret.doorsLocked, ret.doors, ret.windows, ret.lights, + ret.sunroof, ret.trunk, ret.bonnet, ret.odoKm, + ret.carType, ret.eng1FuelPct, ret.eng1RangeKm, + ret.eng2FuelPct, ret.eng2RangeKm, ret.totalRangeKm, + ret.parkState, ret.lat, ret.lon, + ret.acState, ret.auxState, ret.ventState, + ret.targetSoc, ret.chgMode, ret.maxAcA, ret.careMode) + + +def _intervall() -> float: + """Wie lange bis zum naechsten Abruf. + + Waehrend des Ladens dicht, damit die Ladekurve genug Stuetzstellen hat - + eine DC-Ladung ist nach einer halben Stunde vorbei. Beim Parken weit, weil + sich dann ohnehin nichts aendert und das Kontingent begrenzt ist. + """ + if ret.error: + # Nach einem Fehler zurueckhaltend erneut versuchen. Ist die Cloud weg, + # bringt haeufiges Klopfen nichts und kostet nur Kontingent. + return min(_I_RUHE, 60.0*(2**min(ret.error-1, 5))) + if ret.chgState == "CHARGING" or (_haus["wbPlug"] and _haus["wbKw"] > 0.5): + return _I_LADEN + if ret.parkState == "IN_MOTION": + return _I_FAHRT + if _haus["wbPlug"] or ret.chgState in _GESTECKT: + return _I_GESTECKT + if time.time() - _st["letzteAend"] > _RUHE_AB: + return _I_RUHE + return _I_AKTIV + + +def _gezaehlt(jetzt:float): + """Eine Anfrage in die Stundenbilanz aufnehmen.""" + _verbrauch.append(jetzt) + _aufraeumen(jetzt) + + +def _aufraeumen(jetzt:float): + while _verbrauch and jetzt - _verbrauch[0] >= _FENSTER: + _verbrauch.popleft() + + +def _budgetSperre(jetzt:float) -> float: + """Fruehester Zeitpunkt, zu dem wieder eine Anfrage frei ist. + + Sind in der zurueckliegenden Stunde bereits _BUDGET Anfragen gelaufen, + wird gewartet, bis die aelteste aus dem Fenster faellt. Das ist die + eigentliche Sicherung: die Intervalle unten sind zwar so bemessen, dass + sie passen, aber Sonderfaelle wie der Anstoss beim Ladebeginn kommen + zusaetzlich - und ein 429 kostet zwar kein Kontingent, verraet aber, dass + die Rechnung nicht aufging. + """ + _aufraeumen(jetzt) + budget = _BUDGET_ALLEIN if _st["eigenerCmdKey"] else _BUDGET_GETEILT + if len(_verbrauch) < budget: + return 0.0 + return _verbrauch[0] + _FENSTER + 5.0 - jetzt + + +def _kontingent(headers): + """Aus den RateLimit-Headern eine Untergrenze fuer den Abstand ableiten. + + Die Doku nennt derzeit 20 Anfragen je Stunde, ausdruecklich nicht + endgueltig, und erklaert die Header zur massgeblichen Quelle. Bleiben im + laufenden Fenster noch n Anfragen und laeuft es in t Sekunden ab, dann + sind t/n Sekunden Abstand gerade noch tragbar; der Zuschlag haelt Abstand + zur Grenze. Zieht Skoda das Kontingent enger, folgt das Modul von selbst, + ohne dass hier eine Zahl nachgetragen werden muesste. + """ + try: + rest = int(headers.get("RateLimit-Remaining", -1)) + reset = int(headers.get("RateLimit-Reset", -1)) + except (TypeError, ValueError): + return 0.0 + ret.rlRemaining = rest + if rest < 0 or reset < 0: + return 0.0 + if rest == 0: + return float(reset) + 5.0 + return (float(reset)/rest)*1.2 + + +def _keyPruefen(headers): + """Vor dem Ablauf des Schluessels warnen, solange noch Zeit zum Tausch ist.""" + ts = _zeit(headers.get("X-API-Key-Expires-At")) + if not ts: + return + ret.keyExpires = ts + tage = (ts - datetime.datetime.now()).total_seconds()/86400.0 + if tage < _WARN_KEY and not _st["keyGewarnt"]: + _st["keyGewarnt"] = True + _LOGGER.warning("Skoda-API-Schluessel laeuft am " + +ts.strftime("%d.%m.%Y")+" ab ("+str(int(tage)) + +" Tage) - in der MySkoda-App erneuern und skoda.conf ueberschreiben.") + + +# --------------------------------------------------------------------------- +# Historie +# --------------------------------------------------------------------------- + +_SPALTEN = ("datetime, cap_chg, cap_status, cap_odo, cap_fuel, cap_ac, " + "soc, range_m, chg_state, chg_type, chg_kw, chg_kmh, chg_rem_min, " + "chg_full_at, saved_loc, target_soc, care_target_soc, care_mode, " + "chg_mode, max_ac, max_ac_a, auto_unlock, " + "locked, doors_locked, doors, windows, lights, sunroof, trunk, bonnet, " + "odo_km, car_type, total_range_km, adblue_km, " + "eng1_type, eng1_soc, eng1_fuel_pct, eng1_range_km, " + "eng2_type, eng2_soc, eng2_fuel_pct, eng2_range_km, " + "park_state, lat, lon, address, " + "ac_state, ac_target_c, ac_win_front, ac_win_rear, aux_state, vent_state, " + "wb_kw, wb_plug, wb_wh_total, wbog_kw, wbog_plug, wbog_wh_total, " + "pv_kw, grid_kw, " + "http_status, api_errors, rl_remaining, key_expires") + + +def _leer(v): + """Leere Zeichenkette als NULL schreiben. + + Kein Wert und ein leerer Wert sind in der Auswertung nicht dasselbe. Mit + NULL genuegt spaeter IS NULL, sonst muesste jede Abfrage zusaetzlich an + den Leerstring denken - und wer das einmal vergisst, zaehlt Fahrzeuge + ohne Verbrenner als Fahrzeuge mit leerem Tank. + """ + return v if v else None + + +def _werte() -> tuple: + return (datetime.datetime.now().replace(microsecond=0), + ret.capChg, ret.capStatus, ret.capOdo, ret.capFuel, ret.capAc, + ret.soc, ret.range_m, _leer(ret.chgState), _leer(ret.chgType), ret.chgKw, + ret.chgKmh, ret.chgRemMin, ret.chgFullAt, ret.savedLoc, + ret.targetSoc, ret.careTargetSoc, ret.careMode, _leer(ret.chgMode), + _leer(ret.maxAc), ret.maxAcA, ret.autoUnlock, + ret.locked, _leer(ret.doorsLocked), _leer(ret.doors), + _leer(ret.windows), _leer(ret.lights), + _leer(ret.sunroof), _leer(ret.trunk), _leer(ret.bonnet), ret.odoKm, + _leer(ret.carType), ret.totalRangeKm, ret.adBlueKm, + _leer(ret.eng1Type), ret.eng1Soc, ret.eng1FuelPct, ret.eng1RangeKm, + _leer(ret.eng2Type), ret.eng2Soc, ret.eng2FuelPct, ret.eng2RangeKm, + _leer(ret.parkState), ret.lat, ret.lon, _leer(ret.address), + _leer(ret.acState), ret.acTargetC, ret.acWinFront, ret.acWinRear, + _leer(ret.auxState), _leer(ret.ventState), + round(_haus["wbKw"], 3), _haus["wbPlug"], _haus["wbWh"], + round(_haus["wbogKw"], 3), _haus["wbogPlug"], _haus["wbogWh"], + round(_haus["pvKw"], 3), round(_haus["gridKw"], 3), + ret.httpStatus, _leer(ret.apiErrors), ret.rlRemaining, ret.keyExpires) + + +def _schreiben(rohtext:str): + """Eine Zeile in skoda und die unveraenderte Antwort in skoda_raw. + + Blockierend - wird nur ueber run_in_executor aufgerufen, damit der + 3-Sekunden-Takt des Managers nicht daran haengt. + + Die Rohantwort mitzuschreiben kostet wenig und rettet spaeter viel: taucht + ein Feld auf, das hier noch nicht zugeordnet ist, laesst es sich aus der + Historie nachtragen, statt erst ab dem Tag der Erkenntnis zu existieren. + """ + werte = _werte() + platz = ",".join(["%s"]*len(werte)) + try: + with mc.connect(**_db) as verbindung: + with verbindung.cursor() as cursor: + cursor.execute("INSERT INTO skoda ("+_SPALTEN+") VALUES ("+platz+");", + werte) + zeile = cursor.lastrowid + if rohtext: + cursor.execute("INSERT INTO skoda_raw (datetime, sample_id, payload) " + "VALUES (%s,%s,%s);", + (datetime.datetime.now().replace(microsecond=0), + zeile, rohtext)) + verbindung.commit() + except Exception as e: + _LOGGER.error("Skoda-Historie nicht geschrieben: "+str(e)) + + +# --------------------------------------------------------------------------- +# Abruf +# --------------------------------------------------------------------------- + +async def _abrufen(): + """Ein Durchgang: anfragen, auswerten, bei Aenderung protokollieren.""" + kopf = {"X-API-Key": _st["apiKey"], "Accept": "application/json"} + url = _URL + _st["vin"] + rohtext = "" + abstand = 0.0 + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=kopf, timeout=_TIMEOUT) as response: + ret.httpStatus = response.status + rohtext = await response.text() + abstand = _kontingent(response.headers) + _keyPruefen(response.headers) + + if response.status == 200: + uebernehmen(json.loads(rohtext)) + ret.error = 0 + ret.lastOk = time.time() + elif response.status == 429: + # Kontingent erschoepft. Retry-After ist verbindlich. + ret.error += 1 + try: + abstand = max(abstand, float(response.headers.get("Retry-After", 60))) + except (TypeError, ValueError): + abstand = max(abstand, 60.0) + _LOGGER.warning("Skoda-API: Kontingent erschoepft, warte " + +str(int(abstand))+" s.") + rohtext = "" + elif response.status in (401, 403): + # Schluessel abgelaufen, widerrufen oder nicht fuer diese VIN + # freigegeben. Das behebt sich nicht von selbst, also selten + # nachfassen statt im Minutentakt gegen die Wand zu laufen. + ret.error += 1 + abstand = max(abstand, float(_I_RUHE)) + _LOGGER.error("Skoda-API weist den Schluessel ab (HTTP " + +str(response.status)+") - in der MySkoda-App " + "erneuern und skoda.conf ueberschreiben.") + rohtext = "" + else: + ret.error += 1 + _LOGGER.warning("Skoda-API antwortet mit HTTP "+str(response.status)) + rohtext = "" + except asyncio.TimeoutError: + ret.error += 1 + _LOGGER.warning("Skoda-API antwortet nicht rechtzeitig.") + except Exception as e: + ret.error += 1 + _LOGGER.warning("Skoda-API nicht erreichbar: "+str(e)) + + jetzt = time.time() + # 401 und 403 zaehlen laut Doku nicht gegen das Kontingent, alles andere + # schon - auch ein 500 aus einer Stoerung bei Skoda. + if ret.httpStatus not in (401, 403): + _gezaehlt(jetzt) + _st["naechster"] = jetzt + max(_I_MIN, _intervall(), abstand, + _budgetSperre(jetzt)) + + if ret.httpStatus != 200: + return + + sig = _signatur() + geaendert = sig != _st["signatur"] + if geaendert: + _st["letzteAend"] = jetzt + # Ohne Aenderung trotzdem gelegentlich eine Zeile: sonst ist hinterher + # nicht zu unterscheiden, ob das Auto stillstand oder das Modul stand. + if geaendert or (jetzt - _st["letzteZeile"]) >= _HEARTBEAT: + _st["signatur"] = sig + _st["letzteZeile"] = jetzt + try: + await asyncio.get_event_loop().run_in_executor(None, _schreiben, rohtext) + except Exception as e: + _LOGGER.error("Skoda-Historie nicht geschrieben: "+str(e)) + + +async def gatherData(wbKw:float=0.0, wbPlug:bool=False, + wbogKw:float=0.0, wbogPlug:bool=False, + pvKw:float=0.0, gridKw:float=0.0, + wbWh:int=0, wbogWh:int=0) -> SkodaData: + """Letzten bekannten Fahrzeugstand liefern, bei Bedarf einen Abruf anstossen. + + Kehrt sofort zurueck. Der eigentliche Abruf laeuft im Hintergrund, damit + eine langsame Cloud-Antwort den 3-Sekunden-Takt des Managers nicht + verzoegert; das Ergebnis steht dann beim naechsten Aufruf bereit. + + Die hausseitigen Werte kommen vom Manager mit und werden neben dem + Fahrzeugstand protokolliert. Erst dadurch wird die Batterie messbar: die + Wallbox zaehlt die eingespeiste Energie, das Fahrzeug meldet den + Ladestand, und aus kWh je SoC-Prozent ergibt sich die nutzbare Kapazitaet + und ihr Verlauf ueber die Jahre. + """ + _haus["wbKw"] = wbKw + _haus["wbPlug"] = bool(wbPlug) + _haus["wbogKw"] = wbogKw + _haus["wbogPlug"] = bool(wbogPlug) + _haus["pvKw"] = pvKw + _haus["gridKw"] = gridKw + # Gesamtzaehlerstaende der Wallboxen in Wh. Die Energie einer Ladung ist + # dann die Differenz zweier Staende statt einer Summe ueber gemittelte + # Leistungswerte - der Fehler an den Raendern des Ladevorgangs entfaellt. + _haus["wbWh"] = int(wbWh or 0) + _haus["wbogWh"] = int(wbogWh or 0) + + # Die Wallbox merkt Anfang und Ende einer Ladung sofort, das Fahrzeug + # erst beim naechsten Abruf. Bei 20 Anfragen je Stunde sind genau diese + # beiden Augenblicke die wertvollsten: der Ladestand davor und danach + # bestimmt die Kapazitaetsrechnung, waehrend ein Punkt mitten in der + # Kurve wenig beitraegt. Beide Flanken stossen deshalb einen Abruf an - + # die Budgetsperre kann ihn trotzdem noch verzoegern. + laedt = bool(wbPlug) and wbKw > 0.5 + if laedt != _st["ladenVorher"]: + _st["ladenVorher"] = laedt + _st["naechster"] = min(_st["naechster"], time.time()) + + ret.alter = round(time.time() - ret.lastOk, 1) if ret.lastOk else 0.0 + + if _st["laeuft"] or time.time() < _st["naechster"]: + return ret + if not _konfig(): + # Noch kein Schluessel hinterlegt. In Ruhe erneut nachsehen, statt die + # Datei alle drei Sekunden zu suchen. + _st["naechster"] = time.time() + 60 + return ret + _st["laeuft"] = True + if True: + + async def lauf(): + try: + await _abrufen() + finally: + _st["laeuft"] = False + + asyncio.ensure_future(lauf()) + return ret + + +if __name__ == "__main__": + # Pruefung ohne Fahrzeug: gespeicherte Antwort einlesen und zeigen, was + # daraus in der Tabelle landen wuerde. + # python3 gatherSkodaData.py antwort.json + import sys + import pprint + logging.basicConfig(level=logging.DEBUG) + with open(sys.argv[1], "r") as f: + uebernehmen(json.load(f)) + pprint.pprint(ret) + print() + for name, wert in zip(_SPALTEN.replace(" ", "").split(","), _werte()): + print(name.ljust(16), wert) diff --git a/gatherWaterData.py b/gatherWaterData.py new file mode 100644 index 0000000..01c3cc5 --- /dev/null +++ b/gatherWaterData.py @@ -0,0 +1,33 @@ +import asyncio +import logging +import sys +import mysql.connector as mc +from mysql.connector import connect, Error +import konfig + +from dataclasses import dataclass + +_LOGGER = logging.getLogger(__name__) + +@dataclass +class WaterData: + waterHeight:int = 0 + temp:float = 0 + +async def getWaterData(): + ret = WaterData + try: + with connect(**konfig.datenbank()) as connection: + query = ("SELECT water_mm, temp_c FROM zisterne WHERE datetime > DATE_SUB(NOW(),INTERVAL 10 MINUTE) ORDER BY ID DESC LIMIT 1;") + with connection.cursor() as cursor: + cursor.execute(query) + myresult = cursor.fetchone() + connection.commit() + ret.waterHeight = myresult[0] + ret.temp = myresult[1]/10 + except: + #_LOGGER.warning("Water data could not be fetched") + return ret + return ret + #result = await vehicle.remote_services.trigger_remote_light_flash() + #print(result.state) \ No newline at end of file diff --git a/goodwe/__init__.py b/goodwe/__init__.py new file mode 100644 index 0000000..63774d2 --- /dev/null +++ b/goodwe/__init__.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import asyncio +import logging +from typing import Type + +from .dt import DT +from .es import ES +from .et import ET +from .exceptions import InverterError, RequestFailedException +from .goodwe import GoodWeXSProcessor, AbstractDataProcessor, GoodWeInverter +from .inverter import Inverter, OperationMode, Sensor, SensorKind +from .model import DT_MODEL_TAGS, ES_MODEL_TAGS, ET_MODEL_TAGS +from .protocol import ProtocolCommand, UdpInverterProtocol, Aa55ProtocolCommand + +logger = logging.getLogger(__name__) + +# Inverter family names +ET_FAMILY = ["ET", "EH", "BT", "BH"] +ES_FAMILY = ["ES", "EM", "BP"] +DT_FAMILY = ["DT", "MS", "NS", "XS"] + +# Initial discovery command +DISCOVERY_COMMAND = Aa55ProtocolCommand("010200", "0182") + +# supported inverter protocols +_SUPPORTED_PROTOCOLS = [ET, DT, ES] + + +async def connect(host: str, family: str = None, comm_addr: int = 0, timeout: int = 1, retries: int = 3, + do_discover: bool = True) -> Inverter: + """Contact the inverter at the specified host/port and answer appropriate Inverter instance. + + The specific inverter family/type will be detected automatically, but it can be passed explicitly. + Supported inverter family names are ET, EH, BT, BH, ES, EM, BP, DT, MS, D-NS and XS. + + Inverter communication address may be explicitly passed, if not the usual default value + will be used (0xf7 for ET/EH/BT/BH/ES/EM/BP inverters, 0x7f for DT/MS/D-NS/XS inverters). + + Since the UDP communication is by definition unreliable, when no (valid) response is received by the specified + timeout, it is considered lost and the command will be re-tried up to retries times. + + Raise InverterError if unable to contact or recognise supported inverter. + """ + if family in ET_FAMILY: + inv = ET(host, comm_addr, timeout, retries) + elif family in ES_FAMILY: + inv = ES(host, comm_addr, timeout, retries) + elif family in DT_FAMILY: + inv = DT(host, comm_addr, timeout, retries) + elif do_discover: + return await discover(host, timeout, retries) + + logger.debug("Connecting to %s family inverter at %s.", family, host) + await inv.read_device_info() + logger.debug("Connected to inverter %s, S/N:%s.", inv.model_name, inv.serial_number) + return inv + + +async def discover(host: str, timeout: int = 1, retries: int = 3) -> Inverter: + """Contact the inverter at the specified value and answer appropriate Inverter instance + + Raise InverterError if unable to contact or recognise supported inverter + """ + failures = [] + + # Try the common AA55C07F0102000241 command first and detect inverter type from serial_number + try: + logger.debug("Probing inverter at %s.", host) + response = await DISCOVERY_COMMAND.execute(host, timeout, retries) + model_name = response[12:22].decode("ascii").rstrip() + serial_number = response[38:54].decode("ascii") + + inverter_class: Type[Inverter] | None = None + for model_tag in ET_MODEL_TAGS: + if model_tag in serial_number: + logger.debug("Detected ET/EH/BT/BH/GEH inverter %s, S/N:%s.", model_name, serial_number) + inverter_class = ET + for model_tag in ES_MODEL_TAGS: + if model_tag in serial_number: + logger.debug("Detected ES/EM/BP inverter %s, S/N:%s.", model_name, serial_number) + inverter_class = ES + for model_tag in DT_MODEL_TAGS: + if model_tag in serial_number: + logger.debug("Detected DT/MS/D-NS/XS/GEP inverter %s, S/N:%s.", model_name, serial_number) + inverter_class = DT + if inverter_class: + i = inverter_class(host, 0, timeout, retries) + await i.read_device_info() + return i + + except InverterError as ex: + failures.append(ex) + + # Probe inverter specific protocols + for inv in _SUPPORTED_PROTOCOLS: + i = inv(host, 0, timeout, retries) + try: + logger.debug("Probing %s inverter at %s.", inv.__name__, host) + await i.read_device_info() + await i.read_runtime_data() + logger.debug("Detected %s family inverter %s, S/N:%s.", inv.__name__, i.model_name, i.serial_number) + return i + except InverterError as ex: + failures.append(ex) + raise InverterError( + "Unable to connect to the inverter at " + f"host={host}, or your inverter is not supported yet.\n" + f"Failures={str(failures)}" + ) + + +async def search_inverters() -> bytes: + """Scan the network for inverters. + Answer the inverter discovery response string (which includes it IP address) + + Raise InverterError if unable to contact any inverter + """ + logger.debug("Searching inverters by broadcast to port 48899") + loop = asyncio.get_running_loop() + command = ProtocolCommand("WIFIKIT-214028-READ".encode("utf-8"), lambda r: True) + response_future = loop.create_future() + transport, _ = await loop.create_datagram_endpoint( + lambda: UdpInverterProtocol(response_future, command, 1, 3), + remote_addr=("255.255.255.255", 48899), + allow_broadcast=True, + ) + try: + await response_future + result = response_future.result() + if result is not None: + return result + else: + raise InverterError("No response received to broadcast request.") + except asyncio.CancelledError: + raise InverterError("No valid response received to broadcast request.") from None + finally: + transport.close() diff --git a/goodwe/const.py b/goodwe/const.py new file mode 100644 index 0000000..9013ce2 --- /dev/null +++ b/goodwe/const.py @@ -0,0 +1,265 @@ +from typing import Dict + +GOODWE_UDP_PORT = 8899 + +BATTERY_MODES: Dict[int, str] = { + 0: "No battery", + 1: "Standby", + 2: "Discharge", + 3: "Charge", + 4: "To be charged", + 5: "To be discharged", +} + +ENERGY_MODES: Dict[int, str] = { + 0: "Check Mode", + 1: "Wait Mode", + 2: "Normal (On-Grid)", + 4: "Normal (Off-Grid)", + 8: "Flash Mode", + 16: "Fault Mode", + 32: "Battery Standby", + 64: "Battery Charging", + 128: "Battery Discharging", +} + +GRID_MODES: Dict[int, str] = { + 0: "Not connected to grid", + 1: "Connected to grid", + 2: "Fault", +} + +GRID_IN_OUT_MODES: Dict[int, str] = { + 0: "Idle", + 1: "Exporting", + 2: "Importing", +} + +LOAD_MODES: Dict[int, str] = { + 0: "Inverter and the load is disconnected", + 1: "The inverter is connected to a load", +} + +PV_MODES: Dict[int, str] = { + 0: "PV panels not connected", + 1: "PV panels connected, no power", + 2: "PV panels connected, producing power", +} + +WORK_MODES: Dict[int, str] = { + 0: "Wait Mode", + 1: "Normal", + 2: "Error", + 4: "Check Mode", +} + +WORK_MODES_ET: Dict[int, str] = { + 0: "Wait Mode", + 1: "Normal (On-Grid)", + 2: "Normal (Off-Grid)", + 3: "Fault Mode", + 4: "Flash Mode", + 5: "Check Mode", +} + +WORK_MODES_ES: Dict[int, str] = { + 0: "Inverter Off - Standby", + 1: "Inverter On", + 2: "Inverter Abnormal, stopping power", + 3: "Inverter Severly Abnormal, 20 seconds to restart", +} + +SAFETY_COUNTRIES: Dict[int, str] = { + 0: "Italy", + 1: "Czechia", + 2: "Germany", + 3: "Spain", + 4: "Greece Mainland", + 5: "Denmark", + 6: "Belgium", + 7: "Romania", + 8: "G98", + 9: "Australia", + 10: "France", + 11: "China", + 12: "60Hz Grid Default", + 13: "Poland", + 14: "South Africa", + 15: "AustraliaL", + 16: "Brazil", + 17: "Thailand MEA", + 18: "Thailand PEA", + 19: "Mauritius", + 20: "Holland", + 21: "G99", + 22: "China Special", + 23: "French 50Hz", + 24: "French 60Hz", + 25: "Australia Ergon", + 26: "Australia Energex", + 27: "Holland 16/20A", + 28: "Korea", + 29: "China Station", + 30: "Austria", + 31: "India", + 32: "50Hz Grid Default", + 33: "Warehouse", + 34: "Philippines", + 35: "Ireland", + 36: "Taiwan", + 37: "Bulgaria", + 38: "Barbados", + 39: "China Special High", + 40: "G99", + 41: "Sweden", + 42: "Chile", + 43: "Brazil LV", + 44: "NewZealand", + 45: "IEEE1547 208VAC", + 46: "IEEE1547 220VAC", + 47: "IEEE1547 240VAC", + 48: "60Hz LV Default", + 49: "50Hz LV Default", + 50: "AU_WAPN", + 51: "AU_MicroGrid", + 52: "JP_50Hz", + 53: "JP_60Hz", + 54: "India Higher", + 55: "DEWA LV", + 56: "DEWA MV", + 57: "Slovakia", + 58: "GreenGrid", + 59: "Hungary", + 60: "Sri Lanka", + 61: "Spain Islands", + 62: "Ergon30K", + 63: "Energex30K", + 64: "IEEE1547 230/400V", + 65: "IEC61727 60Hz", + 66: "Switzerland", + 67: "CEI-016", + 68: "AU_Horizon", + 69: "Cyprus", + 70: "AU_SAPN", + 71: "AU_Ausgrid", + 72: "AU_Essential", + 73: "AU_Pwcore&CitiPW", + 74: "Hong Kong", + 75: "Poland MV", + 76: "Holland MV", + 77: "Sweden MV", + 78: "VDE4110", + 96: "cUSA_208VacDefault", + 97: "cUSA_240VacDefault", + 98: "cUSA_208VacCA_SCE", + 99: "cUSA_240VacCA_SCE", + 100: "cUSA_208VacCA_SDGE", + 101: "cUSA_240VacCA_SDGE", + 102: "cUSA_208VacCA_PGE", + 103: "cUSA_240VacCA_PGE", + 104: "cUSA_208VacHECO_14HO", + 105: "cUSA_240VacHECO_14HO0x69", + 106: "cUSA_208VacHECO_14HM", + 107: "cUSA_240VacHECO_14HM", +} + +ERROR_CODES: Dict[int, str] = { + 31: 'Internal Communication Failure', + 30: 'EEPROM R/W Failure', + 29: 'Fac Failure', + 28: 'DSP communication failure', + 27: 'PhaseAngleFailure', + 26: '', + 25: 'Relay Check Failure', + 24: '', + 23: 'Vac Consistency Failure', + 22: 'Fac Consistency Failure', + 21: '', + 20: 'Back-Up Over Load', + 19: 'DC Injection High', + 18: 'Isolation Failure', + 17: 'Vac Failure', + 16: 'External Fan Failure', + 15: 'PV Over Voltage', + 14: 'Utility Phase Failure', + 13: 'Over Temperature', + 12: 'InternalFan Failure', + 11: 'DC Bus High', + 10: 'Ground I Failure', + 9: 'Utility Loss', + 8: 'AC HCT Failure', + 7: 'Relay Device Failure', + 6: 'GFCI Device Failure', + 5: '', + 4: 'GFCI Consistency Failure', + 3: 'DCI Consistency Failure', + 2: '', + 1: 'AC HCT Check Failure', + 0: 'GFCI Device Check Failure', +} + +DIAG_STATUS_CODES: Dict[int, str] = { + 0: "Battery voltage low", + 1: "Battery SOC low", + 2: "Battery SOC in back", + 3: "BMS: Discharge disabled", + 4: "Discharge time on", + 5: "Charge time on", + 6: "Discharge Driver On", + 7: "BMS: Discharge current low", + 8: "APP: Discharge current too low", + 9: "Meter communication failure", + 10: "Meter connection reversed", + 11: "Self-use load light", + 12: "EMS: discharge current is zero", + 13: "Discharge BUS high PV voltage", + 14: "Battery Disconnected", + 15: "Battery Overcharged", + 16: "BMS: Temperature too high", + 17: "BMS: Charge too high", + 18: "BMS: Charge disabled", + 19: "Self-use off", + 20: "SOC delta too volatile", + 21: "Battery self discharge too high", + 22: "Battery SOC low (off-grid)", + 23: "Grid wave unstable", + 24: "Export power limit set", + 25: "PF value set", + 26: "Real power limit set", + 27: "DC output on", + 28: "SOC protect off", +} + +BMS_ALARM_CODES: Dict[int, str] = { + 15: 'Charging over-voltage 3', + 14: 'Discharging under-voltage 3', + 13: 'Cell temperature high 3', + 12: 'Communication failure 2', + 11: 'Charging circuit failure', + 10: 'Discharging circuit failure', + 9: 'Battery lock', + 8: 'Battery break', + 7: 'DC bus fault', + 6: 'Precharge fault', + 5: 'Discharging over-current 2', + 4: 'Charging over-current 2', + 3: 'Cell temperature low 2', + 2: 'Cell temperature high 2', + 1: 'Discharging under-voltage 2', + 0: 'Charging over-voltage 2', +} + +BMS_WARNING_CODES: Dict[int, str] = { + 11: 'System temperature high', + 10: 'System temperature low 2', + 9: 'System temperature low 1', + 8: 'Cell imbalance', + 7: 'System reboot', + 6: 'Communication failure 1', + 5: 'Discharging over-current 1', + 4: 'Charging over-current 1', + 3: 'Cell temperature low 1', + 2: 'Cell temperature high 1', + 1: 'Discharging under-voltage 1', + 0: 'Charging over-voltage 1', +} diff --git a/goodwe/dt.py b/goodwe/dt.py new file mode 100644 index 0000000..0537887 --- /dev/null +++ b/goodwe/dt.py @@ -0,0 +1,235 @@ +from __future__ import annotations + +from typing import Tuple + +from .exceptions import InverterError +from .inverter import Inverter +from .inverter import OperationMode +from .inverter import SensorKind as Kind +from .model import is_3_mptt, is_single_phase +from .protocol import ProtocolCommand, ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand +from .sensor import * + + +class DT(Inverter): + """Class representing inverter of DT/MS/D-NS/XS or GE's GEP(PSB/PSC) families""" + + __all_sensors: Tuple[Sensor, ...] = ( + Timestamp("timestamp", 0, "Timestamp"), + Voltage("vpv1", 6, "PV1 Voltage", Kind.PV), + Current("ipv1", 8, "PV1 Current", Kind.PV), + Calculated("ppv1", + lambda data: round(read_voltage(data, 6) * read_current(data, 8)), + "PV1 Power", "W", Kind.PV), + Voltage("vpv2", 10, "PV2 Voltage", Kind.PV), + Current("ipv2", 12, "PV2 Current", Kind.PV), + Calculated("ppv2", + lambda data: round(read_voltage(data, 10) * read_current(data, 12)), + "PV2 Power", "W", Kind.PV), + Voltage("vpv3", 14, "PV3 Voltage", Kind.PV), + Current("ipv3", 16, "PV3 Current", Kind.PV), + Calculated("ppv3", + lambda data: round(read_voltage(data, 14) * read_current(data, 16)), + "PV3 Power", "W", Kind.PV), + # Voltage("vpv4", 14, "PV4 Voltage", Kind.PV), + # Current("ipv4", 16, "PV4 Current", Kind.PV), + # Voltage("vpv5", 14, "PV5 Voltage", Kind.PV), + # Current("ipv5", 16, "PV5 Current", Kind.PV), + # Voltage("vpv6", 14, "PV6 Voltage", Kind.PV), + # Current("ipv6", 16, "PV7 Current", Kind.PV), + Voltage("vline1", 30, "On-grid L1-L2 Voltage", Kind.AC), + Voltage("vline2", 32, "On-grid L2-L3 Voltage", Kind.AC), + Voltage("vline3", 34, "On-grid L3-L1 Voltage", Kind.AC), + Voltage("vgrid1", 36, "On-grid L1 Voltage", Kind.AC), + Voltage("vgrid2", 38, "On-grid L2 Voltage", Kind.AC), + Voltage("vgrid3", 40, "On-grid L3 Voltage", Kind.AC), + Current("igrid1", 42, "On-grid L1 Current", Kind.AC), + Current("igrid2", 44, "On-grid L2 Current", Kind.AC), + Current("igrid3", 46, "On-grid L3 Current", Kind.AC), + Frequency("fgrid1", 48, "On-grid L1 Frequency", Kind.AC), + Frequency("fgrid2", 50, "On-grid L2 Frequency", Kind.AC), + Frequency("fgrid3", 52, "On-grid L3 Frequency", Kind.AC), + Calculated("pgrid1", + lambda data: round(read_voltage(data, 36) * read_current(data, 42)), + "On-grid L1 Power", "W", Kind.AC), + Calculated("pgrid2", + lambda data: round(read_voltage(data, 38) * read_current(data, 44)), + "On-grid L2 Power", "W", Kind.AC), + Calculated("pgrid3", + lambda data: round(read_voltage(data, 40) * read_current(data, 46)), + "On-grid L3 Power", "W", Kind.AC), + Integer("xx54", 54, "Unknown sensor@54"), + Power("ppv", 56, "PV Power", Kind.PV), + Integer("work_mode", 58, "Work Mode code"), + Enum2("work_mode_label", 58, WORK_MODES, "Work Mode"), + Long("error_codes", 60, "Error Codes"), + Integer("warning_code", 64, "Warning code"), + Integer("xx66", 66, "Unknown sensor@66"), + Integer("xx68", 68, "Unknown sensor@68"), + Integer("xx70", 70, "Unknown sensor@70"), + Integer("xx72", 72, "Unknown sensor@72"), + Integer("xx74", 74, "Unknown sensor@74"), + Integer("xx76", 76, "Unknown sensor@76"), + Integer("xx78", 78, "Unknown sensor@78"), + Integer("xx80", 80, "Unknown sensor@80"), + Temp("temperature", 82, "Inverter Temperature", Kind.AC), + Integer("xx84", 84, "Unknown sensor@84"), + Integer("xx86", 86, "Unknown sensor@86"), + Energy("e_day", 88, "Today's PV Generation", Kind.PV), + Energy4("e_total", 90, "Total PV Generation", Kind.PV), + Long("h_total", 94, "Hours Total", "h", Kind.PV), + Integer("safety_country", 98, "Safety Country code", "", Kind.AC), + Enum2("safety_country_label", 98, SAFETY_COUNTRIES, "Safety Country", Kind.AC), + Integer("xx100", 100, "Unknown sensor@100"), + Integer("xx102", 102, "Unknown sensor@102"), + Integer("xx104", 104, "Unknown sensor@104"), + Integer("xx106", 106, "Unknown sensor@106"), + Integer("xx108", 108, "Unknown sensor@108"), + Integer("xx110", 110, "Unknown sensor@110"), + Integer("xx112", 112, "Unknown sensor@112"), + Integer("xx114", 114, "Unknown sensor@114"), + Integer("xx116", 116, "Unknown sensor@116"), + Integer("xx118", 118, "Unknown sensor@118"), + Integer("xx120", 120, "Unknown sensor@120"), + Integer("xx122", 122, "Unknown sensor@122"), + Integer("funbit", 124, "FunBit", "", Kind.PV), + Voltage("vbus", 126, "Bus Voltage", Kind.PV), + Voltage("vnbus", 128, "NBus Voltage", Kind.PV), + Integer("xx130", 130, "Unknown sensor@130"), + Integer("xx132", 132, "Unknown sensor@132"), + Integer("xx134", 134, "Unknown sensor@134"), + Integer("xx136", 136, "Unknown sensor@136"), + Integer("xx138", 138, "Unknown sensor@138"), + Integer("xx140", 140, "Unknown sensor@140"), + Integer("xx142", 142, "Unknown sensor@142"), + Integer("xx144", 144, "Unknown sensor@144"), + ) + + # Modbus registers of inverter settings, offsets are modbus register addresses + __all_settings: Tuple[Sensor, ...] = ( + Timestamp("time", 40313, "Inverter time"), + + Integer("shadow_scan", 40326, "Shadow Scan", "", Kind.PV), + Integer("grid_export", 40327, "Grid Export Enabled", "", Kind.GRID), + Integer("grid_export_limit", 40328, "Grid Export Limit", "%", Kind.GRID), + ) + + # Settings for single phase inverters + __settings_single_phase: Tuple[Sensor, ...] = ( + Long("grid_export_limit", 40328, "Grid Export Limit", "W", Kind.GRID), + ) + + # Settings for three phase inverters + __settings_three_phase: Tuple[Sensor, ...] = ( + Integer("grid_export_limit", 40336, "Grid Export Limit", "%", Kind.GRID), + ) + + def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3): + super().__init__(host, comm_addr, timeout, retries) + if not self.comm_addr: + # Set the default inverter address + self.comm_addr = 0x7f + self._READ_DEVICE_VERSION_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x7531, 0x0028) + self._READ_DEVICE_RUNNING_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x7594, 0x0049) + self._sensors = self.__all_sensors + self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings} + + @staticmethod + def _single_phase_only(s: Sensor) -> bool: + """Filter to exclude phase2/3 sensors on single phase inverters""" + return not ((s.id_.endswith('2') or s.id_.endswith('3')) and 'pv' not in s.id_ and not s.id_.startswith('xx')) + + @staticmethod + def _pv1_pv2_only(s: Sensor) -> bool: + """Filter to exclude sensors on < 3 PV inverters""" + return not s.id_.endswith('pv3') + + async def read_device_info(self): + response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO) + response = response[5:-2] + try: + self.model_name = response[22:32].decode("ascii").rstrip() + except: + print("No model name sent from the inverter.") + self.serial_number = response[6:22].decode("ascii") + self.dsp1_version = read_unsigned_int(response, 66) + self.dsp2_version = read_unsigned_int(response, 68) + self.arm_version = read_unsigned_int(response, 70) + self.firmware = "{}.{}.{:02x}".format(self.dsp1_version, self.dsp2_version, self.arm_version) + + if is_single_phase(self): + # this is single phase inverter, filter out all L2 and L3 sensors + self._sensors = tuple(filter(self._single_phase_only, self.__all_sensors)) + self._settings.update({s.id_: s for s in self.__settings_single_phase}) + else: + self._settings.update({s.id_: s for s in self.__settings_three_phase}) + + if is_3_mptt(self): + # this is 3 PV strings inverter, keep all sensors + pass + else: + # this is only 2 PV strings inverter + self._sensors = tuple(filter(self._pv1_pv2_only, self._sensors)) + pass + + async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict[str, Any]: + raw_data = await self._read_from_socket(self._READ_DEVICE_RUNNING_DATA) + data = self._map_response(raw_data[5:-2], self._sensors, include_unknown_sensors) + return data + + async def read_setting(self, setting_id: str) -> Any: + setting = self._settings.get(setting_id) + if not setting: + raise ValueError(f'Unknown setting "{setting_id}"') + count = (setting.size_ + (setting.size_ % 2)) // 2 + raw_data = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) + with io.BytesIO(raw_data[5:-2]) as buffer: + return setting.read_value(buffer) + + async def write_setting(self, setting_id: str, value: Any): + setting = self._settings.get(setting_id) + if not setting: + raise ValueError(f'Unknown setting "{setting_id}"') + raw_value = setting.encode_value(value) + if len(raw_value) <= 2: + value = int.from_bytes(raw_value, byteorder="big", signed=True) + await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value)) + else: + await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value)) + + async def read_settings_data(self) -> Dict[str, Any]: + data = {} + for setting in self.settings(): + value = await self.read_setting(setting.id_) + data[setting.id_] = value + return data + + async def get_grid_export_limit(self) -> int: + return await self.read_setting('grid_export_limit') + + async def set_grid_export_limit(self, export_limit: int) -> None: + setting = self._settings.get('grid_export_limit') + if (setting.unit == "%" and 0 <= export_limit <= 100) or (setting.unit != "%" and 0 <= export_limit <= 10000): + return await self.write_setting('grid_export_limit', export_limit) + + async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]: + return () + + async def get_operation_mode(self) -> OperationMode: + raise InverterError("Operation not supported.") + + async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power: int = 100, + eco_mode_soc: int = 100) -> None: + raise InverterError("Operation not supported.") + + async def get_ongrid_battery_dod(self) -> int: + raise InverterError("Operation not supported, inverter has no batteries.") + + async def set_ongrid_battery_dod(self, dod: int) -> None: + raise InverterError("Operation not supported, inverter has no batteries.") + + def sensors(self) -> Tuple[Sensor, ...]: + return self._sensors + + def settings(self) -> Tuple[Sensor, ...]: + return tuple(self._settings.values()) diff --git a/goodwe/es.py b/goodwe/es.py new file mode 100644 index 0000000..de4156d --- /dev/null +++ b/goodwe/es.py @@ -0,0 +1,440 @@ +from __future__ import annotations + +import logging +from typing import Tuple, cast + +from .exceptions import InverterError +from .inverter import Inverter +from .inverter import OperationMode +from .inverter import SensorKind as Kind +from .protocol import ProtocolCommand, Aa55ProtocolCommand, Aa55ReadCommand, Aa55WriteCommand, Aa55WriteMultiCommand, \ + ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand +from .sensor import * + +logger = logging.getLogger(__name__) + + +class ES(Inverter): + """Class representing inverter of ES/EM/BP family""" + + _READ_DEVICE_VERSION_INFO: ProtocolCommand = Aa55ProtocolCommand("010200", "0182") + _READ_DEVICE_RUNNING_DATA: ProtocolCommand = Aa55ProtocolCommand("010600", "0186") + _READ_DEVICE_SETTINGS_DATA: ProtocolCommand = Aa55ProtocolCommand("010900", "0189") + + __sensors: Tuple[Sensor, ...] = ( + Voltage("vpv1", 0, "PV1 Voltage", Kind.PV), # modbus 0x500 + Current("ipv1", 2, "PV1 Current", Kind.PV), + Calculated("ppv1", + lambda data: round(read_voltage(data, 0) * read_current(data, 2)), + "PV1 Power", "W", Kind.PV), + Byte("pv1_mode", 4, "PV1 Mode code", "", Kind.PV), + Enum("pv1_mode_label", 4, PV_MODES, "PV1 Mode", Kind.PV), + Voltage("vpv2", 5, "PV2 Voltage", Kind.PV), + Current("ipv2", 7, "PV2 Current", Kind.PV), + Calculated("ppv2", + lambda data: round(read_voltage(data, 5) * read_current(data, 7)), + "PV2 Power", "W", Kind.PV), + Byte("pv2_mode", 9, "PV2 Mode code", "", Kind.PV), + Enum("pv2_mode_label", 9, PV_MODES, "PV2 Mode", Kind.PV), + Calculated("ppv", + lambda data: round(read_voltage(data, 0) * read_current(data, 2)) + round( + read_voltage(data, 5) * read_current(data, 7)), + "PV Power", "W", Kind.PV), + Voltage("vbattery1", 10, "Battery Voltage", Kind.BAT), # modbus 0x506 + # Voltage("vbattery2", 12, "Battery Voltage 2", Kind.BAT), + Integer("battery_status", 14, "Battery Status", "", Kind.BAT), + Temp("battery_temperature", 16, "Battery Temperature", Kind.BAT), + Calculated("ibattery1", + lambda data: abs(read_current(data, 18)) * (-1 if read_byte(data, 30) == 3 else 1), + "Battery Current", "A", Kind.BAT), + # round(vbattery1 * ibattery1), + Calculated("pbattery1", + lambda data: abs( + round(read_voltage(data, 10) * read_current(data, 18)) + ) * (-1 if read_byte(data, 30) == 3 else 1), + "Battery Power", "W", Kind.BAT), + Integer("battery_charge_limit", 20, "Battery Charge Limit", "A", Kind.BAT), + Integer("battery_discharge_limit", 22, "Battery Discharge Limit", "A", Kind.BAT), + Integer("battery_error", 24, "Battery Error Code", "", Kind.BAT), + Byte("battery_soc", 26, "Battery State of Charge", "%", Kind.BAT), # modbus 0x50E + # Byte("cbattery2", 27, "Battery State of Charge 2", "%", Kind.BAT), + # Byte("cbattery3", 28, "Battery State of Charge 3", "%", Kind.BAT), + Byte("battery_soh", 29, "Battery State of Health", "%", Kind.BAT), + Byte("battery_mode", 30, "Battery Mode code", "", Kind.BAT), + Enum("battery_mode_label", 30, BATTERY_MODES, "Battery Mode", Kind.BAT), + Integer("battery_warning", 31, "Battery Warning", "", Kind.BAT), + Byte("meter_status", 33, "Meter Status code", "", Kind.AC), + Voltage("vgrid", 34, "On-grid Voltage", Kind.AC), + Current("igrid", 36, "On-grid Current", Kind.AC), + Calculated("pgrid", + lambda data: abs(read_bytes2(data, 38)) * (-1 if read_byte(data, 80) == 2 else 1), + "On-grid Export Power", "W", Kind.AC), + Frequency("fgrid", 40, "On-grid Frequency", Kind.AC), + Byte("grid_mode", 42, "Work Mode code", "", Kind.GRID), + Enum("grid_mode_label", 42, WORK_MODES_ES, "Work Mode", Kind.GRID), + Voltage("vload", 43, "Back-up Voltage", Kind.UPS), # modbus 0x51b + Current("iload", 45, "Back-up Current", Kind.UPS), + Power("pload", 47, "On-grid Power", Kind.AC), + Frequency("fload", 49, "Back-up Frequency", Kind.UPS), + Byte("load_mode", 51, "Load Mode code", "", Kind.AC), + Enum("load_mode_label", 51, LOAD_MODES, "Load Mode", Kind.AC), + Byte("work_mode", 52, "Energy Mode code", "", Kind.AC), + Enum("work_mode_label", 52, ENERGY_MODES, "Energy Mode", Kind.AC), + Temp("temperature", 53, "Inverter Temperature"), + Long("error_codes", 55, "Error Codes"), + Energy4("e_total", 59, "Total PV Generation", Kind.PV), + Long("h_total", 63, "Hours Total", "h", Kind.PV), + Energy("e_day", 67, "Today's PV Generation", Kind.PV), + Energy("e_load_day", 69, "Today's Load", Kind.AC), + Energy4("e_load_total", 71, "Total Load", Kind.AC), + Power("total_power", 75, "Total Power", Kind.AC), # modbus 0x52c + Byte("effective_work_mode", 77, "Effective Work Mode code"), + Integer("effective_relay_control", 78, "Effective Relay Control", "", None), + Byte("grid_in_out", 80, "On-grid Mode code", "", Kind.GRID), + Enum("grid_in_out_label", 80, GRID_IN_OUT_MODES, "On-grid Mode", Kind.GRID), + Power("pback_up", 81, "Back-up Power", Kind.UPS), + # pload + pback_up + Calculated("plant_power", + lambda data: round(read_bytes2(data, 47) + read_bytes2(data, 81)), + "Plant Power", "W", Kind.AC), + Decimal("meter_power_factor", 83, 1000, "Meter Power Factor", "", Kind.GRID), # modbus 0x531 + Integer("xx85", 85, "Unknown sensor@85"), + Integer("xx87", 87, "Unknown sensor@87"), + Long("diagnose_result", 89, "Diag Status Code"), + EnumBitmap4("diagnose_result_label", 89, DIAG_STATUS_CODES, "Diag Status"), + # Energy4("e_total_exp", 93, "Total Energy (export)", Kind.GRID), + # Energy4("e_total_imp", 97, "Total Energy (import)", Kind.GRID), + # Voltage("vpv3", 101, "PV3 Voltage", Kind.PV), # modbus 0x500 + # Current("ipv3", 103, "PV3 Current", Kind.PV), + # Byte("pv3_mode", 104, "PV1 Mode", "", Kind.PV), + # Voltage("vgrid_uo", 105, "On-grid Uo Voltage", Kind.AC), + # Current("igrid_uo", 107, "On-grid Uo Current", Kind.AC), + # Voltage("vgrid_wo", 109, "On-grid Wo Voltage", Kind.AC), + # Current("igrid_wo", 111, "On-grid Wo Current", Kind.AC), + # Energy4("e_bat_charge_total", 113, "Total Battery Charge", Kind.BAT), + # Energy4("e_bat_discharge_total", 117, "Total Battery Discharge", Kind.BAT), + + # ppv1 + ppv2 + pbattery - pgrid + Calculated("house_consumption", + lambda data: + round(read_voltage(data, 0) * read_current(data, 2)) + + round(read_voltage(data, 5) * read_current(data, 7)) + + (abs(round(read_voltage(data, 10) * read_current(data, 18))) * + (-1 if read_byte(data, 30) == 3 else 1)) - + (abs(read_bytes2(data, 38)) * (-1 if read_byte(data, 80) == 2 else 1)), + "House Consumption", "W", Kind.AC), + ) + + __all_settings: Tuple[Sensor, ...] = ( + Integer("backup_supply", 12, "Backup Supply"), + Integer("off-grid_charge", 14, "Off-grid Charge"), + Integer("shadow_scan", 16, "Shadow Scan", "", Kind.PV), + Integer("grid_export", 18, "Grid Export Enabled", "", Kind.GRID), + Integer("capacity", 22, "Capacity"), + Integer("charge_v", 24, "Charge Voltage", "V"), + Integer("charge_i", 26, "Charge Current", "A", ), + Integer("discharge_i", 28, "Discharge Current", "A", ), + Integer("discharge_v", 30, "Discharge Voltage", "V"), + Calculated("dod", lambda data: 100 - read_bytes2(data, 32), "Depth of Discharge", "%"), + Integer("battery_activated", 34, "Battery Activated"), + Integer("bp_off_grid_charge", 36, "BP Off-grid Charge"), + Integer("bp_pv_discharge", 38, "BP PV Discharge"), + Integer("bp_bms_protocol", 40, "BP BMS Protocol"), + Integer("power_factor", 42, "Power Factor"), + Integer("grid_export_limit", 52, "Grid Export Limit", "W", Kind.GRID), + Integer("battery_soc_protection", 56, "Battery SoC Protection", "", Kind.BAT), + Integer("work_mode", 66, "Work Mode"), + Integer("grid_quality_check", 68, "Grid Quality Check"), + + EcoModeV1("eco_mode_1", 1793, "Eco Mode Group 1"), # 0x701 + ByteH("eco_mode_1_switch", 1796, "Eco Mode Group 1 Switch", "", Kind.BAT), + EcoModeV1("eco_mode_2", 1797, "Eco Mode Group 2"), + ByteH("eco_mode_2_switch", 1800, "Eco Mode Group 2 Switch", "", Kind.BAT), + EcoModeV1("eco_mode_3", 1801, "Eco Mode Group 3"), + ByteH("eco_mode_3_switch", 1804, "Eco Mode Group 3 Switch", "", Kind.BAT), + EcoModeV1("eco_mode_4", 1805, "Eco Mode Group 4"), + ByteH("eco_mode_4_switch", 1808, "Eco Mode Group 4 Switch", "", Kind.BAT), + ) + + # Settings added in ARM firmware 14 + __settings_arm_fw_14: Tuple[Sensor, ...] = ( + EcoModeV2("eco_mode_1", 47547, "Eco Mode Group 1"), + ByteH("eco_mode_1_switch", 47549, "Eco Mode Group 1 Switch"), + EcoModeV2("eco_mode_2", 47553, "Eco Mode Group 2"), + ByteH("eco_mode_2_switch", 47555, "Eco Mode Group 2 Switch"), + EcoModeV2("eco_mode_3", 47559, "Eco Mode Group 3"), + ByteH("eco_mode_3_switch", 47561, "Eco Mode Group 3 Switch"), + EcoModeV2("eco_mode_4", 47565, "Eco Mode Group 4"), + ByteH("eco_mode_4_switch", 47567, "Eco Mode Group 4 Switch"), + ) + + def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3): + super().__init__(host, comm_addr, timeout, retries) + if not self.comm_addr: + # Set the default inverter address + self.comm_addr = 0xf7 + self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings} + + def _supports_eco_mode_v2(self) -> bool: + if self.arm_version < 14: + return False + if "EMU" in self.serial_number: + return self.dsp1_version >= 11 + if "ESU" in self.serial_number: + return self.dsp1_version >= 22 + if "BPS" in self.serial_number: + return self.dsp1_version >= 10 + return False + + async def read_device_info(self): + response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO) + self.firmware = self._decode(response[7:12]).rstrip() + self.model_name = self._decode(response[12:22]).rstrip() + self.serial_number = response[38:54].decode("ascii") + self.software_version = self._decode(response[58:70]) + try: + if len(self.firmware) >= 2: + self.dsp1_version = int(self.firmware[0:2]) + if len(self.firmware) >= 4: + self.dsp2_version = int(self.firmware[2:4]) + if len(self.firmware) >= 5: + self.arm_version = int(self.firmware[4], base=36) + except ValueError: + logger.exception("Error decoding firmware version %s.", self.firmware) + + if self._supports_eco_mode_v2(): + self._settings.update({s.id_: s for s in self.__settings_arm_fw_14}) + + async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict[str, Any]: + raw_data = await self._read_from_socket(self._READ_DEVICE_RUNNING_DATA) + data = self._map_response(raw_data[7:-2], self.__sensors, include_unknown_sensors) + return data + + async def read_setting(self, setting_id: str) -> Any: + if setting_id == 'time': + # Fake setting, just to enable write_setting to work (if checked as pair in read as in HA) + # There does not seem to be time setting/sensor available (or is not known) + return datetime.now() + elif setting_id in ('eco_mode_1', 'eco_mode_2', 'eco_mode_3', 'eco_mode_4'): + setting: Sensor | None = self._settings.get(setting_id) + if not setting: + raise ValueError(f'Unknown setting "{setting_id}"') + count = (setting.size_ + (setting.size_ % 2)) // 2 + if self._is_modbus_setting(setting): + raw_data = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) + with io.BytesIO(raw_data[5:-2]) as buffer: + return setting.read_value(buffer) + else: + raw_data = await self._read_from_socket(Aa55ReadCommand(setting.offset, count)) + with io.BytesIO(raw_data[7:-2]) as buffer: + return setting.read_value(buffer) + else: + all_settings = await self.read_settings_data() + return all_settings.get(setting_id) + + async def write_setting(self, setting_id: str, value: Any): + if setting_id == 'time': + await self._read_from_socket( + Aa55ProtocolCommand("030206" + Timestamp("time", 0, "").encode_value(value).hex(), "0382") + ) + else: + setting: Sensor | None = self._settings.get(setting_id) + if not setting: + raise ValueError(f'Unknown setting "{setting_id}"') + if setting.size_ == 1: + # modbus can address/store only 16 bit values, read the other 8 bytes + if self._is_modbus_setting(setting): + register_data = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1)) + raw_value = setting.encode_value(value, register_data[5:7]) + else: + register_data = await self._read_from_socket(Aa55ReadCommand(self.comm_addr, setting.offset, 1)) + raw_value = setting.encode_value(value, register_data[7:9]) + else: + raw_value = setting.encode_value(value) + if len(raw_value) <= 2: + value = int.from_bytes(raw_value, byteorder="big", signed=True) + if self._is_modbus_setting(setting): + await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value)) + else: + await self._read_from_socket(Aa55WriteCommand(setting.offset, value)) + else: + if self._is_modbus_setting(setting): + await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value)) + else: + await self._read_from_socket(Aa55WriteMultiCommand(setting.offset, raw_value)) + + async def read_settings_data(self) -> Dict[str, Any]: + raw_data = await self._read_from_socket(self._READ_DEVICE_SETTINGS_DATA) + data = self._map_response(raw_data[7:-2], self.settings()) + return data + + async def get_grid_export_limit(self) -> int: + return await self.read_setting('grid_export_limit') + + async def set_grid_export_limit(self, export_limit: int) -> None: + if 0 <= export_limit <= 10000: + await self._read_from_socket( + Aa55ProtocolCommand("033502" + "{:04x}".format(export_limit), "03b5") + ) + + async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]: + result = [e for e in OperationMode] + result.remove(OperationMode.PEAK_SHAVING) + if not include_emulated: + result.remove(OperationMode.ECO_CHARGE) + result.remove(OperationMode.ECO_DISCHARGE) + return tuple(result) + + async def get_operation_mode(self) -> OperationMode: + mode = OperationMode(await self.read_setting('work_mode')) + if OperationMode.ECO != mode: + return mode + ecomode = await self.read_setting('eco_mode_1') + if ecomode.is_eco_charge_mode(): + return OperationMode.ECO_CHARGE + elif ecomode.is_eco_discharge_mode(): + return OperationMode.ECO_DISCHARGE + else: + return OperationMode.ECO + + async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power: int = 100, + eco_mode_soc: int = 100) -> None: + if operation_mode == OperationMode.GENERAL: + await self._set_general_mode() + elif operation_mode == OperationMode.OFF_GRID: + await self._set_offgrid_mode() + elif operation_mode == OperationMode.BACKUP: + await self._set_backup_mode() + elif operation_mode == OperationMode.ECO: + await self._set_eco_mode() + elif operation_mode == OperationMode.PEAK_SHAVING: + raise InverterError("Operation not supported.") + elif operation_mode in (OperationMode.ECO_CHARGE, OperationMode.ECO_DISCHARGE): + if eco_mode_power < 0 or eco_mode_power > 100: + raise ValueError() + if eco_mode_soc < 0 or eco_mode_soc > 100: + raise ValueError() + eco_mode: EcoMode = self._convert_eco_mode(EcoModeV2("", 0, "")) + if operation_mode == OperationMode.ECO_CHARGE: + await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc)) + else: + await self.write_setting('eco_mode_1', eco_mode.encode_discharge(eco_mode_power)) + await self.write_setting('eco_mode_2_switch', 0) + await self.write_setting('eco_mode_3_switch', 0) + await self.write_setting('eco_mode_4_switch', 0) + await self._set_eco_mode() + + async def get_ongrid_battery_dod(self) -> int: + return await self.read_setting('dod') + + async def set_ongrid_battery_dod(self, dod: int) -> None: + if 0 <= dod <= 89: + await self._read_from_socket(Aa55WriteCommand(0x560, 100 - dod)) + + async def _reset_inverter(self) -> None: + await self._read_from_socket(Aa55ProtocolCommand("031d00", "039d")) + + def sensors(self) -> Tuple[Sensor, ...]: + return self.__sensors + + def settings(self) -> Tuple[Sensor, ...]: + return tuple(self._settings.values()) + + async def _set_general_mode(self) -> None: + if self.arm_version >= 7: + if self._supports_eco_mode_v2(): + await self._clear_battery_mode_param() + else: + await self._set_limit_power_for_charge(0, 0, 0, 0, 0) + await self._set_limit_power_for_discharge(0, 0, 0, 0, 0) + await self._clear_battery_mode_param() + else: + await self._set_limit_power_for_charge(0, 0, 0, 0, 0) + await self._set_limit_power_for_discharge(0, 0, 0, 0, 0) + await self._set_offgrid_work_mode(0) + await self._set_work_mode(0) + + async def _set_offgrid_mode(self) -> None: + if self.arm_version >= 7: + await self._clear_battery_mode_param() + else: + await self._set_limit_power_for_charge(0, 0, 23, 59, 0) + await self._set_limit_power_for_discharge(0, 0, 0, 0, 0) + await self._set_offgrid_work_mode(1) + await self._set_relay_control(3) + await self._set_store_energy_mode(0) + await self._set_work_mode(1) + + async def _set_backup_mode(self) -> None: + if self.arm_version >= 7: + if self._supports_eco_mode_v2(): + await self._clear_battery_mode_param() + else: + await self._clear_battery_mode_param() + await self._set_limit_power_for_charge(0, 0, 23, 59, 10) + else: + await self._set_limit_power_for_charge(0, 0, 23, 59, 10) + await self._set_limit_power_for_discharge(0, 0, 0, 0, 0) + await self._set_offgrid_work_mode(0) + await self._set_work_mode(2) + + async def _set_eco_mode(self) -> None: + await self._set_offgrid_work_mode(0) + await self._set_work_mode(3) + + async def _clear_battery_mode_param(self) -> None: + await self._read_from_socket(Aa55WriteCommand(0x0700, 1)) + + async def _set_limit_power_for_charge(self, startH: int, startM: int, stopH: int, stopM: int, limit: int) -> None: + if limit < 0 or limit > 100: + raise ValueError() + await self._read_from_socket(Aa55ProtocolCommand("032c05" + + "{:02x}".format(startH) + "{:02x}".format(startM) + + "{:02x}".format(stopH) + "{:02x}".format(stopM) + + "{:02x}".format(limit), "03AC")) + + async def _set_limit_power_for_discharge(self, startH: int, startM: int, stopH: int, stopM: int, + limit: int) -> None: + if limit < 0 or limit > 100: + raise ValueError() + await self._read_from_socket(Aa55ProtocolCommand("032d05" + + "{:02x}".format(startH) + "{:02x}".format(startM) + + "{:02x}".format(stopH) + "{:02x}".format(stopM) + + "{:02x}".format(limit), "03AD")) + + async def _set_offgrid_work_mode(self, mode: int) -> None: + await self._read_from_socket(Aa55ProtocolCommand("033601" + "{:02x}".format(mode), "03B6")) + + async def _set_relay_control(self, mode: int) -> None: + param = 0 + if mode == 2: + param = 16 + elif mode == 3: + param = 48 + await self._read_from_socket(Aa55ProtocolCommand("03270200" + "{:02x}".format(param), "03B7")) + + async def _set_store_energy_mode(self, mode: int) -> None: + param = 0 + if mode == 0: + param = 4 + elif mode == 1: + param = 2 + elif mode == 2: + param = 8 + elif mode == 3: + param = 1 + await self._read_from_socket(Aa55ProtocolCommand("032601" + "{:02x}".format(param), "03B6")) + + async def _set_work_mode(self, mode: int) -> None: + await self._read_from_socket(Aa55ProtocolCommand("035901" + "{:02x}".format(mode), "03D9")) + + def _convert_eco_mode(self, sensor: Sensor) -> Sensor | EcoMode: + if EcoModeV1 == type(sensor) and self._supports_eco_mode_v2(): + return cast(EcoModeV1, sensor).as_eco_mode_v2() + elif EcoModeV2 == type(sensor) and not self._supports_eco_mode_v2(): + return cast(EcoModeV2, sensor).as_eco_mode_v1() + else: + return sensor + + def _is_modbus_setting(self, sensor: Sensor) -> bool: + return EcoModeV2 == type(sensor) or sensor.offset > 30000 diff --git a/goodwe/et.py b/goodwe/et.py new file mode 100644 index 0000000..dc80fc4 --- /dev/null +++ b/goodwe/et.py @@ -0,0 +1,508 @@ +from __future__ import annotations + +import logging +from typing import Tuple, cast + +from .inverter import Inverter +from .inverter import OperationMode +from .inverter import SensorKind as Kind +from .model import is_4_mptt, is_single_phase +from .protocol import ProtocolCommand, ModbusReadCommand, ModbusWriteCommand, ModbusWriteMultiCommand +from .sensor import * + +logger = logging.getLogger(__name__) + + +class ET(Inverter): + """Class representing inverter of ET/EH/BT/BH or GE's GEH families""" + + # Modbus registers from offset 0x891c (35100), count 0x7d (125) + __all_sensors: Tuple[Sensor, ...] = ( + Timestamp("timestamp", 0, "Timestamp"), + Voltage("vpv1", 6, "PV1 Voltage", Kind.PV), + Current("ipv1", 8, "PV1 Current", Kind.PV), + Power4("ppv1", 10, "PV1 Power", Kind.PV), + Voltage("vpv2", 14, "PV2 Voltage", Kind.PV), + Current("ipv2", 16, "PV2 Current", Kind.PV), + Power4("ppv2", 18, "PV2 Power", Kind.PV), + Voltage("vpv3", 22, "PV3 Voltage", Kind.PV), # modbus35111 + Current("ipv3", 24, "PV3 Current", Kind.PV), + Power4("ppv3", 26, "PV3 Power", Kind.PV), + Voltage("vpv4", 30, "PV4 Voltage", Kind.PV), + Current("ipv4", 32, "PV4 Current", Kind.PV), + Power4("ppv4", 34, "PV4 Power", Kind.PV), + # ppv1 + ppv2 + ppv3 + ppv4 + Calculated("ppv", + lambda data: + read_bytes4(data, 10) + + read_bytes4(data, 18) + + read_bytes4(data, 26) + + read_bytes4(data, 34), + "PV Power", "W", Kind.PV), + Byte("pv4_mode", 38, "PV4 Mode code", "", Kind.PV), + Enum("pv4_mode_label", 38, PV_MODES, "PV4 Mode", Kind.PV), + Byte("pv3_mode", 39, "PV3 Mode code", "", Kind.PV), + Enum("pv3_mode_label", 39, PV_MODES, "PV3 Mode", Kind.PV), + Byte("pv2_mode", 40, "PV2 Mode code", "", Kind.PV), + Enum("pv2_mode_label", 40, PV_MODES, "PV2 Mode", Kind.PV), + Byte("pv1_mode", 41, "PV1 Mode code", "", Kind.PV), + Enum("pv1_mode_label", 41, PV_MODES, "PV1 Mode", Kind.PV), + Voltage("vgrid", 42, "On-grid L1 Voltage", Kind.AC), # modbus 35121 + Current("igrid", 44, "On-grid L1 Current", Kind.AC), + Frequency("fgrid", 46, "On-grid L1 Frequency", Kind.AC), + # 48 reserved + Power("pgrid", 50, "On-grid L1 Power", Kind.AC), + Voltage("vgrid2", 52, "On-grid L2 Voltage", Kind.AC), + Current("igrid2", 54, "On-grid L2 Current", Kind.AC), + Frequency("fgrid2", 56, "On-grid L2 Frequency", Kind.AC), + # 58 reserved + Power("pgrid2", 60, "On-grid L2 Power", Kind.AC), + Voltage("vgrid3", 62, "On-grid L3 Voltage", Kind.AC), + Current("igrid3", 64, "On-grid L3 Current", Kind.AC), + Frequency("fgrid3", 66, "On-grid L3 Frequency", Kind.AC), + # 68 reserved + Power("pgrid3", 70, "On-grid L3 Power", Kind.AC), + Integer("grid_mode", 72, "Grid Mode code", "", Kind.PV), + Enum2("grid_mode_label", 72, GRID_MODES, "Grid Mode", Kind.PV), + # 74 reserved + Power("total_inverter_power", 76, "Total Power", Kind.AC), + # 78 reserved + Power("active_power", 80, "Active Power", Kind.GRID), + Calculated("grid_in_out", + lambda data: read_grid_mode(data, 80), + "On-grid Mode code", "", Kind.GRID), + EnumCalculated("grid_in_out_label", + lambda data: read_grid_mode(data, 80), GRID_IN_OUT_MODES, + "On-grid Mode", Kind.GRID), + # 82 reserved + Reactive("reactive_power", 84, "Reactive Power", Kind.GRID), + # 86 reserved + Apparent("apparent_power", 88, "Apparent Power", Kind.GRID), + Voltage("backup_v1", 90, "Back-up L1 Voltage", Kind.UPS), # modbus 35145 + Current("backup_i1", 92, "Back-up L1 Current", Kind.UPS), + Frequency("backup_f1", 94, "Back-up L1 Frequency", Kind.UPS), + Integer("load_mode1", 96, "Load Mode L1"), + # 98 reserved + Power("backup_p1", 100, "Back-up L1 Power", Kind.UPS), + Voltage("backup_v2", 102, "Back-up L2 Voltage", Kind.UPS), + Current("backup_i2", 104, "Back-up L2 Current", Kind.UPS), + Frequency("backup_f2", 106, "Back-up L2 Frequency", Kind.UPS), + Integer("load_mode2", 108, "Load Mode L2"), + # 110 reserved + Power("backup_p2", 112, "Back-up L2 Power", Kind.UPS), + Voltage("backup_v3", 114, "Back-up L3 Voltage", Kind.UPS), + Current("backup_i3", 116, "Back-up L3 Current", Kind.UPS), + Frequency("backup_f3", 118, "Back-up L3 Frequency", Kind.UPS), + Integer("load_mode3", 120, "Load Mode L3"), + # 122 reserved + Power("backup_p3", 124, "Back-up L3 Power", Kind.UPS), + # 126 reserved + Power("load_p1", 128, "Load L1", Kind.AC), + # 130 reserved + Power("load_p2", 132, "Load L2", Kind.AC), + # 134 reserved + Power("load_p3", 136, "Load L3", Kind.AC), + # 138 reserved + Power("backup_ptotal", 140, "Back-up Load", Kind.UPS), + # 142 reserved + Power("load_ptotal", 144, "Load", Kind.AC), + Integer("ups_load", 146, "Ups Load", "%", Kind.UPS), + Temp("temperature_air", 148, "Inverter Temperature (Air)", Kind.AC), + Temp("temperature_module", 150, "Inverter Temperature (Module)"), + Temp("temperature", 152, "Inverter Temperature (Radiator)", Kind.AC), + Integer("function_bit", 154, "Function Bit"), + Voltage("bus_voltage", 156, "Bus Voltage", None), + Voltage("nbus_voltage", 158, "NBus Voltage", None), + Voltage("vbattery1", 160, "Battery Voltage", Kind.BAT), # modbus 35180 + Current("ibattery1", 162, "Battery Current", Kind.BAT), + # round(vbattery1 * ibattery1), + Calculated("pbattery1", + lambda data: round(read_voltage(data, 160) * read_current(data, 162)), + "Battery Power", "W", Kind.BAT), + Integer("battery_mode", 168, "Battery Mode code", "", Kind.BAT), + Enum2("battery_mode_label", 168, BATTERY_MODES, "Battery Mode", Kind.BAT), + Integer("warning_code", 170, "Warning code"), + Integer("safety_country", 172, "Safety Country code", "", Kind.AC), + Enum2("safety_country_label", 172, SAFETY_COUNTRIES, "Safety Country", Kind.AC), + Integer("work_mode", 174, "Work Mode code"), + Enum2("work_mode_label", 174, WORK_MODES_ET, "Work Mode"), + Integer("operation_mode", 176, "Operation Mode code"), + Long("error_codes", 178, "Error Codes"), + EnumBitmap4("errors", 178, ERROR_CODES, "Errors"), + Energy4("e_total", 182, "Total PV Generation", Kind.PV), + Energy4("e_day", 186, "Today's PV Generation", Kind.PV), + Energy4("e_total_exp", 190, "Total Energy (export)", Kind.AC), + Long("h_total", 194, "Hours Total", "h", Kind.PV), + Energy("e_day_exp", 198, "Today Energy (export)", Kind.AC), + Energy4("e_total_imp", 200, "Total Energy (import)", Kind.AC), + Energy("e_day_imp", 204, "Today Energy (import)", Kind.AC), + Energy4("e_load_total", 206, "Total Load", Kind.AC), + Energy("e_load_day", 210, "Today Load", Kind.AC), + Energy4("e_bat_charge_total", 212, "Total Battery Charge", Kind.BAT), + Energy("e_bat_charge_day", 216, "Today Battery Charge", Kind.BAT), + Energy4("e_bat_discharge_total", 218, "Total Battery Discharge", Kind.BAT), + Energy("e_bat_discharge_day", 222, "Today Battery Discharge", Kind.BAT), + Long("diagnose_result", 240, "Diag Status Code"), + EnumBitmap4("diagnose_result_label", 240, DIAG_STATUS_CODES, "Diag Status"), + # ppv1 + ppv2 + pbattery - active_power + Calculated("house_consumption", + lambda data: + read_bytes4(data, 10) + + read_bytes4(data, 18) + + read_bytes4(data, 26) + + read_bytes4(data, 34) + + round(read_voltage(data, 160) * read_current(data, 162)) - + read_bytes2(data, 80), + "House Consumption", "W", Kind.AC), + ) + + # Modbus registers from offset 0x9088 (37000) + __all_sensors_battery: Tuple[Sensor, ...] = ( + Integer("battery_bms", 0, "Battery BMS", "", Kind.BAT), + Integer("battery_index", 2, "Battery Index", "", Kind.BAT), + Integer("battery_status", 4, "Battery Status", "", Kind.BAT), + Temp("battery_temperature", 6, "Battery Temperature", Kind.BAT), + Integer("battery_charge_limit", 8, "Battery Charge Limit", "A", Kind.BAT), + Integer("battery_discharge_limit", 10, "Battery Discharge Limit", "A", Kind.BAT), + Integer("battery_error_l", 12, "Battery Error L", "", Kind.BAT), + Integer("battery_soc", 14, "Battery State of Charge", "%", Kind.BAT), + Integer("battery_soh", 16, "Battery State of Health", "%", Kind.BAT), + Integer("battery_modules", 18, "Battery Modules", "", Kind.BAT), # modbus 37009 + Integer("battery_warning_l", 20, "Battery Warning L", "", Kind.BAT), + Integer("battery_protocol", 22, "Battery Protocol", "", Kind.BAT), + Integer("battery_error_h", 24, "Battery Error H", "", Kind.BAT), + EnumBitmap22("battery_error", 24, 12, BMS_ALARM_CODES, "Battery Error", Kind.BAT), + Integer("battery_warning_h", 28, "Battery Warning H", "", Kind.BAT), + EnumBitmap22("battery_warning", 28, 20, BMS_WARNING_CODES, "Battery Warning", Kind.BAT), + Integer("battery_sw_version", 30, "Battery Software Version", "", Kind.BAT), + Integer("battery_hw_version", 32, "Battery Hardware Version", "", Kind.BAT), + Integer("battery_max_cell_temp_id", 34, "Battery Max Cell Temperature ID", "", Kind.BAT), + Integer("battery_min_cell_temp_id", 36, "Battery Min Cell Temperature ID", "", Kind.BAT), + Integer("battery_max_cell_voltage_id", 38, "Battery Max Cell Voltage ID", "", Kind.BAT), + Integer("battery_min_cell_voltage_id", 40, "Battery Min Cell Voltage ID", "", Kind.BAT), + Temp("battery_max_cell_temp", 42, "Battery Max Cell Temperature", Kind.BAT), + Temp("battery_min_cell_temp", 44, "Battery Min Cell Temperature", Kind.BAT), + Voltage("battery_max_cell_voltage", 46, "Battery Max Cell Voltage", Kind.BAT), + Voltage("battery_min_cell_voltage", 48, "Battery Min Cell Voltage", Kind.BAT), + ) + + # Inverter's meter data + # Modbus registers from offset 0x8ca0 (36000) + __all_sensors_meter: Tuple[Sensor, ...] = ( + Integer("commode", 0, "Commode"), + Integer("rssi", 2, "RSSI"), + Integer("manufacture_code", 4, "Manufacture Code"), + Integer("meter_test_status", 6, "Meter Test Status"), # 1: correct,2: reverse,3: incorrect,0: not checked + Integer("meter_comm_status", 8, "Meter Communication Status"), # 1 OK, 0 NotOK + Power("active_power1", 10, "Active Power L1", Kind.GRID), # modbus 36005 + Power("active_power2", 12, "Active Power L2", Kind.GRID), + Power("active_power3", 14, "Active Power L3", Kind.GRID), + Power("active_power_total", 16, "Active Power Total", Kind.GRID), + Reactive("reactive_power_total", 18, "Reactive Power Total", Kind.GRID), + Decimal("meter_power_factor1", 20, 1000, "Meter Power Factor L1", "", Kind.GRID), + Decimal("meter_power_factor2", 22, 1000, "Meter Power Factor L2", "", Kind.GRID), + Decimal("meter_power_factor3", 24, 1000, "Meter Power Factor L3", "", Kind.GRID), + Decimal("meter_power_factor", 26, 1000, "Meter Power Factor", "", Kind.GRID), + Frequency("meter_freq", 28, "Meter Frequency", Kind.GRID), # modbus 36014 + Float("meter_e_total_exp", 30, 1000, "Meter Total Energy (export)", "kWh", Kind.GRID), + Float("meter_e_total_imp", 34, 1000, "Meter Total Energy (import)", "kWh", Kind.GRID), + Power4("meter_active_power1", 38, "Meter Active Power L1", Kind.GRID), + Power4("meter_active_power2", 42, "Meter Active Power L2", Kind.GRID), + Power4("meter_active_power3", 46, "Meter Active Power L3", Kind.GRID), + Power4("meter_active_power_total", 50, "Meter Active Power Total", Kind.GRID), + Reactive4("meter_reactive_power1", 54, "Meter Reactive Power L1", Kind.GRID), + Reactive4("meter_reactive_power2", 58, "Meter Reactive Power L2", Kind.GRID), + Reactive4("meter_reactive_power3", 62, "Meter Reactive Power L2", Kind.GRID), + Reactive4("meter_reactive_power_total", 66, "Meter Reactive Power Total", Kind.GRID), + Apparent4("meter_apparent_power1", 70, "Meter Apparent Power L1", Kind.GRID), + Apparent4("meter_apparent_power2", 74, "Meter Apparent Power L2", Kind.GRID), + Apparent4("meter_apparent_power3", 78, "Meter Apparent Power L3", Kind.GRID), + Apparent4("meter_apparent_power_total", 82, "Meter Apparent Power Total", Kind.GRID), + Integer("meter_type", 86, "Meter Type", "", Kind.GRID), + Integer("meter_sw_version", 88, "Meter Software Version", "", Kind.GRID), + ) + + # Modbus registers of inverter settings, offsets are modbus register addresses + __all_settings: Tuple[Sensor, ...] = ( + Integer("comm_address", 45127, "Communication Address", ""), + + Timestamp("time", 45200, "Inverter time"), + + Integer("sensitivity_check", 45246, "Sensitivity Check Mode", "", Kind.AC), + Integer("cold_start", 45248, "Cold Start", "", Kind.AC), + Integer("shadow_scan", 45251, "Shadow Scan", "", Kind.PV), + Integer("backup_supply", 45252, "Backup Supply", "", Kind.UPS), + Integer("unbalanced_output", 45264, "Unbalanced Output", "", Kind.AC), + Integer("pen_relay", 45288, "PE-N Relay", "", Kind.AC), + + Integer("battery_capacity", 45350, "Battery Capacity", "Ah", Kind.BAT), + Integer("battery_modules", 45351, "Battery Modules", "", Kind.BAT), + Voltage("battery_charge_voltage", 45352, "Battery Charge Voltage", Kind.BAT), + Current("battery_charge_current", 45353, "Battery Charge Current", Kind.BAT), + Voltage("battery_discharge_voltage", 45354, "Battery Discharge Voltage", Kind.BAT), + Current("battery_discharge_current", 45355, "Battery Discharge Current", Kind.BAT), + Integer("battery_discharge_depth", 45356, "Battery Discharge Depth", "%", Kind.BAT), + Voltage("battery_discharge_voltage_offline", 45357, "Battery Discharge Voltage (off-line)", Kind.BAT), + Integer("battery_discharge_depth_offline", 45358, "Battery Discharge Depth (off-line)", "%", Kind.BAT), + + Decimal("power_factor", 45482, 100, "Power Factor"), + + Integer("work_mode", 47000, "Work Mode", "", Kind.AC), + Integer("dred", 47010, "DRED/Remote Shutdown", "", Kind.AC), + + Integer("battery_soc_protection", 47500, "Battery SoC Protection", "", Kind.BAT), + + Integer("grid_export", 47509, "Grid Export Enabled", "", Kind.GRID), + Integer("grid_export_limit", 47510, "Grid Export Limit", "W", Kind.GRID), + + Integer("battery_protocol_code", 47514, "Battery Protocol Code", "", Kind.BAT), + + EcoModeV1("eco_mode_1", 47515, "Eco Mode Group 1"), + ByteH("eco_mode_1_switch", 47518, "Eco Mode Group 1 Switch"), + EcoModeV1("eco_mode_2", 47519, "Eco Mode Group 2"), + ByteH("eco_mode_2_switch", 47522, "Eco Mode Group 2 Switch"), + EcoModeV1("eco_mode_3", 47523, "Eco Mode Group 3"), + ByteH("eco_mode_3_switch", 47526, "Eco Mode Group 3 Switch"), + EcoModeV1("eco_mode_4", 47527, "Eco Mode Group 4"), + ByteH("eco_mode_4_switch", 47530, "Eco Mode Group 4 Switch"), + ) + + # Settings added in ARM firmware 19 + __settings_arm_fw_19: Tuple[Sensor, ...] = ( + Integer("fast_charging", 47545, "Fast Charging Enabled", "", Kind.BAT), + Integer("fast_charging_soc", 47546, "Fast Charging SoC", "%", Kind.BAT), + EcoModeV2("eco_mode_1", 47547, "Eco Mode Group 1"), + ByteH("eco_mode_1_switch", 47549, "Eco Mode Group 1 Switch"), + EcoModeV2("eco_mode_2", 47553, "Eco Mode Group 2"), + ByteH("eco_mode_2_switch", 47555, "Eco Mode Group 2 Switch"), + EcoModeV2("eco_mode_3", 47559, "Eco Mode Group 3"), + ByteH("eco_mode_3_switch", 47561, "Eco Mode Group 3 Switch"), + EcoModeV2("eco_mode_4", 47565, "Eco Mode Group 4"), + ByteH("eco_mode_4_switch", 47567, "Eco Mode Group 4 Switch"), + + Integer("load_control_mode", 47595, "Load Control Mode", "", Kind.AC), + Integer("load_control_switch", 47596, "Load Control Switch", "", Kind.AC), + Integer("load_control_soc", 47596, "Load Control SoC", "", Kind.AC), + + Integer("fast_charging_power", 47603, "Fast Charging Power", "%", Kind.BAT), + ) + + # Settings added in ARM firmware 22 + __settings_arm_fw_22: Tuple[Sensor, ...] = ( + # EcoModeV2("eco_modeV2_5", 47571, "Eco Mode Version 2 Power Group 5"), + # EcoModeV2("eco_modeV2_6", 47577, "Eco Mode Version 2 Power Group 6"), + # EcoModeV2("eco_modeV2_7", 47583, "Eco Mode Version 2 Power Group 7"), + PeakShavingMode("peak_shaving_mode", 47589, "Peak Shaving Mode"), + + Integer("dod_holding", 47602, "DoD Holding", "", Kind.BAT), + ) + + def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3): + super().__init__(host, comm_addr, timeout, retries) + if not self.comm_addr: + # Set the default inverter address + self.comm_addr = 0xf7 + self._READ_DEVICE_VERSION_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x88b8, 0x0021) + self._READ_RUNNING_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x891c, 0x007d) + self._READ_METER_DATA: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x8ca0, 0x2d) + self._READ_BATTERY_INFO: ProtocolCommand = ModbusReadCommand(self.comm_addr, 0x9088, 0x0018) + self._has_battery: bool = True + # By default, we set up only PV1 on PV2 sensors, only few inverters support PV3 and PV4 + # In case they are needed, they are added later in read_device_info + self._sensors = tuple(filter(self._pv1_pv2_only, self.__all_sensors)) + self._sensors_battery = self.__all_sensors_battery + self._sensors_meter = self.__all_sensors_meter + self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings} + + def _supports_eco_mode_v2(self) -> bool: + return self.arm_version >= 19 + + def _supports_peak_shaving(self) -> bool: + return self.arm_version >= 22 + + @staticmethod + def _single_phase_only(s: Sensor) -> bool: + """Filter to exclude phase2/3 sensors on single phase inverters""" + return not ((s.id_.endswith('2') or s.id_.endswith('3')) and 'pv' not in s.id_) + + @staticmethod + def _pv1_pv2_only(s: Sensor) -> bool: + """Filter to exclude sensors on < 4 PV inverters""" + return not (('pv3' in s.id_) or ('pv4' in s.id_)) + + async def read_device_info(self): + response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO) + response = response[5:-2] + # Modbus registers from offset (35000) + self.modbus_version = read_unsigned_int(response, 0) + self.rated_power = read_unsigned_int(response, 2) + self.ac_output_type = read_unsigned_int(response, 4) # 0: 1-phase, 1: 3-phase (4 wire), 2: 3-phase (3 wire) + self.serial_number = response[6:22].decode("ascii") + self.model_name = response[22:32].decode("ascii").rstrip() + self.dsp1_version = read_unsigned_int(response, 32) + self.dsp2_version = read_unsigned_int(response, 34) + self.dsp_svn_version = read_unsigned_int(response, 36) + self.arm_version = read_unsigned_int(response, 38) + self.arm_svn_version = read_unsigned_int(response, 40) + self.firmware = self._decode(response[42:54]) + self.arm_firmware = self._decode(response[54:66]) + + if is_4_mptt(self): + # this is PV3/PV4 re-include all sensors + self._sensors = tuple(self.__all_sensors) + self._sensors_meter = tuple(self._sensors_meter) + + if is_single_phase(self): + # this is single phase inverter, filter out all L2 and L3 sensors + self._sensors = tuple(filter(self._single_phase_only, self._sensors)) + self._sensors_meter = tuple(filter(self._single_phase_only, self._sensors_meter)) + + if self.arm_version >= 19: + self._settings.update({s.id_: s for s in self.__settings_arm_fw_19}) + if self.arm_version >= 22: + self._settings.update({s.id_: s for s in self.__settings_arm_fw_22}) + + async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict[str, Any]: + raw_data = await self._read_from_socket(self._READ_RUNNING_DATA) + data = self._map_response(raw_data[5:-2], self._sensors, include_unknown_sensors) + + self._has_battery = data.get('battery_mode', 0) != 0 + if self._has_battery: + raw_data = await self._read_from_socket(self._READ_BATTERY_INFO) + data.update(self._map_response(raw_data[5:-2], self._sensors_battery, include_unknown_sensors)) + + raw_data = await self._read_from_socket(self._READ_METER_DATA) + data.update(self._map_response(raw_data[5:-2], self._sensors_meter, include_unknown_sensors)) + return data + + async def read_setting(self, setting_id: str) -> Any: + setting = self._settings.get(setting_id) + if not setting: + raise ValueError(f'Unknown setting "{setting_id}"') + count = (setting.size_ + (setting.size_ % 2)) // 2 + raw_data = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, count)) + with io.BytesIO(raw_data[5:-2]) as buffer: + return setting.read_value(buffer) + + async def write_setting(self, setting_id: str, value: Any): + setting = self._settings.get(setting_id) + if not setting: + raise ValueError(f'Unknown setting "{setting_id}"') + if setting.size_ == 1: + # modbus can address/store only 16 bit values, read the other 8 bytes + register_data = await self._read_from_socket(ModbusReadCommand(self.comm_addr, setting.offset, 1)) + raw_value = setting.encode_value(value, register_data[5:7]) + else: + raw_value = setting.encode_value(value) + if len(raw_value) <= 2: + value = int.from_bytes(raw_value, byteorder="big", signed=True) + await self._read_from_socket(ModbusWriteCommand(self.comm_addr, setting.offset, value)) + else: + await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, setting.offset, raw_value)) + + async def read_settings_data(self) -> Dict[str, Any]: + data = {} + for setting in self.settings(): + try: + value = await self.read_setting(setting.id_) + data[setting.id_] = value + except ValueError: + logger.exception("Error reading setting %s.", setting.id_) + data[setting.id_] = None + return data + + async def get_grid_export_limit(self) -> int: + return await self.read_setting('grid_export_limit') + + async def set_grid_export_limit(self, export_limit: int) -> None: + if 0 <= export_limit <= 10000: + await self.write_setting('grid_export_limit', export_limit) + + async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]: + result = [e for e in OperationMode] + if not self._supports_peak_shaving(): + result.remove(OperationMode.PEAK_SHAVING) + if not include_emulated: + result.remove(OperationMode.ECO_CHARGE) + result.remove(OperationMode.ECO_DISCHARGE) + return tuple(result) + + async def get_operation_mode(self) -> OperationMode: + mode = OperationMode(await self.read_setting('work_mode')) + if OperationMode.ECO != mode: + return mode + ecomode = await self.read_setting('eco_mode_1') + if ecomode.is_eco_charge_mode(): + return OperationMode.ECO_CHARGE + elif ecomode.is_eco_discharge_mode(): + return OperationMode.ECO_DISCHARGE + else: + return OperationMode.ECO + + async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power: int = 100, + eco_mode_soc: int = 100) -> None: + if operation_mode == OperationMode.GENERAL: + await self.write_setting('work_mode', 0) + await self._set_offline(False) + await self._clear_battery_mode_param() + elif operation_mode == OperationMode.OFF_GRID: + await self.write_setting('work_mode', 1) + await self._set_offline(True) + await self.write_setting('backup_supply', 1) + await self.write_setting('cold_start', 4) + elif operation_mode == OperationMode.BACKUP: + await self.write_setting('work_mode', 2) + await self._set_offline(False) + await self._clear_battery_mode_param() + elif operation_mode == OperationMode.ECO: + await self.write_setting('work_mode', 3) + await self._set_offline(False) + elif operation_mode == OperationMode.PEAK_SHAVING: + await self.write_setting('work_mode', 4) + await self._set_offline(False) + elif operation_mode in (OperationMode.ECO_CHARGE, OperationMode.ECO_DISCHARGE): + if eco_mode_power < 0 or eco_mode_power > 100: + raise ValueError() + if eco_mode_soc < 0 or eco_mode_soc > 100: + raise ValueError() + eco_mode: EcoMode = self._convert_eco_mode(EcoModeV2("", 0, "")) + if operation_mode == OperationMode.ECO_CHARGE: + await self.write_setting('eco_mode_1', eco_mode.encode_charge(eco_mode_power, eco_mode_soc)) + else: + await self.write_setting('eco_mode_1', eco_mode.encode_discharge(eco_mode_power)) + await self.write_setting('eco_mode_2_switch', 0) + await self.write_setting('eco_mode_3_switch', 0) + await self.write_setting('eco_mode_4_switch', 0) + await self.write_setting('work_mode', 3) + await self._set_offline(False) + + async def get_ongrid_battery_dod(self) -> int: + return 100 - await self.read_setting('battery_discharge_depth') + + async def set_ongrid_battery_dod(self, dod: int) -> None: + if 0 <= dod <= 90: + await self.write_setting('battery_discharge_depth', 100 - dod) + + def sensors(self) -> Tuple[Sensor, ...]: + if self._has_battery: + return self._sensors + self._sensors_battery + self._sensors_meter + else: + return self._sensors + self._sensors_meter + + def settings(self) -> Tuple[Sensor, ...]: + return tuple(self._settings.values()) + + async def _clear_battery_mode_param(self) -> None: + await self._read_from_socket(ModbusWriteCommand(self.comm_addr, 0xb9ad, 1)) + + async def _set_offline(self, mode: bool) -> None: + value = bytes.fromhex('00070000') if mode else bytes.fromhex('00010000') + await self._read_from_socket(ModbusWriteMultiCommand(self.comm_addr, 0xb997, value)) + + def _convert_eco_mode(self, sensor: Sensor) -> Sensor | EcoMode: + if EcoModeV1 == type(sensor) and self._supports_eco_mode_v2(): + return cast(EcoModeV1, sensor).as_eco_mode_v2() + elif EcoModeV2 == type(sensor) and not self._supports_eco_mode_v2(): + return cast(EcoModeV2, sensor).as_eco_mode_v1() + else: + return sensor diff --git a/goodwe/exceptions.py b/goodwe/exceptions.py new file mode 100644 index 0000000..a07840c --- /dev/null +++ b/goodwe/exceptions.py @@ -0,0 +1,37 @@ +class InverterError(Exception): + """Indicates error communicating with inverter""" + + +class RequestFailedException(InverterError): + """ + Indicates request sent to inverter has failed and did not yield in valid response, + even after several retries. + + Attributes: + message -- explanation of the error + consecutive_failures_count -- number requests failed in a consecutive streak + """ + + def __init__(self, message: str = '', consecutive_failures_count: int = 0): + self.message: str = message + self.consecutive_failures_count: int = consecutive_failures_count + + +class RequestRejectedException(InverterError): + """ + Indicates request sent to inverter was rejected and protocol exception response was received. + + Attributes: + message -- rejection reason + """ + + def __init__(self, message: str = ''): + self.message: str = message + + +class ProcessingException(InverterError): + """Indicates an error occurred during processing of inverter data""" + + +class MaxRetriesException(InverterError): + """Indicates the maximum number of retries has been reached""" diff --git a/goodwe/goodwe.py b/goodwe/goodwe.py new file mode 100644 index 0000000..78c2d5c --- /dev/null +++ b/goodwe/goodwe.py @@ -0,0 +1,22 @@ +import logging +from typing import Tuple + +from .exceptions import ProcessingException +from .processor import ProcessorResult, AbstractDataProcessor +from .xs import GoodWeXSProcessor + +logger = logging.getLogger(__name__) + +class GoodWeInverter: + def __init__(self, inverter_address: Tuple[str, int], processor: AbstractDataProcessor): + self.address = inverter_address + self.processor = processor + + async def request_data(self) -> ProcessorResult: + try: + logger.debug('awaiting future') + data = await self.processor.get_runtime_data_command().execute(self.address[0], 1, 3) + return self.processor.process_data(data) + except (TypeError, ValueError) as e: + logger.debug(f'exception occurred during processing inverter data: {e}') + raise ProcessingException diff --git a/goodwe/inverter.py b/goodwe/inverter.py new file mode 100644 index 0000000..a1243c5 --- /dev/null +++ b/goodwe/inverter.py @@ -0,0 +1,305 @@ +from __future__ import annotations + +import asyncio +import io +import logging +from abc import ABC, abstractmethod +from dataclasses import dataclass +from enum import Enum, IntEnum +from typing import Any, Callable, Dict, Tuple, Optional + +from .exceptions import MaxRetriesException, RequestFailedException +from .protocol import ProtocolCommand + +logger = logging.getLogger(__name__) + + +class SensorKind(Enum): + """ + Enumeration of sensor kinds. + + Possible values are: + PV - inverter photo-voltaic (e.g. dc voltage of pv panels) + AC - inverter grid output (e.g. ac voltage of grid connected output) + UPS - inverter ups/eps/backup output (e.g. ac voltage of backup/off-grid connected output) + BAT - battery (e.g. dc voltage of connected battery pack) + GRID - power grid/smart meter (e.g. active power exported to grid) + """ + + PV = 1 + AC = 2 + UPS = 3 + BAT = 4 + GRID = 5 + + +@dataclass +class Sensor: + """Definition of inverter sensor and its attributes""" + + id_: str + offset: int + name: str + size_: int + unit: str + kind: Optional[SensorKind] + + def read_value(self, data: io.BytesIO) -> Any: + """Read the sensor value from data at current position""" + raise NotImplementedError() + + def read(self, data: io.BytesIO) -> Any: + """Read the sensor value from data (at sensor offset)""" + data.seek(self.offset) + return self.read_value(data) + + def encode_value(self, value: Any) -> bytes: + """Encode the (setting mostly) value to (usually) 2 byte raw register value""" + raise NotImplementedError() + + +class OperationMode(IntEnum): + """ + Enumeration of sensor kinds. + + Possible values are: + GENERAL - General mode + OFF_GRID - Off grid mode + BACKUP - Backup mode + ECO - Eco mode + PEAK_SHAVING - Peak shaving mode + ECO_CHARGE - Eco mode with a single "Charge" group valid all the time (from 00:00-23:59, Mon-Sun) + ECO_DISCHARGE - Eco mode with a single "Discharge" group valid all the time (from 00:00-23:59, Mon-Sun) + """ + + GENERAL = 0 + OFF_GRID = 1 + BACKUP = 2 + ECO = 3 + PEAK_SHAVING = 4 + ECO_CHARGE = 5 + ECO_DISCHARGE = 6 + + +class Inverter(ABC): + """ + Common superclass for various inverter models implementations. + Represents the inverter state and its basic behavior + """ + + def __init__(self, host: str, comm_addr: int = 0, timeout: int = 1, retries: int = 3): + self.host: str = host + self.comm_addr: int = comm_addr + self.timeout: int = timeout + self.retries: int = retries + self._running_loop: asyncio.AbstractEventLoop | None = None + self._lock: asyncio.Lock | None = None + self._consecutive_failures_count: int = 0 + + self.model_name: str | None = None + self.serial_number: str | None = None + self.rated_power: int | None = None + self.ac_output_type: int | None = None + self.firmware: str | None = None + self.arm_firmware: str | None = None + self.modbus_version: int | None = None + self.dsp1_version: int = 0 + self.dsp2_version: int = 0 + self.dsp_svn_version: int | None = None + self.arm_version: int = 0 + self.arm_svn_version: int | None = None + + def _ensure_lock(self) -> asyncio.Lock: + """Validate (or create) asyncio Lock. + + The asyncio.Lock must always be created from within's asyncio loop, + so it cannot be eagerly created in constructor. + Additionally, since asyncio.run() creates and closes its own loop, + the lock's scope (its creating loop) mus be verified to support proper + behavior in subsequent asyncio.run() invocations. + """ + if self._lock and self._running_loop == asyncio.get_event_loop(): + return self._lock + else: + logger.debug("Creating lock instance for current event loop.") + self._lock = asyncio.Lock() + self._running_loop = asyncio.get_event_loop() + return self._lock + + async def _read_from_socket(self, command: ProtocolCommand) -> bytes: + async with self._ensure_lock(): + try: + result = await command.execute(self.host, self.timeout, self.retries) + self._consecutive_failures_count = 0 + return result + except MaxRetriesException: + self._consecutive_failures_count += 1 + raise RequestFailedException(f'No valid response received even after {self.retries} retries', + self._consecutive_failures_count) + except RequestFailedException as ex: + self._consecutive_failures_count += 1 + raise RequestFailedException(ex.message, self._consecutive_failures_count) + + @abstractmethod + async def read_device_info(self): + """ + Request the device information from the inverter. + The inverter instance variables will be loaded with relevant data. + """ + raise NotImplementedError() + + @abstractmethod + async def read_runtime_data(self, include_unknown_sensors: bool = False) -> Dict[str, Any]: + """ + Request the runtime data from the inverter. + Answer dictionary of individual sensors and their values. + List of supported sensors (and their definitions) is provided by sensors() method. + + If include_unknown_sensors parameter is set to True, return all runtime values, + including those "xx*" sensors whose meaning is not yet identified. + """ + raise NotImplementedError() + + @abstractmethod + async def read_setting(self, setting_id: str) -> Any: + """ + Read the value of specific inverter setting/configuration parameter. + Setting must be in list provided by settings() method, otherwise ValueError is raised. + """ + raise NotImplementedError() + + @abstractmethod + async def write_setting(self, setting_id: str, value: Any): + """ + Set the value of specific inverter settings/configuration parameter. + Setting must be in list provided by settings() method, otherwise ValueError is raised. + + BEWARE !!! + This method modifies inverter operational parameter (usually accessible to installers only). + Use with caution and at your own risk ! + """ + raise NotImplementedError() + + @abstractmethod + async def read_settings_data(self) -> Dict[str, Any]: + """ + Request the settings data from the inverter. + Answer dictionary of individual settings and their values. + List of supported settings (and their definitions) is provided by settings() method. + """ + raise NotImplementedError() + + async def send_command( + self, command: bytes, validator: Callable[[bytes], bool] = lambda x: True + ) -> bytes: + """ + Send low level udp command (as bytes). + Answer command's raw response data. + """ + return await self._read_from_socket(ProtocolCommand(command, validator)) + + @abstractmethod + async def get_grid_export_limit(self) -> int: + """ + Get the current grid export limit in W + """ + raise NotImplementedError() + + @abstractmethod + async def set_grid_export_limit(self, export_limit: int) -> None: + """ + BEWARE !!! + This method modifies inverter operational parameter accessible to installers only. + Use with caution and at your own risk ! + + Set the grid export limit in W + """ + raise NotImplementedError() + + @abstractmethod + async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]: + """ + Answer list of supported inverter operation modes + """ + return () + + @abstractmethod + async def get_operation_mode(self) -> OperationMode: + """ + Get the inverter operation mode + """ + raise NotImplementedError() + + @abstractmethod + async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power: int = 100, + eco_mode_soc: int = 100) -> None: + """ + BEWARE !!! + This method modifies inverter operational parameter accessible to installers only. + Use with caution and at your own risk ! + + Set the inverter operation mode + + The modes ECO_CHARGE and ECO_DISCHARGE are not real inverter operation modes, but a convenience + shortcuts to enter Eco Mode with a single group valid all the time (from 00:00-23:59, Mon-Sun) + charging or discharging with optional charging power and SoC (%) parameters. + """ + raise NotImplementedError() + + @abstractmethod + async def get_ongrid_battery_dod(self) -> int: + """ + Get the On-Grid Battery DoD + 0% - 89% + """ + raise NotImplementedError() + + @abstractmethod + async def set_ongrid_battery_dod(self, dod: int) -> None: + """ + BEWARE !!! + This method modifies On-Grid Battery DoD parameter accessible to installers only. + Use with caution and at your own risk ! + + Set the On-Grid Battery DoD + 0% - 89% + """ + raise NotImplementedError() + + @abstractmethod + def sensors(self) -> Tuple[Sensor, ...]: + """ + Return tuple of sensor definitions + """ + raise NotImplementedError() + + @abstractmethod + def settings(self) -> Tuple[Sensor, ...]: + """ + Return tuple of settings definitions + """ + raise NotImplementedError() + + @staticmethod + def _map_response(resp_data: bytes, sensors: Tuple[Sensor, ...], incl_xx: bool = True) -> Dict[str, Any]: + """Process the response data and return dictionary with runtime values""" + with io.BytesIO(resp_data) as buffer: + result = {} + for sensor in sensors: + if incl_xx or not sensor.id_.startswith("xx"): + try: + result[sensor.id_] = sensor.read(buffer) + except ValueError: + logger.exception("Error reading sensor %s.", sensor.id_) + result[sensor.id_] = None + return result + + @staticmethod + def _decode(data: bytes) -> str: + """Decode the bytes to ascii string""" + try: + if any(x < 32 for x in data): + return data.hex() + return data.decode("ascii") + except ValueError: + return data.hex() diff --git a/goodwe/modbus.py b/goodwe/modbus.py new file mode 100644 index 0000000..daab4d4 --- /dev/null +++ b/goodwe/modbus.py @@ -0,0 +1,149 @@ +import logging +from typing import Union + +from .exceptions import RequestRejectedException + +logger = logging.getLogger(__name__) + +MODBUS_READ_CMD: int = 0x3 +MODBUS_WRITE_CMD: int = 0x6 +MODBUS_WRITE_MULTI_CMD: int = 0x10 + +FAILURE_CODES = { + 1: "ILLEGAL FUNCTION", + 2: "ILLEGAL DATA ADDRESS", + 3: "ILLEGAL DATA VALUE", + 4: "SLAVE DEVICE FAILURE", + 5: "ACKNOWLEDGE", + 6: "SLAVE DEVICE BUSY", + 7: "NEGATIVE ACKNOWLEDGEMENT", + 8: "MEMORY PARITY ERROR", + 10: "GATEWAY PATH UNAVAILABLE", + 11: "GATEWAY TARGET DEVICE FAILED TO RESPOND", +} + + +def _create_crc16_table() -> tuple: + """Construct (modbus) CRC-16 table""" + table = [] + for i in range(256): + buffer = i << 1 + crc = 0 + for _ in range(8, 0, -1): + buffer >>= 1 + if (buffer ^ crc) & 0x0001: + crc = (crc >> 1) ^ 0xA001 + else: + crc >>= 1 + table.append(crc) + return tuple(table) + + +_CRC_16_TABLE = _create_crc16_table() + + +def _modbus_checksum(data: Union[bytearray, bytes]) -> int: + """ + Calculate modbus crc-16 checksum + """ + crc = 0xFFFF + for ch in data: + crc = (crc >> 8) ^ _CRC_16_TABLE[(crc ^ ch) & 0xFF] + return crc + + +def create_modbus_request(comm_addr: int, cmd: int, offset: int, value: int) -> bytes: + """ + Create modbus request. + data[0] is inverter address + data[1] is modbus command + data[2:3] is command offset parameter + data[4:5] is command value parameter + data[6:7] is crc-16 checksum + """ + data: bytearray = bytearray(6) + data[0] = comm_addr + data[1] = cmd + data[2] = (offset >> 8) & 0xFF + data[3] = offset & 0xFF + data[4] = (value >> 8) & 0xFF + data[5] = value & 0xFF + checksum = _modbus_checksum(data) + data.append(checksum & 0xFF) + data.append((checksum >> 8) & 0xFF) + return bytes(data) + + +def create_modbus_multi_request(comm_addr: int, cmd: int, offset: int, values: bytes) -> bytes: + """ + Create modbus (multi value) request. + data[0] is inverter address + data[1] is modbus command + data[2:3] is command offset parameter + data[4:5] is number of registers + data[6] is number of bytes + data[7-n] is data payload + data[n+1:n+2] is crc-16 checksum + """ + data: bytearray = bytearray(7) + data[0] = comm_addr + data[1] = cmd + data[2] = (offset >> 8) & 0xFF + data[3] = offset & 0xFF + data[4] = 0 + data[5] = len(values) // 2 + data[6] = len(values) + data.extend(values) + checksum = _modbus_checksum(data) + data.append(checksum & 0xFF) + data.append((checksum >> 8) & 0xFF) + return bytes(data) + + +def validate_modbus_response(data: bytes, cmd: int, offset: int, value: int) -> bool: + """ + Validate the modbus response. + data[0:1] is header + data[2] is source address + data[3] is command return type + data[4] is response payload length (for read commands) + data[-2:] is crc-16 checksum + """ + if len(data) <= 4: + logger.debug("Response is too short.") + return False + if data[3] == MODBUS_READ_CMD: + if data[4] != value * 2: + logger.debug("Response has unexpected length: %d, expected %d.", data[4], value * 2) + return False + expected_length = data[4] + 7 + if len(data) < expected_length: + logger.debug("Response is too short: %d, expected %d.", len(data), expected_length) + return False + elif data[3] in (MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD): + if len(data) < 10: + logger.debug("Response has unexpected length: %d, expected %d.", len(data), 10) + return False + expected_length = 10 + response_offset = int.from_bytes(data[4:6], byteorder='big', signed=False) + if response_offset != offset: + logger.debug("Response has wrong offset: %X, expected %X.", response_offset, offset) + return False + response_value = int.from_bytes(data[6:8], byteorder='big', signed=True) + if response_value != value: + logger.debug("Response has wrong value: %X, expected %X.", response_value, value) + return False + else: + expected_length = len(data) + + checksum_offset = expected_length - 2 + if _modbus_checksum(data[2:checksum_offset]) != ((data[checksum_offset + 1] << 8) + data[checksum_offset]): + logger.debug("Response CRC-16 checksum does not match.") + return False + + if data[3] != cmd: + failure_code = FAILURE_CODES.get(data[4], "UNKNOWN") + logger.debug("Response is command failure: %s.", FAILURE_CODES.get(data[4], "UNKNOWN")) + raise RequestRejectedException(failure_code) + + return True diff --git a/goodwe/model.py b/goodwe/model.py new file mode 100644 index 0000000..aca0649 --- /dev/null +++ b/goodwe/model.py @@ -0,0 +1,26 @@ +from .inverter import Inverter + +# Serial number tags to identify inverter type +ET_MODEL_TAGS = ["ETU", "ETL", "ETR", "ETC", "EHU", "EHR", "EHB", "BTU", "BTN", "BTC", "BHU", "AES", "ABP", "HHI", + "HSB", "HUA", "CUA", + "ESN", "EMN", "ERN", "EBN", # ES Gen 2 + "HLB", "HMB", "HBB", "SPN"] # Gen 2 +ES_MODEL_TAGS = ["ESU", "EMU", "ESA", "BPS", "BPU", "EMJ", "IJL"] +DT_MODEL_TAGS = ["DTU", "DTS", "MSU", "MST", "DSN", "DTN", "DST", "NSU", "SSN", "SST", "SSX", "SSY", "PSB", "PSC"] + +SINGLE_PHASE_MODELS = ["DSN", "DST", "NSU", "SSN", "SST", "SSX", "SSY", # DT + "MSU", "MST", "PSB", "PSC", + "EHU", "EHR", "HSB", # ET + "ESN", "EMN", "ERN", "EBN", "HLB", "HMB", "HBB", "SPN"] # ES Gen 2 + + +def is_single_phase(inverter: Inverter) -> bool: + return any(model in inverter.serial_number for model in SINGLE_PHASE_MODELS) + + +def is_3_mptt(inverter: Inverter) -> bool: + return any(model in inverter.serial_number for model in ["MSU", "MST", "PSC"]) + + +def is_4_mptt(inverter: Inverter) -> bool: + return any(model in inverter.serial_number for model in ["HSB"]) diff --git a/goodwe/processor.py b/goodwe/processor.py new file mode 100644 index 0000000..287484e --- /dev/null +++ b/goodwe/processor.py @@ -0,0 +1,37 @@ +from abc import ABC +from dataclasses import dataclass, field +from datetime import datetime + +from goodwe.protocol import ProtocolCommand + + +@dataclass(init=True, order=True) +class ProcessorResult: + sort_index: datetime = field(init=False) + date: datetime + volts_dc: float + current_dc: float + volts_ac: float + current_ac: float + frequency_ac: float + generation_today: float + generation_total: float + rssi: float + operational_hours: float + temperature: float + power: float + status: str + + def __post_init__(self) -> None: + self.sort_index = self.date + + def __str__(self) -> str: + return f'{self.date.strftime("%Y-%m-%d %H:%M:%S")}: (status: {self.status}, power: {self.power})' + + +class AbstractDataProcessor(ABC): + def process_data(self, data: bytes) -> ProcessorResult: + """Process the data provided by the GoodWe inverter and return ProcessorResult""" + + def get_runtime_data_command(self) -> ProtocolCommand: + """Answer protocol command for reading runtime data""" diff --git a/goodwe/protocol.py b/goodwe/protocol.py new file mode 100644 index 0000000..3a0123d --- /dev/null +++ b/goodwe/protocol.py @@ -0,0 +1,270 @@ +from __future__ import annotations + +import asyncio +import logging +from asyncio.futures import Future +from typing import Tuple, Optional, Callable + +from .const import GOODWE_UDP_PORT +from .exceptions import MaxRetriesException, RequestFailedException, RequestRejectedException +from .modbus import create_modbus_request, create_modbus_multi_request, validate_modbus_response, MODBUS_READ_CMD, \ + MODBUS_WRITE_CMD, MODBUS_WRITE_MULTI_CMD + +logger = logging.getLogger(__name__) + + +class UdpInverterProtocol(asyncio.DatagramProtocol): + def __init__( + self, + response_future: Future, + command: ProtocolCommand, + timeout: int, + retries: int + ): + super().__init__() + self.response_future: Future = response_future + self.command: ProtocolCommand = command + self._transport: asyncio.transports.DatagramTransport | None = None + self._retry_timeout: int = timeout + self._max_retries: int = retries + self._retries: int = 0 + + def connection_made(self, transport: asyncio.DatagramTransport) -> None: + """On connection made""" + self._transport = transport + self._send_request() + + def connection_lost(self, exc: Optional[Exception]) -> None: + """On connection lost""" + if exc is not None: + logger.debug("Socket closed with error: %s.", exc) + # Cancel Future on connection lost + if not self.response_future.done(): + self.response_future.cancel() + + def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None: + """On datagram received""" + try: + if self.command.validator(data): + logger.debug("Received: %s", data.hex()) + self.response_future.set_result(data) + else: + logger.debug("Received invalid response: %s", data.hex()) + self._retries += 1 + self._send_request() + except RequestRejectedException as ex: + logger.debug("Received exception response: %s", data.hex()) + self.response_future.set_exception(ex) + + def error_received(self, exc: Exception) -> None: + """On error received""" + logger.debug("Received error: %s", exc) + self.response_future.set_exception(exc) + + def _send_request(self) -> None: + """Send message via transport""" + logger.debug("Sending: %s%s", self.command, + f' - retry #{self._retries}/{self._max_retries}' if self._retries > 0 else '') + self._transport.sendto(self.command.request) + asyncio.get_event_loop().call_later(self._retry_timeout, self._retry_mechanism) + + def _retry_mechanism(self) -> None: + """Retry mechanism to prevent hanging transport""" + if self.response_future.done(): + self._transport.close() + elif self._retries < self._max_retries: + logger.debug("Failed to receive response to %s in time (%ds).", self.command, self._retry_timeout) + self._retries += 1 + self._send_request() + else: + logger.debug("Max number of retries (%d) reached, request %s failed.", self._max_retries, self.command) + self.response_future.set_exception(MaxRetriesException) + + +class ProtocolCommand: + """Definition of inverter protocol command""" + + def __init__(self, request: bytes, validator: Callable[[bytes], bool]): + self.request: bytes = request + self.validator: Callable[[bytes], bool] = validator + + def __repr__(self): + return self.request.hex() + + async def execute(self, host: str, timeout: int, retries: int) -> bytes: + """ + Execute the udp protocol command on the specified address/port. + Since the UDP communication is by definition unreliable, when no (valid) response is received by specified + timeout, the command will be re-tried up to retries times. + + Return raw response data + """ + loop = asyncio.get_running_loop() + response_future = loop.create_future() + transport, _ = await loop.create_datagram_endpoint( + lambda: UdpInverterProtocol(response_future, self, timeout, retries), + remote_addr=(host, GOODWE_UDP_PORT), + ) + try: + await response_future + result = response_future.result() + if result is not None: + return result + else: + raise RequestFailedException( + "No response received to '" + self.request.hex() + "' request." + ) + except asyncio.CancelledError: + raise RequestFailedException( + "No valid response received to '" + self.request.hex() + "' request." + ) from None + finally: + transport.close() + + +class Aa55ProtocolCommand(ProtocolCommand): + """ + Inverter communication protocol seen mostly on older generations of inverters. + Quite probably it is some variation of the protocol used on RS-485 serial link, + extended/adapted to UDP transport layer. + + Each request starts with header of 0xAA, 0x55, then 0xC0, 0x7F (probably some sort of address/command) + followed by actual payload data. + It is suffixed with 2 bytes of plain checksum of header+payload. + + Response starts again with 0xAA, 0x55, then 0x7F, 0xC0. + 5-6th bytes are some response type, byte 7 is length of the response payload. + The last 2 bytes are again plain checksum of header+payload. + """ + + def __init__(self, payload: str, response_type: str): + super().__init__( + bytes.fromhex( + "AA55C07F" + + payload + + self._checksum(bytes.fromhex("AA55C07F" + payload)).hex() + ), + lambda x: self._validate_response(x, response_type), + ) + + @staticmethod + def _checksum(data: bytes) -> bytes: + checksum = 0 + for each in data: + checksum += each + return checksum.to_bytes(2, byteorder="big", signed=False) + + @staticmethod + def _validate_response(data: bytes, response_type: str) -> bool: + """ + Validate the response. + data[0:3] is header + data[4:5] is response type + data[6] is response payload length + data[-2:] is checksum (plain sum of response data incl. header) + """ + if len(data) <= 8 or len(data) != data[6] + 9: + logger.debug("Response has unexpected length: %d, expected %d.", len(data), data[6] + 9) + return False + elif response_type: + data_rt_int = int.from_bytes(data[4:6], byteorder="big", signed=True) + if int(response_type, 16) != data_rt_int: + logger.debug("Response type unexpected: %04x, expected %s.", data_rt_int, response_type) + return False + checksum = 0 + for each in data[:-2]: + checksum += each + if checksum != int.from_bytes(data[-2:], byteorder="big", signed=True): + logger.debug("Response checksum does not match.") + return False + return True + + +class Aa55ReadCommand(Aa55ProtocolCommand): + """ + Inverter modbus READ command for retrieving modbus registers starting at register # + """ + + def __init__(self, offset: int, count: int): + super().__init__("011A03" + "{:04x}".format(offset) + "{:02x}".format(count), "019A") + + +class Aa55WriteCommand(Aa55ProtocolCommand): + """ + Inverter aa55 WRITE command setting single register # value + """ + + def __init__(self, register: int, value: int): + super().__init__("023905" + "{:04x}".format(register) + "01" + "{:04x}".format(value), "02B9") + + +class Aa55WriteMultiCommand(Aa55ProtocolCommand): + """ + Inverter aa55 WRITE command setting multiple register # value + """ + + def __init__(self, offset: int, values: bytes): + super().__init__("02390B" + "{:04x}".format(offset) + "{:02x}".format(len(values)) + values.hex(), + "02B9") + + +class ModbusProtocolCommand(ProtocolCommand): + """ + Inverter communication protocol seen on newer generation of inverters, based on Modbus + protocol over UDP transport layer. + The modbus communication is rather simple, there are "registers" at specified addresses/offsets, + each represented by 2 bytes. The protocol may query/update individual or range of these registers. + Each register represents some measured value or operational settings. + It's inverter implementation specific which register means what. + Some values may span more registers (i.e. 4bytes measurement value over 2 registers). + + Every request usually starts with communication address (usually 0xF7, but can be changed). + Second byte is the modbus command - 0x03 read multiple, 0x06 write single, 0x10 write multiple. + Bytes 3-4 represent the register address (or start of range) + Bytes 5-6 represent the command parameter (range size or actual value for write). + Last 2 bytes of request is the CRC-16 (modbus flavor) of the request. + + Responses seem to always start with 0xAA, 0x55, then the comm_addr and modbus command. + (If the command fails, the highest bit of command is set to 1 ?) + For read requests, next byte is response payload length, then the actual payload. + Last 2 bytes of response is again the CRC-16 of the response. + """ + + def __init__(self, request: bytes, cmd: int, offset: int, value: int): + super().__init__( + request, + lambda x: validate_modbus_response(x, cmd, offset, value), + ) + + +class ModbusReadCommand(ModbusProtocolCommand): + """ + Inverter modbus READ command for retrieving modbus registers starting at register # + """ + + def __init__(self, comm_addr: int, offset: int, count: int): + super().__init__( + create_modbus_request(comm_addr, MODBUS_READ_CMD, offset, count), + MODBUS_READ_CMD, offset, count) + + +class ModbusWriteCommand(ModbusProtocolCommand): + """ + Inverter modbus WRITE command setting single modbus register # value + """ + + def __init__(self, comm_addr: int, register: int, value: int): + super().__init__( + create_modbus_request(comm_addr, MODBUS_WRITE_CMD, register, value), + MODBUS_WRITE_CMD, register, value) + + +class ModbusWriteMultiCommand(ModbusProtocolCommand): + """ + Inverter modbus WRITE command setting multiple modbus register # value + """ + + def __init__(self, comm_addr: int, offset: int, values: bytes): + super().__init__( + create_modbus_multi_request(comm_addr, MODBUS_WRITE_MULTI_CMD, offset, values), + MODBUS_WRITE_MULTI_CMD, offset, len(values) // 2) diff --git a/goodwe/sensor.py b/goodwe/sensor.py new file mode 100644 index 0000000..b9fe1db --- /dev/null +++ b/goodwe/sensor.py @@ -0,0 +1,761 @@ +from __future__ import annotations + +import io +from abc import ABC, abstractmethod +from datetime import datetime +from struct import unpack +from typing import Any, Callable, Optional + +from .const import * +from .inverter import Sensor, SensorKind + +DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + + +class Voltage(Sensor): + """Sensor representing voltage [V] value encoded in 2 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 2, "V", kind) + + def read_value(self, data: io.BytesIO): + return read_voltage(data) + + def encode_value(self, value: Any) -> bytes: + return encode_voltage(value) + + +class Current(Sensor): + """Sensor representing current [A] value encoded in 2 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 2, "A", kind) + + def read_value(self, data: io.BytesIO): + return read_current(data) + + def encode_value(self, value: Any) -> bytes: + return encode_current(value) + + +class Frequency(Sensor): + """Sensor representing frequency [Hz] value encoded in 2 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 2, "Hz", kind) + + def read_value(self, data: io.BytesIO): + return read_freq(data) + + +class Power(Sensor): + """Sensor representing power [W] value encoded in 2 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 2, "W", kind) + + def read_value(self, data: io.BytesIO): + return read_bytes2(data) + + +class Power4(Sensor): + """Sensor representing power [W] value encoded in 4 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 4, "W", kind) + + def read_value(self, data: io.BytesIO): + return read_bytes4(data) + + +class Energy(Sensor): + """Sensor representing energy [kWh] value encoded in 2 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 2, "kWh", kind) + + def read_value(self, data: io.BytesIO): + value = read_bytes2(data) + if value == -1: + return None + else: + return float(value) / 10 + + +class Energy4(Sensor): + """Sensor representing energy [kWh] value encoded in 4 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 4, "kWh", kind) + + def read_value(self, data: io.BytesIO): + value = read_bytes4(data) + if value == -1: + return None + else: + return float(value) / 10 + + +class Apparent(Sensor): + """Sensor representing apparent power [VA] value encoded in 2 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 2, "VA", kind) + + def read_value(self, data: io.BytesIO): + return read_bytes2(data) + + +class Apparent4(Sensor): + """Sensor representing apparent power [VA] value encoded in 4 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 2, "VA", kind) + + def read_value(self, data: io.BytesIO): + return read_bytes4(data) + + +class Reactive(Sensor): + """Sensor representing reactive power [var] value encoded in 2 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 2, "var", kind) + + def read_value(self, data: io.BytesIO): + return read_bytes2(data) + + +class Reactive4(Sensor): + """Sensor representing reactive power [var] value encoded in 4 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind]): + super().__init__(id_, offset, name, 2, "var", kind) + + def read_value(self, data: io.BytesIO): + return read_bytes4(data) + + +class Temp(Sensor): + """Sensor representing temperature [C] value encoded in 2 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 2, "C", kind) + + def read_value(self, data: io.BytesIO): + return read_temp(data) + + +class Byte(Sensor): + """Sensor representing signed int value encoded in 1 byte""" + + def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 1, unit, kind) + + def read_value(self, data: io.BytesIO): + return read_byte(data) + + def encode_value(self, value: Any) -> bytes: + raise NotImplementedError() + + +class ByteH(Byte): + """Sensor representing signed int value encoded in 1 byte (high 8 bits of 16bit register)""" + + def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, unit, kind) + + def encode_value(self, value: Any, register_value: bytes) -> bytes: + word = bytearray(register_value) + word[0] = int.to_bytes(int(value), length=1, byteorder="big", signed=True)[0] + return bytes(word) + + +class ByteL(Byte): + """Sensor representing signed int value encoded in 1 byte (low 8 bits of 16bit register)""" + + def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, unit, kind) + + def encode_value(self, value: Any, register_value: bytes) -> bytes: + word = bytearray(register_value) + word[1] = int.to_bytes(int(value), length=1, byteorder="big", signed=True)[0] + return bytes(word) + + +class Integer(Sensor): + """Sensor representing signed int value encoded in 2 bytes""" + + def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 2, unit, kind) + + def read_value(self, data: io.BytesIO): + return read_bytes2(data) + + def encode_value(self, value: Any) -> bytes: + return int.to_bytes(int(value), length=2, byteorder="big", signed=True) + + +class Long(Sensor): + """Sensor representing signed int value encoded in 4 bytes""" + + def __init__(self, id_: str, offset: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 4, unit, kind) + + def read_value(self, data: io.BytesIO): + return read_bytes4(data) + + def encode_value(self, value: Any) -> bytes: + return int.to_bytes(int(value), length=4, byteorder="big", signed=True) + + +class Decimal(Sensor): + """Sensor representing signed decimal value encoded in 2 bytes""" + + def __init__(self, id_: str, offset: int, scale: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 2, unit, kind) + self.scale = scale + + def read_value(self, data: io.BytesIO): + return read_decimal2(data, self.scale) + + def encode_value(self, value: Any) -> bytes: + return int.to_bytes(int(value * self.scale), length=2, byteorder="big", signed=True) + + +class Float(Sensor): + """Sensor representing signed int value encoded in 4 bytes""" + + def __init__(self, id_: str, offset: int, scale: int, name: str, unit: str = "", kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 4, unit, kind) + self.scale = scale + + def read_value(self, data: io.BytesIO): + return round(read_float4(data) / self.scale, 3) + + +class Timestamp(Sensor): + """Sensor representing datetime value encoded in 6 bytes""" + + def __init__(self, id_: str, offset: int, name: str, kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 6, "", kind) + + def read_value(self, data: io.BytesIO): + return read_datetime(data) + + def encode_value(self, value: Any) -> bytes: + return encode_datetime(value) + + +class Enum(Sensor): + """Sensor representing label from enumeration encoded in 1 bytes""" + + def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 1, "", kind) + self._labels: Dict = labels + + def read_value(self, data: io.BytesIO): + return self._labels.get(read_byte(data)) + + +class Enum2(Sensor): + """Sensor representing label from enumeration encoded in 2 bytes""" + + def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 2, "", kind) + self._labels: Dict = labels + + def read_value(self, data: io.BytesIO): + return self._labels.get(read_bytes2(data)) + + +class EnumBitmap4(Sensor): + """Sensor representing label from bitmap encoded in 4 bytes""" + + def __init__(self, id_: str, offset: int, labels: Dict, name: str, kind: Optional[SensorKind] = None): + super().__init__(id_, offset, name, 4, "", kind) + self._labels: Dict = labels + + def read_value(self, data: io.BytesIO) -> Any: + raise NotImplementedError() + + def read(self, data: io.BytesIO): + return decode_bitmap(read_bytes4(data, self.offset), self._labels) + + +class EnumBitmap22(Sensor): + """Sensor representing label from bitmap encoded in 2+2 bytes""" + + def __init__(self, id_: str, offsetH: int, offsetL: int, labels: Dict, name: str, + kind: Optional[SensorKind] = None): + super().__init__(id_, offsetH, name, 2, "", kind) + self._labels: Dict = labels + self._offsetL: int = offsetL + + def read_value(self, data: io.BytesIO) -> Any: + raise NotImplementedError() + + def read(self, data: io.BytesIO): + return decode_bitmap(read_bytes2(data, self.offset) << 16 + read_bytes2(data, self._offsetL), self._labels) + + +class EnumCalculated(Sensor): + """Sensor representing label from enumeration of calculated value""" + + def __init__(self, id_: str, getter: Callable[[io.BytesIO], Any], labels: Dict, name: str, + kind: Optional[SensorKind] = None): + super().__init__(id_, 0, name, 0, "", kind) + self._getter: Callable[[io.BytesIO], Any] = getter + self._labels: Dict = labels + + def read_value(self, data: io.BytesIO) -> Any: + raise NotImplementedError() + + def read(self, data: io.BytesIO): + return self._labels.get(self._getter(data)) + + +class EcoMode(ABC): + """Sensor representing Eco Mode Battery Power Group API""" + + @abstractmethod + def encode_charge(self, eco_mode_power: int, eco_mode_soc: int = 100) -> bytes: + """Answer bytes representing all the time enabled charging eco mode group""" + + @abstractmethod + def encode_discharge(self, eco_mode_power: int) -> bytes: + """Answer bytes representing all the time enabled discharging eco mode group""" + + @abstractmethod + def encode_off(self) -> bytes: + """Answer bytes representing empty and disabled eco mode group""" + + @abstractmethod + def is_eco_charge_mode(self) -> bool: + """Answer if it represents the emulated 24/7 fulltime discharge mode""" + + @abstractmethod + def is_eco_discharge_mode(self) -> bool: + """Answer if it represents the emulated 24/7 fulltime discharge mode""" + + +class EcoModeV1(Sensor, EcoMode): + """Sensor representing Eco Mode Battery Power Group encoded in 8 bytes""" + + def __init__(self, id_: str, offset: int, name: str): + super().__init__(id_, offset, name, 8, "", SensorKind.BAT) + self.start_h: int | None = None + self.start_m: int | None = None + self.end_h: int | None = None + self.end_m: int | None = None + self.power: int | None = None + self.on_off: int | None = None + self.day_bits: int | None = None + self.days: str | None = None + self.soc: int = 100 # just to keep same API with V2 + + def __str__(self): + return f"{self.start_h}:{self.start_m}-{self.end_h}:{self.end_m} {self.days} {self.power}% {'On' if self.on_off != 0 else 'Off'}" + + def read_value(self, data: io.BytesIO): + self.start_h = read_byte(data) + if (self.start_h < 0 or self.start_h > 23) and self.start_h != 48: + raise ValueError(f"{self.id_}: start_h value {self.start_h} out of range.") + self.start_m = read_byte(data) + if self.start_m < 0 or self.start_m > 59: + raise ValueError(f"{self.id_}: start_m value {self.start_m} out of range.") + self.end_h = read_byte(data) + if (self.end_h < 0 or self.end_h > 23) and self.end_h != 48: + raise ValueError(f"{self.id_}: end_h value {self.end_h} out of range.") + self.end_m = read_byte(data) + if self.end_m < 0 or self.end_m > 59: + raise ValueError(f"{self.id_}: end_m value {self.end_m} out of range.") + self.power = read_bytes2(data) # negative=charge, positive=discharge + if self.power < -100 or self.power > 100: + raise ValueError(f"{self.id_}: power value {self.power} out of range.") + self.on_off = read_byte(data) + if self.on_off not in (0, -1): + raise ValueError(f"{self.id_}: on_off value {self.on_off} out of range.") + self.day_bits = read_byte(data) + self.days = decode_day_of_week(self.day_bits) + if self.day_bits < 0: + raise ValueError(f"{self.id_}: day_bits value {self.day_bits} out of range.") + return self + + def encode_value(self, value: Any) -> bytes: + if isinstance(value, bytes) and len(value) == 8: + # try to read_value to check if values are valid + if self.read_value(io.BytesIO(value)): + return value + raise ValueError + + def encode_charge(self, eco_mode_power: int, eco_mode_soc: int = 100) -> bytes: + """Answer bytes representing all the time enabled charging eco mode group""" + return bytes.fromhex("0000173b{:04x}ff7f".format((-1 * abs(eco_mode_power)) & (2 ** 16 - 1))) + + def encode_discharge(self, eco_mode_power: int) -> bytes: + """Answer bytes representing all the time enabled discharging eco mode group""" + return bytes.fromhex("0000173b{:04x}ff7f".format(abs(eco_mode_power))) + + def encode_off(self) -> bytes: + """Answer bytes representing empty and disabled eco mode group""" + return bytes.fromhex("3000300000640000") + + def is_eco_charge_mode(self) -> bool: + """Answer if it represents the emulated 24/7 fulltime discharge mode""" + return self.start_h == 0 \ + and self.start_m == 0 \ + and self.end_h == 23 \ + and self.end_m == 59 \ + and self.on_off != 0 \ + and self.day_bits == 127 \ + and self.power < 0 + + def is_eco_discharge_mode(self) -> bool: + """Answer if it represents the emulated 24/7 fulltime discharge mode""" + return self.start_h == 0 \ + and self.start_m == 0 \ + and self.end_h == 23 \ + and self.end_m == 59 \ + and self.on_off != 0 \ + and self.day_bits == 127 \ + and self.power > 0 + + def as_eco_mode_v2(self) -> EcoModeV2: + """Convert V1 to V2 EcoMode""" + result = EcoModeV2(self.id_, self.offset, self.name) + result.start_h = self.start_h + result.start_m = self.start_m + result.end_h = self.end_h + result.end_m = self.end_m + result.power = self.power + result.on_off = self.on_off + result.day_bits = self.day_bits + result.days = decode_day_of_week(self.day_bits) + result.soc = 100 + return result + + +class EcoModeV2(Sensor, EcoMode): + """Sensor representing Eco Mode Battery Power Group encoded in 12 bytes""" + + def __init__(self, id_: str, offset: int, name: str): + super().__init__(id_, offset, name, 12, "", SensorKind.BAT) + self.start_h: int | None = None + self.start_m: int | None = None + self.end_h: int | None = None + self.end_m: int | None = None + self.on_off: int | None = None + self.day_bits: int | None = None + self.days: str | None = None + self.power: int | None = None + self.soc: int | None = None + # 2 bytes padding 0000 + + def __str__(self): + return f"{self.start_h}:{self.start_m}-{self.end_h}:{self.end_m} {self.days} {self.power}% (SoC {self.soc}%) {'On' if self.on_off != 0 else 'Off'}" + + def read_value(self, data: io.BytesIO): + self.start_h = read_byte(data) + if (self.start_h < 0 or self.start_h > 23) and self.start_h != 48: + raise ValueError(f"{self.id_}: start_h value {self.start_h} out of range.") + self.start_m = read_byte(data) + if self.start_m < 0 or self.start_m > 59: + raise ValueError(f"{self.id_}: start_m value {self.start_m} out of range.") + self.end_h = read_byte(data) + if (self.end_h < 0 or self.end_h > 23) and self.end_h != 48: + raise ValueError(f"{self.id_}: end_h value {self.end_h} out of range.") + self.end_m = read_byte(data) + if self.end_m < 0 or self.end_m > 59: + raise ValueError(f"{self.id_}: end_m value {self.end_m} out of range.") + self.on_off = read_byte(data) + if self.on_off not in (0, -1): + raise ValueError(f"{self.id_}: on_off value {self.on_off} out of range.") + self.day_bits = read_byte(data) + self.days = decode_day_of_week(self.day_bits) + if self.day_bits < 0: + raise ValueError(f"{self.id_}: day_bits value {self.day_bits} out of range.") + self.power = read_bytes2(data) # negative=charge, positive=discharge + if self.power < -100 or self.power > 100: + raise ValueError(f"{self.id_}: power value {self.power} out of range.") + self.soc = read_bytes2(data) + if self.soc < 0 or self.soc > 100: + raise ValueError(f"{self.id_}: SoC value {self.soc} out of range.") + return self + + def encode_value(self, value: Any) -> bytes: + if isinstance(value, bytes) and len(value) == 12: + # try to read_value to check if values are valid + if self.read_value(io.BytesIO(value)): + return value + raise ValueError + + def encode_charge(self, eco_mode_power: int, eco_mode_soc: int = 100) -> bytes: + """Answer bytes representing all the time enabled charging eco mode group""" + return bytes.fromhex( + "0000173bff7f{:04x}{:04x}0000".format((-1 * abs(eco_mode_power)) & (2 ** 16 - 1), eco_mode_soc)) + + def encode_discharge(self, eco_mode_power: int) -> bytes: + """Answer bytes representing all the time enabled discharging eco mode group""" + return bytes.fromhex("0000173bff7f{:04x}00640000".format(abs(eco_mode_power))) + + def encode_off(self) -> bytes: + """Answer bytes representing empty and disabled eco mode group""" + return bytes.fromhex("300030000000006400640000") + + def is_eco_charge_mode(self) -> bool: + """Answer if it represents the emulated 24/7 fulltime discharge mode""" + return self.start_h == 0 \ + and self.start_m == 0 \ + and self.end_h == 23 \ + and self.end_m == 59 \ + and self.on_off != 0 \ + and self.day_bits == 127 \ + and self.power < 0 + + def is_eco_discharge_mode(self) -> bool: + """Answer if it represents the emulated 24/7 fulltime discharge mode""" + return self.start_h == 0 \ + and self.start_m == 0 \ + and self.end_h == 23 \ + and self.end_m == 59 \ + and self.on_off != 0 \ + and self.day_bits == 127 \ + and self.power > 0 + + def as_eco_mode_v1(self) -> EcoModeV1: + """Convert V2 to V1 EcoMode""" + result = EcoModeV1(self.id_, self.offset, self.name) + result.start_h = self.start_h + result.start_m = self.start_m + result.end_h = self.end_h + result.end_m = self.end_m + result.power = self.power + result.on_off = self.on_off + result.day_bits = self.day_bits + result.days = self.days + return result + + +class PeakShavingMode(Sensor): + """Sensor representing Peak Shaving Mode encoded in 12 bytes""" + + def __init__(self, id_: str, offset: int, name: str): + super().__init__(id_, offset, name, 12, "", SensorKind.BAT) + self.start_h: int | None = None + self.start_m: int | None = None + self.end_h: int | None = None + self.end_m: int | None = None + self.on_off: int | None = None + self.day_bits: int | None = None + self.days: str | None = None + self.import_power: float | None = None + self.soc: int | None = None + # 2 bytes padding 0000 + + def __str__(self): + return f"{self.start_h}:{self.start_m}-{self.end_h}:{self.end_m} {self.days} {self.import_power}kW (SoC {self.soc}%) {'On' if self.on_off == -4 else 'Off'}" + + def read_value(self, data: io.BytesIO): + self.start_h = read_byte(data) + if (self.start_h < 0 or self.start_h > 23) and self.start_h != 48: + raise ValueError(f"{self.id_}: start_h value {self.start_h} out of range.") + self.start_m = read_byte(data) + if self.start_m < 0 or self.start_m > 59: + raise ValueError(f"{self.id_}: start_m value {self.start_m} out of range.") + self.end_h = read_byte(data) + if (self.end_h < 0 or self.end_h > 23) and self.end_h != 48: + raise ValueError(f"{self.id_}: end_h value {self.end_h} out of range.") + self.end_m = read_byte(data) + if self.end_m < 0 or self.end_m > 59: + raise ValueError(f"{self.id_}: end_m value {self.end_m} out of range.") + self.on_off = read_byte(data) + if self.on_off not in (-4, 3): + raise ValueError(f"{self.id_}: on_off value {self.on_off} out of range.") + self.day_bits = read_byte(data) + self.days = decode_day_of_week(self.day_bits) + if self.day_bits < 0: + raise ValueError(f"{self.id_}: day_bits value {self.day_bits} out of range.") + self.import_power = read_decimal2(data, 100) + if self.import_power < 0 or self.import_power > 500: + raise ValueError(f"{self.id_}: import_power value {self.import_power} out of range.") + self.soc = read_bytes2(data) + if self.soc < 0 or self.soc > 100: + raise ValueError(f"{self.id_}: soc value {self.soc} out of range.") + return self + + def encode_value(self, value: Any) -> bytes: + if isinstance(value, bytes) and len(value) == 12: + # try to read_value to check if values are valid + if self.read_value(io.BytesIO(value)): + return value + raise ValueError + + def encode_off(self) -> bytes: + """Answer bytes representing empty and disabled eco mode group""" + return bytes.fromhex("300030000000006400640000") + + +class Calculated(Sensor): + """Sensor representing calculated value""" + + def __init__(self, id_: str, getter: Callable[[io.BytesIO], Any], name: str, unit: str, + kind: Optional[SensorKind] = None): + super().__init__(id_, 0, name, 0, unit, kind) + self._getter: Callable[[io.BytesIO], Any] = getter + + def read_value(self, data: io.BytesIO) -> Any: + raise NotImplementedError() + + def read(self, data: io.BytesIO): + return self._getter(data) + + +def read_byte(buffer: io.BytesIO, offset: int = None) -> int: + """Retrieve single byte (signed int) value from buffer""" + if offset is not None: + buffer.seek(offset) + return int.from_bytes(buffer.read(1), byteorder="big", signed=True) + + +def read_bytes2(buffer: io.BytesIO, offset: int = None) -> int: + """Retrieve 2 byte (signed int) value from buffer""" + if offset is not None: + buffer.seek(offset) + return int.from_bytes(buffer.read(2), byteorder="big", signed=True) + + +def read_bytes4(buffer: io.BytesIO, offset: int = None) -> int: + """Retrieve 4 byte (signed int) value from buffer""" + if offset is not None: + buffer.seek(offset) + return int.from_bytes(buffer.read(4), byteorder="big", signed=True) + + +def read_decimal2(buffer: io.BytesIO, scale: int, offset: int = None) -> float: + """Retrieve 2 byte (signed float) value from buffer""" + if offset is not None: + buffer.seek(offset) + return float(int.from_bytes(buffer.read(2), byteorder="big", signed=True)) / scale + + +def read_float4(buffer: io.BytesIO, offset: int = None) -> float: + """Retrieve 4 byte (signed float) value from buffer""" + if offset is not None: + buffer.seek(offset) + data = buffer.read(4) + if len(data) == 4: + return unpack('>f', data)[0] + else: + return float(0) + + +def read_voltage(buffer: io.BytesIO, offset: int = None) -> float: + """Retrieve voltage [V] value (2 bytes) from buffer""" + if offset is not None: + buffer.seek(offset) + value = int.from_bytes(buffer.read(2), byteorder="big", signed=True) + return float(value) / 10 + + +def encode_voltage(value: Any) -> bytes: + """Encode voltage value to raw (2 bytes) payload""" + return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=True) + + +def read_current(buffer: io.BytesIO, offset: int = None) -> float: + """Retrieve current [A] value (2 bytes) from buffer""" + if offset is not None: + buffer.seek(offset) + value = int.from_bytes(buffer.read(2), byteorder="big", signed=True) + return float(value) / 10 + + +def encode_current(value: Any) -> bytes: + """Encode current value to raw (2 bytes) payload""" + return int.to_bytes(int(value * 10), length=2, byteorder="big", signed=True) + + +def read_freq(buffer: io.BytesIO, offset: int = None) -> float: + """Retrieve frequency [Hz] value (2 bytes) from buffer""" + if offset is not None: + buffer.seek(offset) + value = int.from_bytes(buffer.read(2), byteorder="big", signed=True) + return float(value) / 100 + + +def read_temp(buffer: io.BytesIO, offset: int = None) -> float: + """Retrieve temperature [C] value (2 bytes) from buffer""" + if offset is not None: + buffer.seek(offset) + value = int.from_bytes(buffer.read(2), byteorder="big", signed=True) + return float(value) / 10 + + +def read_datetime(buffer: io.BytesIO, offset: int = None) -> datetime: + """Retrieve datetime value (6 bytes) from buffer""" + if offset is not None: + buffer.seek(offset) + year = 2000 + int.from_bytes(buffer.read(1), byteorder='big') + month = int.from_bytes(buffer.read(1), byteorder='big') + day = int.from_bytes(buffer.read(1), byteorder='big') + hour = int.from_bytes(buffer.read(1), byteorder='big') + minute = int.from_bytes(buffer.read(1), byteorder='big') + second = int.from_bytes(buffer.read(1), byteorder='big') + return datetime(year=year, month=month, day=day, hour=hour, minute=minute, second=second) + + +def encode_datetime(value: Any) -> bytes: + """Encode datetime value to raw (6 bytes) payload""" + timestamp = value + if isinstance(value, str): + timestamp = datetime.fromisoformat(value) + + result = bytes([ + timestamp.year - 2000, + timestamp.month, + timestamp.day, + timestamp.hour, + timestamp.minute, + timestamp.second, + ]) + return result + + +def read_grid_mode(buffer: io.BytesIO, offset: int = None) -> int: + """Retrieve 'grid mode' sign value from buffer""" + value = read_bytes2(buffer, offset) + if value < -90: + return 2 + elif value >= 90: + return 1 + else: + return 0 + + +def read_unsigned_int(data: bytes, offset: int) -> int: + """Retrieve 2 byte (unsigned int) value from bytes at specified offset""" + return int.from_bytes(data[offset:offset + 2], byteorder="big", signed=False) + + +def decode_bitmap(value: int, bitmap: Dict[int, str]) -> str: + bits = value + result = [] + for i in range(32): + if bits & 0x1 == 1: + result.append(bitmap.get(i, f'err{i}')) + bits = bits >> 1 + return ", ".join(result) + + +def decode_day_of_week(data: int) -> str: + bits = bin(data)[2:] + daynames = list(DAY_NAMES) + days = "" + for each in bits[::-1]: + if each == '1': + if len(days) > 0: + days += "," + days += daynames[0] + daynames.pop(0) + return days diff --git a/goodwe/xs.py b/goodwe/xs.py new file mode 100644 index 0000000..08fd79c --- /dev/null +++ b/goodwe/xs.py @@ -0,0 +1,43 @@ +import logging + +from .dt import DT +from .processor import ProcessorResult, AbstractDataProcessor +from .protocol import ProtocolCommand +from .sensor import * + +logger = logging.getLogger(__name__) + + +class GoodWeXSProcessor(AbstractDataProcessor): + + def __init__(self): + self.dummy_inverter = DT("localhost") + + def process_data(self, data: bytes) -> ProcessorResult: + """Process the data provided by the GoodWe XS inverter and return ProcessorResult""" + sensors = self.dummy_inverter._map_response(data[5:-2], self.dummy_inverter.sensors()) + + return ProcessorResult( + date=sensors['timestamp'], + volts_dc=sensors['vpv1'], + current_dc=sensors['ipv1'], + volts_ac=sensors['vgrid1'], + current_ac=sensors['igrid1'], + frequency_ac=sensors['fgrid1'], + generation_today=sensors['e_day'], + generation_total=sensors['e_total'], + # this is just response checksum + rssi=self._get_rssi(data), + operational_hours=sensors['h_total'], + temperature=sensors['temperature'], + power=sensors['ppv'], + status=sensors['work_mode_label']) + + def _get_rssi(self, data) -> float: + """Retrieve rssi from GoodWe data""" + with io.BytesIO(data) as buffer: + return read_bytes2(buffer, 149) + + def get_runtime_data_command(self) -> ProtocolCommand: + """Answer protocol command for reading runtime data""" + return self.dummy_inverter._READ_DEVICE_RUNNING_DATA diff --git a/google/__init__.py b/google/__init__.py new file mode 100644 index 0000000..5585614 --- /dev/null +++ b/google/__init__.py @@ -0,0 +1,4 @@ +try: + __import__('pkg_resources').declare_namespace(__name__) +except ImportError: + __path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/google/protobuf/__init__.py b/google/protobuf/__init__.py new file mode 100644 index 0000000..297cccf --- /dev/null +++ b/google/protobuf/__init__.py @@ -0,0 +1,10 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +# Copyright 2007 Google Inc. All Rights Reserved. + +__version__ = '5.27.3' diff --git a/google/protobuf/any_pb2.py b/google/protobuf/any_pb2.py new file mode 100644 index 0000000..4d4b8a0 --- /dev/null +++ b/google/protobuf/any_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/any.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/any.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/any.proto\x12\x0fgoogle.protobuf\"6\n\x03\x41ny\x12\x19\n\x08type_url\x18\x01 \x01(\tR\x07typeUrl\x12\x14\n\x05value\x18\x02 \x01(\x0cR\x05valueBv\n\x13\x63om.google.protobufB\x08\x41nyProtoP\x01Z,google.golang.org/protobuf/types/known/anypb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.any_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\010AnyProtoP\001Z,google.golang.org/protobuf/types/known/anypb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _globals['_ANY']._serialized_start=46 + _globals['_ANY']._serialized_end=100 +# @@protoc_insertion_point(module_scope) diff --git a/google/protobuf/api_pb2.py b/google/protobuf/api_pb2.py new file mode 100644 index 0000000..8d247d9 --- /dev/null +++ b/google/protobuf/api_pb2.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/api.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/api.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 +from google.protobuf import type_pb2 as google_dot_protobuf_dot_type__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19google/protobuf/api.proto\x12\x0fgoogle.protobuf\x1a$google/protobuf/source_context.proto\x1a\x1agoogle/protobuf/type.proto\"\xc1\x02\n\x03\x41pi\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x31\n\x07methods\x18\x02 \x03(\x0b\x32\x17.google.protobuf.MethodR\x07methods\x12\x31\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\x12\x18\n\x07version\x18\x04 \x01(\tR\x07version\x12\x45\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContextR\rsourceContext\x12.\n\x06mixins\x18\x06 \x03(\x0b\x32\x16.google.protobuf.MixinR\x06mixins\x12/\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.SyntaxR\x06syntax\"\xb2\x02\n\x06Method\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12(\n\x10request_type_url\x18\x02 \x01(\tR\x0erequestTypeUrl\x12+\n\x11request_streaming\x18\x03 \x01(\x08R\x10requestStreaming\x12*\n\x11response_type_url\x18\x04 \x01(\tR\x0fresponseTypeUrl\x12-\n\x12response_streaming\x18\x05 \x01(\x08R\x11responseStreaming\x12\x31\n\x07options\x18\x06 \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\x12/\n\x06syntax\x18\x07 \x01(\x0e\x32\x17.google.protobuf.SyntaxR\x06syntax\"/\n\x05Mixin\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n\x04root\x18\x02 \x01(\tR\x04rootBv\n\x13\x63om.google.protobufB\x08\x41piProtoP\x01Z,google.golang.org/protobuf/types/known/apipb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.api_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\010ApiProtoP\001Z,google.golang.org/protobuf/types/known/apipb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _globals['_API']._serialized_start=113 + _globals['_API']._serialized_end=434 + _globals['_METHOD']._serialized_start=437 + _globals['_METHOD']._serialized_end=743 + _globals['_MIXIN']._serialized_start=745 + _globals['_MIXIN']._serialized_end=792 +# @@protoc_insertion_point(module_scope) diff --git a/google/protobuf/compiler/__init__.py b/google/protobuf/compiler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/google/protobuf/compiler/plugin_pb2.py b/google/protobuf/compiler/plugin_pb2.py new file mode 100644 index 0000000..c7ab83b --- /dev/null +++ b/google/protobuf/compiler/plugin_pb2.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/compiler/plugin.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/compiler/plugin.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%google/protobuf/compiler/plugin.proto\x12\x18google.protobuf.compiler\x1a google/protobuf/descriptor.proto\"c\n\x07Version\x12\x14\n\x05major\x18\x01 \x01(\x05R\x05major\x12\x14\n\x05minor\x18\x02 \x01(\x05R\x05minor\x12\x14\n\x05patch\x18\x03 \x01(\x05R\x05patch\x12\x16\n\x06suffix\x18\x04 \x01(\tR\x06suffix\"\xcf\x02\n\x14\x43odeGeneratorRequest\x12(\n\x10\x66ile_to_generate\x18\x01 \x03(\tR\x0e\x66ileToGenerate\x12\x1c\n\tparameter\x18\x02 \x01(\tR\tparameter\x12\x43\n\nproto_file\x18\x0f \x03(\x0b\x32$.google.protobuf.FileDescriptorProtoR\tprotoFile\x12\\\n\x17source_file_descriptors\x18\x11 \x03(\x0b\x32$.google.protobuf.FileDescriptorProtoR\x15sourceFileDescriptors\x12L\n\x10\x63ompiler_version\x18\x03 \x01(\x0b\x32!.google.protobuf.compiler.VersionR\x0f\x63ompilerVersion\"\x85\x04\n\x15\x43odeGeneratorResponse\x12\x14\n\x05\x65rror\x18\x01 \x01(\tR\x05\x65rror\x12-\n\x12supported_features\x18\x02 \x01(\x04R\x11supportedFeatures\x12\'\n\x0fminimum_edition\x18\x03 \x01(\x05R\x0eminimumEdition\x12\'\n\x0fmaximum_edition\x18\x04 \x01(\x05R\x0emaximumEdition\x12H\n\x04\x66ile\x18\x0f \x03(\x0b\x32\x34.google.protobuf.compiler.CodeGeneratorResponse.FileR\x04\x66ile\x1a\xb1\x01\n\x04\x46ile\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\'\n\x0finsertion_point\x18\x02 \x01(\tR\x0einsertionPoint\x12\x18\n\x07\x63ontent\x18\x0f \x01(\tR\x07\x63ontent\x12R\n\x13generated_code_info\x18\x10 \x01(\x0b\x32\".google.protobuf.GeneratedCodeInfoR\x11generatedCodeInfo\"W\n\x07\x46\x65\x61ture\x12\x10\n\x0c\x46\x45\x41TURE_NONE\x10\x00\x12\x1b\n\x17\x46\x45\x41TURE_PROTO3_OPTIONAL\x10\x01\x12\x1d\n\x19\x46\x45\x41TURE_SUPPORTS_EDITIONS\x10\x02\x42r\n\x1c\x63om.google.protobuf.compilerB\x0cPluginProtosZ)google.golang.org/protobuf/types/pluginpb\xaa\x02\x18Google.Protobuf.Compiler') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.compiler.plugin_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.google.protobuf.compilerB\014PluginProtosZ)google.golang.org/protobuf/types/pluginpb\252\002\030Google.Protobuf.Compiler' + _globals['_VERSION']._serialized_start=101 + _globals['_VERSION']._serialized_end=200 + _globals['_CODEGENERATORREQUEST']._serialized_start=203 + _globals['_CODEGENERATORREQUEST']._serialized_end=538 + _globals['_CODEGENERATORRESPONSE']._serialized_start=541 + _globals['_CODEGENERATORRESPONSE']._serialized_end=1058 + _globals['_CODEGENERATORRESPONSE_FILE']._serialized_start=792 + _globals['_CODEGENERATORRESPONSE_FILE']._serialized_end=969 + _globals['_CODEGENERATORRESPONSE_FEATURE']._serialized_start=971 + _globals['_CODEGENERATORRESPONSE_FEATURE']._serialized_end=1058 +# @@protoc_insertion_point(module_scope) diff --git a/google/protobuf/descriptor.py b/google/protobuf/descriptor.py new file mode 100644 index 0000000..d8c6a43 --- /dev/null +++ b/google/protobuf/descriptor.py @@ -0,0 +1,1511 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Descriptors essentially contain exactly the information found in a .proto +file, in types that make this information accessible in Python. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import abc +import binascii +import os +import threading +import warnings + +from google.protobuf.internal import api_implementation + +_USE_C_DESCRIPTORS = False +if api_implementation.Type() != 'python': + # pylint: disable=protected-access + _message = api_implementation._c_module + # TODO: Remove this import after fix api_implementation + if _message is None: + from google.protobuf.pyext import _message + _USE_C_DESCRIPTORS = True + + +class Error(Exception): + """Base error for this module.""" + + +class TypeTransformationError(Error): + """Error transforming between python proto type and corresponding C++ type.""" + + +if _USE_C_DESCRIPTORS: + # This metaclass allows to override the behavior of code like + # isinstance(my_descriptor, FieldDescriptor) + # and make it return True when the descriptor is an instance of the extension + # type written in C++. + class DescriptorMetaclass(type): + + def __instancecheck__(cls, obj): + if super(DescriptorMetaclass, cls).__instancecheck__(obj): + return True + if isinstance(obj, cls._C_DESCRIPTOR_CLASS): + return True + return False +else: + # The standard metaclass; nothing changes. + DescriptorMetaclass = abc.ABCMeta + + +class _Lock(object): + """Wrapper class of threading.Lock(), which is allowed by 'with'.""" + + def __new__(cls): + self = object.__new__(cls) + self._lock = threading.Lock() # pylint: disable=protected-access + return self + + def __enter__(self): + self._lock.acquire() + + def __exit__(self, exc_type, exc_value, exc_tb): + self._lock.release() + + +_lock = threading.Lock() + + +def _Deprecated(name): + if _Deprecated.count > 0: + _Deprecated.count -= 1 + warnings.warn( + 'Call to deprecated create function %s(). Note: Create unlinked ' + 'descriptors is going to go away. Please use get/find descriptors from ' + 'generated code or query the descriptor_pool.' + % name, + category=DeprecationWarning, stacklevel=3) + +# These must match the values in descriptor.proto, but we can't use them +# directly because we sometimes need to reference them in feature helpers +# below *during* the build of descriptor.proto. +_FEATURESET_MESSAGE_ENCODING_DELIMITED = 2 +_FEATURESET_FIELD_PRESENCE_IMPLICIT = 2 +_FEATURESET_FIELD_PRESENCE_LEGACY_REQUIRED = 3 +_FEATURESET_REPEATED_FIELD_ENCODING_PACKED = 1 +_FEATURESET_ENUM_TYPE_CLOSED = 2 + +# Deprecated warnings will print 100 times at most which should be enough for +# users to notice and do not cause timeout. +_Deprecated.count = 100 + + +_internal_create_key = object() + + +class DescriptorBase(metaclass=DescriptorMetaclass): + + """Descriptors base class. + + This class is the base of all descriptor classes. It provides common options + related functionality. + + Attributes: + has_options: True if the descriptor has non-default options. Usually it is + not necessary to read this -- just call GetOptions() which will happily + return the default instance. However, it's sometimes useful for + efficiency, and also useful inside the protobuf implementation to avoid + some bootstrapping issues. + file (FileDescriptor): Reference to file info. + """ + + if _USE_C_DESCRIPTORS: + # The class, or tuple of classes, that are considered as "virtual + # subclasses" of this descriptor class. + _C_DESCRIPTOR_CLASS = () + + def __init__(self, file, options, serialized_options, options_class_name): + """Initialize the descriptor given its options message and the name of the + class of the options message. The name of the class is required in case + the options message is None and has to be created. + """ + self._features = None + self.file = file + self._options = options + self._loaded_options = None + self._options_class_name = options_class_name + self._serialized_options = serialized_options + + # Does this descriptor have non-default options? + self.has_options = (self._options is not None) or ( + self._serialized_options is not None + ) + + @property + @abc.abstractmethod + def _parent(self): + pass + + def _InferLegacyFeatures(self, edition, options, features): + """Infers features from proto2/proto3 syntax so that editions logic can be used everywhere. + + Args: + edition: The edition to infer features for. + options: The options for this descriptor that are being processed. + features: The feature set object to modify with inferred features. + """ + pass + + def _GetFeatures(self): + if not self._features: + self._LazyLoadOptions() + return self._features + + def _ResolveFeatures(self, edition, raw_options): + """Resolves features from the raw options of this descriptor. + + Args: + edition: The edition to use for feature defaults. + raw_options: The options for this descriptor that are being processed. + + Returns: + A fully resolved feature set for making runtime decisions. + """ + # pylint: disable=g-import-not-at-top + from google.protobuf import descriptor_pb2 + + if self._parent: + features = descriptor_pb2.FeatureSet() + features.CopyFrom(self._parent._GetFeatures()) + else: + features = self.file.pool._CreateDefaultFeatures(edition) + unresolved = descriptor_pb2.FeatureSet() + unresolved.CopyFrom(raw_options.features) + self._InferLegacyFeatures(edition, raw_options, unresolved) + features.MergeFrom(unresolved) + + # Use the feature cache to reduce memory bloat. + return self.file.pool._InternFeatures(features) + + def _LazyLoadOptions(self): + """Lazily initializes descriptor options towards the end of the build.""" + if self._loaded_options: + return + + # pylint: disable=g-import-not-at-top + from google.protobuf import descriptor_pb2 + + if not hasattr(descriptor_pb2, self._options_class_name): + raise RuntimeError( + 'Unknown options class name %s!' % self._options_class_name + ) + options_class = getattr(descriptor_pb2, self._options_class_name) + features = None + edition = self.file._edition + + if not self.has_options: + if not self._features: + features = self._ResolveFeatures( + descriptor_pb2.Edition.Value(edition), options_class() + ) + with _lock: + self._loaded_options = options_class() + if not self._features: + self._features = features + else: + if not self._serialized_options: + options = self._options + else: + options = _ParseOptions(options_class(), self._serialized_options) + + if not self._features: + features = self._ResolveFeatures( + descriptor_pb2.Edition.Value(edition), options + ) + with _lock: + self._loaded_options = options + if not self._features: + self._features = features + if options.HasField('features'): + options.ClearField('features') + if not options.SerializeToString(): + self._loaded_options = options_class() + self.has_options = False + + def GetOptions(self): + """Retrieves descriptor options. + + Returns: + The options set on this descriptor. + """ + if not self._loaded_options: + self._LazyLoadOptions() + return self._loaded_options + + +class _NestedDescriptorBase(DescriptorBase): + """Common class for descriptors that can be nested.""" + + def __init__(self, options, options_class_name, name, full_name, + file, containing_type, serialized_start=None, + serialized_end=None, serialized_options=None): + """Constructor. + + Args: + options: Protocol message options or None to use default message options. + options_class_name (str): The class name of the above options. + name (str): Name of this protocol message type. + full_name (str): Fully-qualified name of this protocol message type, which + will include protocol "package" name and the name of any enclosing + types. + containing_type: if provided, this is a nested descriptor, with this + descriptor as parent, otherwise None. + serialized_start: The start index (inclusive) in block in the + file.serialized_pb that describes this descriptor. + serialized_end: The end index (exclusive) in block in the + file.serialized_pb that describes this descriptor. + serialized_options: Protocol message serialized options or None. + """ + super(_NestedDescriptorBase, self).__init__( + file, options, serialized_options, options_class_name + ) + + self.name = name + # TODO: Add function to calculate full_name instead of having it in + # memory? + self.full_name = full_name + self.containing_type = containing_type + + self._serialized_start = serialized_start + self._serialized_end = serialized_end + + def CopyToProto(self, proto): + """Copies this to the matching proto in descriptor_pb2. + + Args: + proto: An empty proto instance from descriptor_pb2. + + Raises: + Error: If self couldn't be serialized, due to to few constructor + arguments. + """ + if (self.file is not None and + self._serialized_start is not None and + self._serialized_end is not None): + proto.ParseFromString(self.file.serialized_pb[ + self._serialized_start:self._serialized_end]) + else: + raise Error('Descriptor does not contain serialization.') + + +class Descriptor(_NestedDescriptorBase): + + """Descriptor for a protocol message type. + + Attributes: + name (str): Name of this protocol message type. + full_name (str): Fully-qualified name of this protocol message type, + which will include protocol "package" name and the name of any + enclosing types. + containing_type (Descriptor): Reference to the descriptor of the type + containing us, or None if this is top-level. + fields (list[FieldDescriptor]): Field descriptors for all fields in + this type. + fields_by_number (dict(int, FieldDescriptor)): Same + :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed + by "number" attribute in each FieldDescriptor. + fields_by_name (dict(str, FieldDescriptor)): Same + :class:`FieldDescriptor` objects as in :attr:`fields`, but indexed by + "name" attribute in each :class:`FieldDescriptor`. + nested_types (list[Descriptor]): Descriptor references + for all protocol message types nested within this one. + nested_types_by_name (dict(str, Descriptor)): Same Descriptor + objects as in :attr:`nested_types`, but indexed by "name" attribute + in each Descriptor. + enum_types (list[EnumDescriptor]): :class:`EnumDescriptor` references + for all enums contained within this type. + enum_types_by_name (dict(str, EnumDescriptor)): Same + :class:`EnumDescriptor` objects as in :attr:`enum_types`, but + indexed by "name" attribute in each EnumDescriptor. + enum_values_by_name (dict(str, EnumValueDescriptor)): Dict mapping + from enum value name to :class:`EnumValueDescriptor` for that value. + extensions (list[FieldDescriptor]): All extensions defined directly + within this message type (NOT within a nested type). + extensions_by_name (dict(str, FieldDescriptor)): Same FieldDescriptor + objects as :attr:`extensions`, but indexed by "name" attribute of each + FieldDescriptor. + is_extendable (bool): Does this type define any extension ranges? + oneofs (list[OneofDescriptor]): The list of descriptors for oneof fields + in this message. + oneofs_by_name (dict(str, OneofDescriptor)): Same objects as in + :attr:`oneofs`, but indexed by "name" attribute. + file (FileDescriptor): Reference to file descriptor. + is_map_entry: If the message type is a map entry. + + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.Descriptor + + def __new__( + cls, + name=None, + full_name=None, + filename=None, + containing_type=None, + fields=None, + nested_types=None, + enum_types=None, + extensions=None, + options=None, + serialized_options=None, + is_extendable=True, + extension_ranges=None, + oneofs=None, + file=None, # pylint: disable=redefined-builtin + serialized_start=None, + serialized_end=None, + syntax=None, + is_map_entry=False, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindMessageTypeByName(full_name) + + # NOTE: The file argument redefining a builtin is nothing we can + # fix right now since we don't know how many clients already rely on the + # name of the argument. + def __init__(self, name, full_name, filename, containing_type, fields, + nested_types, enum_types, extensions, options=None, + serialized_options=None, + is_extendable=True, extension_ranges=None, oneofs=None, + file=None, serialized_start=None, serialized_end=None, # pylint: disable=redefined-builtin + syntax=None, is_map_entry=False, create_key=None): + """Arguments to __init__() are as described in the description + of Descriptor fields above. + + Note that filename is an obsolete argument, that is not used anymore. + Please use file.name to access this as an attribute. + """ + if create_key is not _internal_create_key: + _Deprecated('Descriptor') + + super(Descriptor, self).__init__( + options, 'MessageOptions', name, full_name, file, + containing_type, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + + # We have fields in addition to fields_by_name and fields_by_number, + # so that: + # 1. Clients can index fields by "order in which they're listed." + # 2. Clients can easily iterate over all fields with the terse + # syntax: for f in descriptor.fields: ... + self.fields = fields + for field in self.fields: + field.containing_type = self + field.file = file + self.fields_by_number = dict((f.number, f) for f in fields) + self.fields_by_name = dict((f.name, f) for f in fields) + self._fields_by_camelcase_name = None + + self.nested_types = nested_types + for nested_type in nested_types: + nested_type.containing_type = self + self.nested_types_by_name = dict((t.name, t) for t in nested_types) + + self.enum_types = enum_types + for enum_type in self.enum_types: + enum_type.containing_type = self + self.enum_types_by_name = dict((t.name, t) for t in enum_types) + self.enum_values_by_name = dict( + (v.name, v) for t in enum_types for v in t.values) + + self.extensions = extensions + for extension in self.extensions: + extension.extension_scope = self + self.extensions_by_name = dict((f.name, f) for f in extensions) + self.is_extendable = is_extendable + self.extension_ranges = extension_ranges + self.oneofs = oneofs if oneofs is not None else [] + self.oneofs_by_name = dict((o.name, o) for o in self.oneofs) + for oneof in self.oneofs: + oneof.containing_type = self + oneof.file = file + self._is_map_entry = is_map_entry + + @property + def _parent(self): + return self.containing_type or self.file + + @property + def fields_by_camelcase_name(self): + """Same FieldDescriptor objects as in :attr:`fields`, but indexed by + :attr:`FieldDescriptor.camelcase_name`. + """ + if self._fields_by_camelcase_name is None: + self._fields_by_camelcase_name = dict( + (f.camelcase_name, f) for f in self.fields) + return self._fields_by_camelcase_name + + def EnumValueName(self, enum, value): + """Returns the string name of an enum value. + + This is just a small helper method to simplify a common operation. + + Args: + enum: string name of the Enum. + value: int, value of the enum. + + Returns: + string name of the enum value. + + Raises: + KeyError if either the Enum doesn't exist or the value is not a valid + value for the enum. + """ + return self.enum_types_by_name[enum].values_by_number[value].name + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.DescriptorProto. + + Args: + proto: An empty descriptor_pb2.DescriptorProto. + """ + # This function is overridden to give a better doc comment. + super(Descriptor, self).CopyToProto(proto) + + +# TODO: We should have aggressive checking here, +# for example: +# * If you specify a repeated field, you should not be allowed +# to specify a default value. +# * [Other examples here as needed]. +# +# TODO: for this and other *Descriptor classes, we +# might also want to lock things down aggressively (e.g., +# prevent clients from setting the attributes). Having +# stronger invariants here in general will reduce the number +# of runtime checks we must do in reflection.py... +class FieldDescriptor(DescriptorBase): + + """Descriptor for a single field in a .proto file. + + Attributes: + name (str): Name of this field, exactly as it appears in .proto. + full_name (str): Name of this field, including containing scope. This is + particularly relevant for extensions. + index (int): Dense, 0-indexed index giving the order that this + field textually appears within its message in the .proto file. + number (int): Tag number declared for this field in the .proto file. + + type (int): (One of the TYPE_* constants below) Declared type. + cpp_type (int): (One of the CPPTYPE_* constants below) C++ type used to + represent this field. + + label (int): (One of the LABEL_* constants below) Tells whether this + field is optional, required, or repeated. + has_default_value (bool): True if this field has a default value defined, + otherwise false. + default_value (Varies): Default value of this field. Only + meaningful for non-repeated scalar fields. Repeated fields + should always set this to [], and non-repeated composite + fields should always set this to None. + + containing_type (Descriptor): Descriptor of the protocol message + type that contains this field. Set by the Descriptor constructor + if we're passed into one. + Somewhat confusingly, for extension fields, this is the + descriptor of the EXTENDED message, not the descriptor + of the message containing this field. (See is_extension and + extension_scope below). + message_type (Descriptor): If a composite field, a descriptor + of the message type contained in this field. Otherwise, this is None. + enum_type (EnumDescriptor): If this field contains an enum, a + descriptor of that enum. Otherwise, this is None. + + is_extension: True iff this describes an extension field. + extension_scope (Descriptor): Only meaningful if is_extension is True. + Gives the message that immediately contains this extension field. + Will be None iff we're a top-level (file-level) extension field. + + options (descriptor_pb2.FieldOptions): Protocol message field options or + None to use default field options. + + containing_oneof (OneofDescriptor): If the field is a member of a oneof + union, contains its descriptor. Otherwise, None. + + file (FileDescriptor): Reference to file descriptor. + """ + + # Must be consistent with C++ FieldDescriptor::Type enum in + # descriptor.h. + # + # TODO: Find a way to eliminate this repetition. + TYPE_DOUBLE = 1 + TYPE_FLOAT = 2 + TYPE_INT64 = 3 + TYPE_UINT64 = 4 + TYPE_INT32 = 5 + TYPE_FIXED64 = 6 + TYPE_FIXED32 = 7 + TYPE_BOOL = 8 + TYPE_STRING = 9 + TYPE_GROUP = 10 + TYPE_MESSAGE = 11 + TYPE_BYTES = 12 + TYPE_UINT32 = 13 + TYPE_ENUM = 14 + TYPE_SFIXED32 = 15 + TYPE_SFIXED64 = 16 + TYPE_SINT32 = 17 + TYPE_SINT64 = 18 + MAX_TYPE = 18 + + # Must be consistent with C++ FieldDescriptor::CppType enum in + # descriptor.h. + # + # TODO: Find a way to eliminate this repetition. + CPPTYPE_INT32 = 1 + CPPTYPE_INT64 = 2 + CPPTYPE_UINT32 = 3 + CPPTYPE_UINT64 = 4 + CPPTYPE_DOUBLE = 5 + CPPTYPE_FLOAT = 6 + CPPTYPE_BOOL = 7 + CPPTYPE_ENUM = 8 + CPPTYPE_STRING = 9 + CPPTYPE_MESSAGE = 10 + MAX_CPPTYPE = 10 + + _PYTHON_TO_CPP_PROTO_TYPE_MAP = { + TYPE_DOUBLE: CPPTYPE_DOUBLE, + TYPE_FLOAT: CPPTYPE_FLOAT, + TYPE_ENUM: CPPTYPE_ENUM, + TYPE_INT64: CPPTYPE_INT64, + TYPE_SINT64: CPPTYPE_INT64, + TYPE_SFIXED64: CPPTYPE_INT64, + TYPE_UINT64: CPPTYPE_UINT64, + TYPE_FIXED64: CPPTYPE_UINT64, + TYPE_INT32: CPPTYPE_INT32, + TYPE_SFIXED32: CPPTYPE_INT32, + TYPE_SINT32: CPPTYPE_INT32, + TYPE_UINT32: CPPTYPE_UINT32, + TYPE_FIXED32: CPPTYPE_UINT32, + TYPE_BYTES: CPPTYPE_STRING, + TYPE_STRING: CPPTYPE_STRING, + TYPE_BOOL: CPPTYPE_BOOL, + TYPE_MESSAGE: CPPTYPE_MESSAGE, + TYPE_GROUP: CPPTYPE_MESSAGE + } + + # Must be consistent with C++ FieldDescriptor::Label enum in + # descriptor.h. + # + # TODO: Find a way to eliminate this repetition. + LABEL_OPTIONAL = 1 + LABEL_REQUIRED = 2 + LABEL_REPEATED = 3 + MAX_LABEL = 3 + + # Must be consistent with C++ constants kMaxNumber, kFirstReservedNumber, + # and kLastReservedNumber in descriptor.h + MAX_FIELD_NUMBER = (1 << 29) - 1 + FIRST_RESERVED_FIELD_NUMBER = 19000 + LAST_RESERVED_FIELD_NUMBER = 19999 + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.FieldDescriptor + + def __new__(cls, name, full_name, index, number, type, cpp_type, label, + default_value, message_type, enum_type, containing_type, + is_extension, extension_scope, options=None, + serialized_options=None, + has_default_value=True, containing_oneof=None, json_name=None, + file=None, create_key=None): # pylint: disable=redefined-builtin + _message.Message._CheckCalledFromGeneratedFile() + if is_extension: + return _message.default_pool.FindExtensionByName(full_name) + else: + return _message.default_pool.FindFieldByName(full_name) + + def __init__(self, name, full_name, index, number, type, cpp_type, label, + default_value, message_type, enum_type, containing_type, + is_extension, extension_scope, options=None, + serialized_options=None, + has_default_value=True, containing_oneof=None, json_name=None, + file=None, create_key=None): # pylint: disable=redefined-builtin + """The arguments are as described in the description of FieldDescriptor + attributes above. + + Note that containing_type may be None, and may be set later if necessary + (to deal with circular references between message types, for example). + Likewise for extension_scope. + """ + if create_key is not _internal_create_key: + _Deprecated('FieldDescriptor') + + super(FieldDescriptor, self).__init__( + file, options, serialized_options, 'FieldOptions' + ) + self.name = name + self.full_name = full_name + self._camelcase_name = None + if json_name is None: + self.json_name = _ToJsonName(name) + else: + self.json_name = json_name + self.index = index + self.number = number + self._type = type + self.cpp_type = cpp_type + self._label = label + self.has_default_value = has_default_value + self.default_value = default_value + self.containing_type = containing_type + self.message_type = message_type + self.enum_type = enum_type + self.is_extension = is_extension + self.extension_scope = extension_scope + self.containing_oneof = containing_oneof + if api_implementation.Type() == 'python': + self._cdescriptor = None + else: + if is_extension: + self._cdescriptor = _message.default_pool.FindExtensionByName(full_name) + else: + self._cdescriptor = _message.default_pool.FindFieldByName(full_name) + + @property + def _parent(self): + if self.containing_oneof: + return self.containing_oneof + if self.is_extension: + return self.extension_scope or self.file + return self.containing_type + + def _InferLegacyFeatures(self, edition, options, features): + # pylint: disable=g-import-not-at-top + from google.protobuf import descriptor_pb2 + + if edition >= descriptor_pb2.Edition.EDITION_2023: + return + + if self._label == FieldDescriptor.LABEL_REQUIRED: + features.field_presence = ( + descriptor_pb2.FeatureSet.FieldPresence.LEGACY_REQUIRED + ) + + if self._type == FieldDescriptor.TYPE_GROUP: + features.message_encoding = ( + descriptor_pb2.FeatureSet.MessageEncoding.DELIMITED + ) + + if options.HasField('packed'): + features.repeated_field_encoding = ( + descriptor_pb2.FeatureSet.RepeatedFieldEncoding.PACKED + if options.packed + else descriptor_pb2.FeatureSet.RepeatedFieldEncoding.EXPANDED + ) + + @property + def type(self): + if ( + self._GetFeatures().message_encoding + == _FEATURESET_MESSAGE_ENCODING_DELIMITED + and self.message_type + and not self.message_type.GetOptions().map_entry + and not self.containing_type.GetOptions().map_entry + ): + return FieldDescriptor.TYPE_GROUP + return self._type + + @type.setter + def type(self, val): + self._type = val + + @property + def label(self): + if ( + self._GetFeatures().field_presence + == _FEATURESET_FIELD_PRESENCE_LEGACY_REQUIRED + ): + return FieldDescriptor.LABEL_REQUIRED + return self._label + + @property + def camelcase_name(self): + """Camelcase name of this field. + + Returns: + str: the name in CamelCase. + """ + if self._camelcase_name is None: + self._camelcase_name = _ToCamelCase(self.name) + return self._camelcase_name + + @property + def has_presence(self): + """Whether the field distinguishes between unpopulated and default values. + + Raises: + RuntimeError: singular field that is not linked with message nor file. + """ + if self.label == FieldDescriptor.LABEL_REPEATED: + return False + if ( + self.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE + or self.is_extension + or self.containing_oneof + ): + return True + + return ( + self._GetFeatures().field_presence + != _FEATURESET_FIELD_PRESENCE_IMPLICIT + ) + + @property + def is_packed(self): + """Returns if the field is packed.""" + if self.label != FieldDescriptor.LABEL_REPEATED: + return False + field_type = self.type + if (field_type == FieldDescriptor.TYPE_STRING or + field_type == FieldDescriptor.TYPE_GROUP or + field_type == FieldDescriptor.TYPE_MESSAGE or + field_type == FieldDescriptor.TYPE_BYTES): + return False + + return ( + self._GetFeatures().repeated_field_encoding + == _FEATURESET_REPEATED_FIELD_ENCODING_PACKED + ) + + @staticmethod + def ProtoTypeToCppProtoType(proto_type): + """Converts from a Python proto type to a C++ Proto Type. + + The Python ProtocolBuffer classes specify both the 'Python' datatype and the + 'C++' datatype - and they're not the same. This helper method should + translate from one to another. + + Args: + proto_type: the Python proto type (descriptor.FieldDescriptor.TYPE_*) + Returns: + int: descriptor.FieldDescriptor.CPPTYPE_*, the C++ type. + Raises: + TypeTransformationError: when the Python proto type isn't known. + """ + try: + return FieldDescriptor._PYTHON_TO_CPP_PROTO_TYPE_MAP[proto_type] + except KeyError: + raise TypeTransformationError('Unknown proto_type: %s' % proto_type) + + +class EnumDescriptor(_NestedDescriptorBase): + + """Descriptor for an enum defined in a .proto file. + + Attributes: + name (str): Name of the enum type. + full_name (str): Full name of the type, including package name + and any enclosing type(s). + + values (list[EnumValueDescriptor]): List of the values + in this enum. + values_by_name (dict(str, EnumValueDescriptor)): Same as :attr:`values`, + but indexed by the "name" field of each EnumValueDescriptor. + values_by_number (dict(int, EnumValueDescriptor)): Same as :attr:`values`, + but indexed by the "number" field of each EnumValueDescriptor. + containing_type (Descriptor): Descriptor of the immediate containing + type of this enum, or None if this is an enum defined at the + top level in a .proto file. Set by Descriptor's constructor + if we're passed into one. + file (FileDescriptor): Reference to file descriptor. + options (descriptor_pb2.EnumOptions): Enum options message or + None to use default enum options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.EnumDescriptor + + def __new__(cls, name, full_name, filename, values, + containing_type=None, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindEnumTypeByName(full_name) + + def __init__(self, name, full_name, filename, values, + containing_type=None, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + """Arguments are as described in the attribute description above. + + Note that filename is an obsolete argument, that is not used anymore. + Please use file.name to access this as an attribute. + """ + if create_key is not _internal_create_key: + _Deprecated('EnumDescriptor') + + super(EnumDescriptor, self).__init__( + options, 'EnumOptions', name, full_name, file, + containing_type, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + + self.values = values + for value in self.values: + value.file = file + value.type = self + self.values_by_name = dict((v.name, v) for v in values) + # Values are reversed to ensure that the first alias is retained. + self.values_by_number = dict((v.number, v) for v in reversed(values)) + + @property + def _parent(self): + return self.containing_type or self.file + + @property + def is_closed(self): + """Returns true whether this is a "closed" enum. + + This means that it: + - Has a fixed set of values, rather than being equivalent to an int32. + - Encountering values not in this set causes them to be treated as unknown + fields. + - The first value (i.e., the default) may be nonzero. + + WARNING: Some runtimes currently have a quirk where non-closed enums are + treated as closed when used as the type of fields defined in a + `syntax = proto2;` file. This quirk is not present in all runtimes; as of + writing, we know that: + + - C++, Java, and C++-based Python share this quirk. + - UPB and UPB-based Python do not. + - PHP and Ruby treat all enums as open regardless of declaration. + + Care should be taken when using this function to respect the target + runtime's enum handling quirks. + """ + return self._GetFeatures().enum_type == _FEATURESET_ENUM_TYPE_CLOSED + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.EnumDescriptorProto. + + Args: + proto (descriptor_pb2.EnumDescriptorProto): An empty descriptor proto. + """ + # This function is overridden to give a better doc comment. + super(EnumDescriptor, self).CopyToProto(proto) + + +class EnumValueDescriptor(DescriptorBase): + + """Descriptor for a single value within an enum. + + Attributes: + name (str): Name of this value. + index (int): Dense, 0-indexed index giving the order that this + value appears textually within its enum in the .proto file. + number (int): Actual number assigned to this enum value. + type (EnumDescriptor): :class:`EnumDescriptor` to which this value + belongs. Set by :class:`EnumDescriptor`'s constructor if we're + passed into one. + options (descriptor_pb2.EnumValueOptions): Enum value options message or + None to use default enum value options options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.EnumValueDescriptor + + def __new__(cls, name, index, number, + type=None, # pylint: disable=redefined-builtin + options=None, serialized_options=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + # There is no way we can build a complete EnumValueDescriptor with the + # given parameters (the name of the Enum is not known, for example). + # Fortunately generated files just pass it to the EnumDescriptor() + # constructor, which will ignore it, so returning None is good enough. + return None + + def __init__(self, name, index, number, + type=None, # pylint: disable=redefined-builtin + options=None, serialized_options=None, create_key=None): + """Arguments are as described in the attribute description above.""" + if create_key is not _internal_create_key: + _Deprecated('EnumValueDescriptor') + + super(EnumValueDescriptor, self).__init__( + type.file if type else None, + options, + serialized_options, + 'EnumValueOptions', + ) + self.name = name + self.index = index + self.number = number + self.type = type + + @property + def _parent(self): + return self.type + + +class OneofDescriptor(DescriptorBase): + """Descriptor for a oneof field. + + Attributes: + name (str): Name of the oneof field. + full_name (str): Full name of the oneof field, including package name. + index (int): 0-based index giving the order of the oneof field inside + its containing type. + containing_type (Descriptor): :class:`Descriptor` of the protocol message + type that contains this field. Set by the :class:`Descriptor` constructor + if we're passed into one. + fields (list[FieldDescriptor]): The list of field descriptors this + oneof can contain. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.OneofDescriptor + + def __new__( + cls, name, full_name, index, containing_type, fields, options=None, + serialized_options=None, create_key=None): + _message.Message._CheckCalledFromGeneratedFile() + return _message.default_pool.FindOneofByName(full_name) + + def __init__( + self, name, full_name, index, containing_type, fields, options=None, + serialized_options=None, create_key=None): + """Arguments are as described in the attribute description above.""" + if create_key is not _internal_create_key: + _Deprecated('OneofDescriptor') + + super(OneofDescriptor, self).__init__( + containing_type.file if containing_type else None, + options, + serialized_options, + 'OneofOptions', + ) + self.name = name + self.full_name = full_name + self.index = index + self.containing_type = containing_type + self.fields = fields + + @property + def _parent(self): + return self.containing_type + + +class ServiceDescriptor(_NestedDescriptorBase): + + """Descriptor for a service. + + Attributes: + name (str): Name of the service. + full_name (str): Full name of the service, including package name. + index (int): 0-indexed index giving the order that this services + definition appears within the .proto file. + methods (list[MethodDescriptor]): List of methods provided by this + service. + methods_by_name (dict(str, MethodDescriptor)): Same + :class:`MethodDescriptor` objects as in :attr:`methods_by_name`, but + indexed by "name" attribute in each :class:`MethodDescriptor`. + options (descriptor_pb2.ServiceOptions): Service options message or + None to use default service options. + file (FileDescriptor): Reference to file info. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.ServiceDescriptor + + def __new__( + cls, + name=None, + full_name=None, + index=None, + methods=None, + options=None, + serialized_options=None, + file=None, # pylint: disable=redefined-builtin + serialized_start=None, + serialized_end=None, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access + return _message.default_pool.FindServiceByName(full_name) + + def __init__(self, name, full_name, index, methods, options=None, + serialized_options=None, file=None, # pylint: disable=redefined-builtin + serialized_start=None, serialized_end=None, create_key=None): + if create_key is not _internal_create_key: + _Deprecated('ServiceDescriptor') + + super(ServiceDescriptor, self).__init__( + options, 'ServiceOptions', name, full_name, file, + None, serialized_start=serialized_start, + serialized_end=serialized_end, serialized_options=serialized_options) + self.index = index + self.methods = methods + self.methods_by_name = dict((m.name, m) for m in methods) + # Set the containing service for each method in this service. + for method in self.methods: + method.file = self.file + method.containing_service = self + + @property + def _parent(self): + return self.file + + def FindMethodByName(self, name): + """Searches for the specified method, and returns its descriptor. + + Args: + name (str): Name of the method. + + Returns: + MethodDescriptor: The descriptor for the requested method. + + Raises: + KeyError: if the method cannot be found in the service. + """ + return self.methods_by_name[name] + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.ServiceDescriptorProto. + + Args: + proto (descriptor_pb2.ServiceDescriptorProto): An empty descriptor proto. + """ + # This function is overridden to give a better doc comment. + super(ServiceDescriptor, self).CopyToProto(proto) + + +class MethodDescriptor(DescriptorBase): + + """Descriptor for a method in a service. + + Attributes: + name (str): Name of the method within the service. + full_name (str): Full name of method. + index (int): 0-indexed index of the method inside the service. + containing_service (ServiceDescriptor): The service that contains this + method. + input_type (Descriptor): The descriptor of the message that this method + accepts. + output_type (Descriptor): The descriptor of the message that this method + returns. + client_streaming (bool): Whether this method uses client streaming. + server_streaming (bool): Whether this method uses server streaming. + options (descriptor_pb2.MethodOptions or None): Method options message, or + None to use default method options. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.MethodDescriptor + + def __new__(cls, + name, + full_name, + index, + containing_service, + input_type, + output_type, + client_streaming=False, + server_streaming=False, + options=None, + serialized_options=None, + create_key=None): + _message.Message._CheckCalledFromGeneratedFile() # pylint: disable=protected-access + return _message.default_pool.FindMethodByName(full_name) + + def __init__(self, + name, + full_name, + index, + containing_service, + input_type, + output_type, + client_streaming=False, + server_streaming=False, + options=None, + serialized_options=None, + create_key=None): + """The arguments are as described in the description of MethodDescriptor + attributes above. + + Note that containing_service may be None, and may be set later if necessary. + """ + if create_key is not _internal_create_key: + _Deprecated('MethodDescriptor') + + super(MethodDescriptor, self).__init__( + containing_service.file if containing_service else None, + options, + serialized_options, + 'MethodOptions', + ) + self.name = name + self.full_name = full_name + self.index = index + self.containing_service = containing_service + self.input_type = input_type + self.output_type = output_type + self.client_streaming = client_streaming + self.server_streaming = server_streaming + + @property + def _parent(self): + return self.containing_service + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.MethodDescriptorProto. + + Args: + proto (descriptor_pb2.MethodDescriptorProto): An empty descriptor proto. + + Raises: + Error: If self couldn't be serialized, due to too few constructor + arguments. + """ + if self.containing_service is not None: + from google.protobuf import descriptor_pb2 + service_proto = descriptor_pb2.ServiceDescriptorProto() + self.containing_service.CopyToProto(service_proto) + proto.CopyFrom(service_proto.method[self.index]) + else: + raise Error('Descriptor does not contain a service.') + + +class FileDescriptor(DescriptorBase): + """Descriptor for a file. Mimics the descriptor_pb2.FileDescriptorProto. + + Note that :attr:`enum_types_by_name`, :attr:`extensions_by_name`, and + :attr:`dependencies` fields are only set by the + :py:mod:`google.protobuf.message_factory` module, and not by the generated + proto code. + + Attributes: + name (str): Name of file, relative to root of source tree. + package (str): Name of the package + edition (Edition): Enum value indicating edition of the file + serialized_pb (bytes): Byte string of serialized + :class:`descriptor_pb2.FileDescriptorProto`. + dependencies (list[FileDescriptor]): List of other :class:`FileDescriptor` + objects this :class:`FileDescriptor` depends on. + public_dependencies (list[FileDescriptor]): A subset of + :attr:`dependencies`, which were declared as "public". + message_types_by_name (dict(str, Descriptor)): Mapping from message names to + their :class:`Descriptor`. + enum_types_by_name (dict(str, EnumDescriptor)): Mapping from enum names to + their :class:`EnumDescriptor`. + extensions_by_name (dict(str, FieldDescriptor)): Mapping from extension + names declared at file scope to their :class:`FieldDescriptor`. + services_by_name (dict(str, ServiceDescriptor)): Mapping from services' + names to their :class:`ServiceDescriptor`. + pool (DescriptorPool): The pool this descriptor belongs to. When not passed + to the constructor, the global default pool is used. + """ + + if _USE_C_DESCRIPTORS: + _C_DESCRIPTOR_CLASS = _message.FileDescriptor + + def __new__( + cls, + name, + package, + options=None, + serialized_options=None, + serialized_pb=None, + dependencies=None, + public_dependencies=None, + syntax=None, + edition=None, + pool=None, + create_key=None, + ): + # FileDescriptor() is called from various places, not only from generated + # files, to register dynamic proto files and messages. + # pylint: disable=g-explicit-bool-comparison + if serialized_pb: + return _message.default_pool.AddSerializedFile(serialized_pb) + else: + return super(FileDescriptor, cls).__new__(cls) + + def __init__( + self, + name, + package, + options=None, + serialized_options=None, + serialized_pb=None, + dependencies=None, + public_dependencies=None, + syntax=None, + edition=None, + pool=None, + create_key=None, + ): + """Constructor.""" + if create_key is not _internal_create_key: + _Deprecated('FileDescriptor') + + super(FileDescriptor, self).__init__( + self, options, serialized_options, 'FileOptions' + ) + + if edition and edition != 'EDITION_UNKNOWN': + self._edition = edition + elif syntax == 'proto3': + self._edition = 'EDITION_PROTO3' + else: + self._edition = 'EDITION_PROTO2' + + if pool is None: + from google.protobuf import descriptor_pool + pool = descriptor_pool.Default() + self.pool = pool + self.message_types_by_name = {} + self.name = name + self.package = package + self.serialized_pb = serialized_pb + + self.enum_types_by_name = {} + self.extensions_by_name = {} + self.services_by_name = {} + self.dependencies = (dependencies or []) + self.public_dependencies = (public_dependencies or []) + + def CopyToProto(self, proto): + """Copies this to a descriptor_pb2.FileDescriptorProto. + + Args: + proto: An empty descriptor_pb2.FileDescriptorProto. + """ + proto.ParseFromString(self.serialized_pb) + + @property + def _parent(self): + return None + + +def _ParseOptions(message, string): + """Parses serialized options. + + This helper function is used to parse serialized options in generated + proto2 files. It must not be used outside proto2. + """ + message.ParseFromString(string) + return message + + +def _ToCamelCase(name): + """Converts name to camel-case and returns it.""" + capitalize_next = False + result = [] + + for c in name: + if c == '_': + if result: + capitalize_next = True + elif capitalize_next: + result.append(c.upper()) + capitalize_next = False + else: + result += c + + # Lower-case the first letter. + if result and result[0].isupper(): + result[0] = result[0].lower() + return ''.join(result) + + +def _OptionsOrNone(descriptor_proto): + """Returns the value of the field `options`, or None if it is not set.""" + if descriptor_proto.HasField('options'): + return descriptor_proto.options + else: + return None + + +def _ToJsonName(name): + """Converts name to Json name and returns it.""" + capitalize_next = False + result = [] + + for c in name: + if c == '_': + capitalize_next = True + elif capitalize_next: + result.append(c.upper()) + capitalize_next = False + else: + result += c + + return ''.join(result) + + +def MakeDescriptor( + desc_proto, + package='', + build_file_if_cpp=True, + syntax=None, + edition=None, + file_desc=None, +): + """Make a protobuf Descriptor given a DescriptorProto protobuf. + + Handles nested descriptors. Note that this is limited to the scope of defining + a message inside of another message. Composite fields can currently only be + resolved if the message is defined in the same scope as the field. + + Args: + desc_proto: The descriptor_pb2.DescriptorProto protobuf message. + package: Optional package name for the new message Descriptor (string). + build_file_if_cpp: Update the C++ descriptor pool if api matches. Set to + False on recursion, so no duplicates are created. + syntax: The syntax/semantics that should be used. Set to "proto3" to get + proto3 field presence semantics. + edition: The edition that should be used if syntax is "edition". + file_desc: A FileDescriptor to place this descriptor into. + + Returns: + A Descriptor for protobuf messages. + """ + # pylint: disable=g-import-not-at-top + from google.protobuf import descriptor_pb2 + + # Generate a random name for this proto file to prevent conflicts with any + # imported ones. We need to specify a file name so the descriptor pool + # accepts our FileDescriptorProto, but it is not important what that file + # name is actually set to. + proto_name = binascii.hexlify(os.urandom(16)).decode('ascii') + + if package: + file_name = os.path.join(package.replace('.', '/'), proto_name + '.proto') + else: + file_name = proto_name + '.proto' + + if api_implementation.Type() != 'python' and build_file_if_cpp: + # The C++ implementation requires all descriptors to be backed by the same + # definition in the C++ descriptor pool. To do this, we build a + # FileDescriptorProto with the same definition as this descriptor and build + # it into the pool. + file_descriptor_proto = descriptor_pb2.FileDescriptorProto() + file_descriptor_proto.message_type.add().MergeFrom(desc_proto) + + if package: + file_descriptor_proto.package = package + file_descriptor_proto.name = file_name + + _message.default_pool.Add(file_descriptor_proto) + result = _message.default_pool.FindFileByName(file_descriptor_proto.name) + + if _USE_C_DESCRIPTORS: + return result.message_types_by_name[desc_proto.name] + + if file_desc is None: + file_desc = FileDescriptor( + pool=None, + name=file_name, + package=package, + syntax=syntax, + edition=edition, + options=None, + serialized_pb='', + dependencies=[], + public_dependencies=[], + create_key=_internal_create_key, + ) + full_message_name = [desc_proto.name] + if package: full_message_name.insert(0, package) + + # Create Descriptors for enum types + enum_types = {} + for enum_proto in desc_proto.enum_type: + full_name = '.'.join(full_message_name + [enum_proto.name]) + enum_desc = EnumDescriptor( + enum_proto.name, + full_name, + None, + [ + EnumValueDescriptor( + enum_val.name, + ii, + enum_val.number, + create_key=_internal_create_key, + ) + for ii, enum_val in enumerate(enum_proto.value) + ], + file=file_desc, + create_key=_internal_create_key, + ) + enum_types[full_name] = enum_desc + + # Create Descriptors for nested types + nested_types = {} + for nested_proto in desc_proto.nested_type: + full_name = '.'.join(full_message_name + [nested_proto.name]) + # Nested types are just those defined inside of the message, not all types + # used by fields in the message, so no loops are possible here. + nested_desc = MakeDescriptor( + nested_proto, + package='.'.join(full_message_name), + build_file_if_cpp=False, + syntax=syntax, + edition=edition, + file_desc=file_desc, + ) + nested_types[full_name] = nested_desc + + fields = [] + for field_proto in desc_proto.field: + full_name = '.'.join(full_message_name + [field_proto.name]) + enum_desc = None + nested_desc = None + if field_proto.json_name: + json_name = field_proto.json_name + else: + json_name = None + if field_proto.HasField('type_name'): + type_name = field_proto.type_name + full_type_name = '.'.join(full_message_name + + [type_name[type_name.rfind('.')+1:]]) + if full_type_name in nested_types: + nested_desc = nested_types[full_type_name] + elif full_type_name in enum_types: + enum_desc = enum_types[full_type_name] + # Else type_name references a non-local type, which isn't implemented + field = FieldDescriptor( + field_proto.name, + full_name, + field_proto.number - 1, + field_proto.number, + field_proto.type, + FieldDescriptor.ProtoTypeToCppProtoType(field_proto.type), + field_proto.label, + None, + nested_desc, + enum_desc, + None, + False, + None, + options=_OptionsOrNone(field_proto), + has_default_value=False, + json_name=json_name, + file=file_desc, + create_key=_internal_create_key, + ) + fields.append(field) + + desc_name = '.'.join(full_message_name) + return Descriptor( + desc_proto.name, + desc_name, + None, + None, + fields, + list(nested_types.values()), + list(enum_types.values()), + [], + options=_OptionsOrNone(desc_proto), + file=file_desc, + create_key=_internal_create_key, + ) diff --git a/google/protobuf/descriptor.upb.c b/google/protobuf/descriptor.upb.c new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/google/protobuf/descriptor.upb.c @@ -0,0 +1 @@ + diff --git a/google/protobuf/descriptor.upb.h b/google/protobuf/descriptor.upb.h new file mode 100644 index 0000000..d2a8801 --- /dev/null +++ b/google/protobuf/descriptor.upb.h @@ -0,0 +1,6699 @@ +/* This file was generated by upb_generator from the input file: + * + * google/protobuf/descriptor.proto + * + * Do not edit -- your changes will be discarded when the file is + * regenerated. */ + +#ifndef GOOGLE_PROTOBUF_DESCRIPTOR_PROTO_UPB_H_ +#define GOOGLE_PROTOBUF_DESCRIPTOR_PROTO_UPB_H_ + +#include "upb/generated_code_support.h" + +#include "google/protobuf/descriptor.upb_minitable.h" + +// Must be last. +#include "upb/port/def.inc" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct google_protobuf_FileDescriptorSet { upb_Message UPB_PRIVATE(base); } google_protobuf_FileDescriptorSet; +typedef struct google_protobuf_FileDescriptorProto { upb_Message UPB_PRIVATE(base); } google_protobuf_FileDescriptorProto; +typedef struct google_protobuf_DescriptorProto { upb_Message UPB_PRIVATE(base); } google_protobuf_DescriptorProto; +typedef struct google_protobuf_DescriptorProto_ExtensionRange { upb_Message UPB_PRIVATE(base); } google_protobuf_DescriptorProto_ExtensionRange; +typedef struct google_protobuf_DescriptorProto_ReservedRange { upb_Message UPB_PRIVATE(base); } google_protobuf_DescriptorProto_ReservedRange; +typedef struct google_protobuf_ExtensionRangeOptions { upb_Message UPB_PRIVATE(base); } google_protobuf_ExtensionRangeOptions; +typedef struct google_protobuf_ExtensionRangeOptions_Declaration { upb_Message UPB_PRIVATE(base); } google_protobuf_ExtensionRangeOptions_Declaration; +typedef struct google_protobuf_FieldDescriptorProto { upb_Message UPB_PRIVATE(base); } google_protobuf_FieldDescriptorProto; +typedef struct google_protobuf_OneofDescriptorProto { upb_Message UPB_PRIVATE(base); } google_protobuf_OneofDescriptorProto; +typedef struct google_protobuf_EnumDescriptorProto { upb_Message UPB_PRIVATE(base); } google_protobuf_EnumDescriptorProto; +typedef struct google_protobuf_EnumDescriptorProto_EnumReservedRange { upb_Message UPB_PRIVATE(base); } google_protobuf_EnumDescriptorProto_EnumReservedRange; +typedef struct google_protobuf_EnumValueDescriptorProto { upb_Message UPB_PRIVATE(base); } google_protobuf_EnumValueDescriptorProto; +typedef struct google_protobuf_ServiceDescriptorProto { upb_Message UPB_PRIVATE(base); } google_protobuf_ServiceDescriptorProto; +typedef struct google_protobuf_MethodDescriptorProto { upb_Message UPB_PRIVATE(base); } google_protobuf_MethodDescriptorProto; +typedef struct google_protobuf_FileOptions { upb_Message UPB_PRIVATE(base); } google_protobuf_FileOptions; +typedef struct google_protobuf_MessageOptions { upb_Message UPB_PRIVATE(base); } google_protobuf_MessageOptions; +typedef struct google_protobuf_FieldOptions { upb_Message UPB_PRIVATE(base); } google_protobuf_FieldOptions; +typedef struct google_protobuf_FieldOptions_EditionDefault { upb_Message UPB_PRIVATE(base); } google_protobuf_FieldOptions_EditionDefault; +typedef struct google_protobuf_FieldOptions_FeatureSupport { upb_Message UPB_PRIVATE(base); } google_protobuf_FieldOptions_FeatureSupport; +typedef struct google_protobuf_OneofOptions { upb_Message UPB_PRIVATE(base); } google_protobuf_OneofOptions; +typedef struct google_protobuf_EnumOptions { upb_Message UPB_PRIVATE(base); } google_protobuf_EnumOptions; +typedef struct google_protobuf_EnumValueOptions { upb_Message UPB_PRIVATE(base); } google_protobuf_EnumValueOptions; +typedef struct google_protobuf_ServiceOptions { upb_Message UPB_PRIVATE(base); } google_protobuf_ServiceOptions; +typedef struct google_protobuf_MethodOptions { upb_Message UPB_PRIVATE(base); } google_protobuf_MethodOptions; +typedef struct google_protobuf_UninterpretedOption { upb_Message UPB_PRIVATE(base); } google_protobuf_UninterpretedOption; +typedef struct google_protobuf_UninterpretedOption_NamePart { upb_Message UPB_PRIVATE(base); } google_protobuf_UninterpretedOption_NamePart; +typedef struct google_protobuf_FeatureSet { upb_Message UPB_PRIVATE(base); } google_protobuf_FeatureSet; +typedef struct google_protobuf_FeatureSetDefaults { upb_Message UPB_PRIVATE(base); } google_protobuf_FeatureSetDefaults; +typedef struct google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault { upb_Message UPB_PRIVATE(base); } google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault; +typedef struct google_protobuf_SourceCodeInfo { upb_Message UPB_PRIVATE(base); } google_protobuf_SourceCodeInfo; +typedef struct google_protobuf_SourceCodeInfo_Location { upb_Message UPB_PRIVATE(base); } google_protobuf_SourceCodeInfo_Location; +typedef struct google_protobuf_GeneratedCodeInfo { upb_Message UPB_PRIVATE(base); } google_protobuf_GeneratedCodeInfo; +typedef struct google_protobuf_GeneratedCodeInfo_Annotation { upb_Message UPB_PRIVATE(base); } google_protobuf_GeneratedCodeInfo_Annotation; + +typedef enum { + google_protobuf_EDITION_UNKNOWN = 0, + google_protobuf_EDITION_1_TEST_ONLY = 1, + google_protobuf_EDITION_2_TEST_ONLY = 2, + google_protobuf_EDITION_LEGACY = 900, + google_protobuf_EDITION_PROTO2 = 998, + google_protobuf_EDITION_PROTO3 = 999, + google_protobuf_EDITION_2023 = 1000, + google_protobuf_EDITION_2024 = 1001, + google_protobuf_EDITION_99997_TEST_ONLY = 99997, + google_protobuf_EDITION_99998_TEST_ONLY = 99998, + google_protobuf_EDITION_99999_TEST_ONLY = 99999, + google_protobuf_EDITION_MAX = 2147483647 +} google_protobuf_Edition; + +typedef enum { + google_protobuf_ExtensionRangeOptions_DECLARATION = 0, + google_protobuf_ExtensionRangeOptions_UNVERIFIED = 1 +} google_protobuf_ExtensionRangeOptions_VerificationState; + +typedef enum { + google_protobuf_FeatureSet_ENUM_TYPE_UNKNOWN = 0, + google_protobuf_FeatureSet_OPEN = 1, + google_protobuf_FeatureSet_CLOSED = 2 +} google_protobuf_FeatureSet_EnumType; + +typedef enum { + google_protobuf_FeatureSet_FIELD_PRESENCE_UNKNOWN = 0, + google_protobuf_FeatureSet_EXPLICIT = 1, + google_protobuf_FeatureSet_IMPLICIT = 2, + google_protobuf_FeatureSet_LEGACY_REQUIRED = 3 +} google_protobuf_FeatureSet_FieldPresence; + +typedef enum { + google_protobuf_FeatureSet_JSON_FORMAT_UNKNOWN = 0, + google_protobuf_FeatureSet_ALLOW = 1, + google_protobuf_FeatureSet_LEGACY_BEST_EFFORT = 2 +} google_protobuf_FeatureSet_JsonFormat; + +typedef enum { + google_protobuf_FeatureSet_MESSAGE_ENCODING_UNKNOWN = 0, + google_protobuf_FeatureSet_LENGTH_PREFIXED = 1, + google_protobuf_FeatureSet_DELIMITED = 2 +} google_protobuf_FeatureSet_MessageEncoding; + +typedef enum { + google_protobuf_FeatureSet_REPEATED_FIELD_ENCODING_UNKNOWN = 0, + google_protobuf_FeatureSet_PACKED = 1, + google_protobuf_FeatureSet_EXPANDED = 2 +} google_protobuf_FeatureSet_RepeatedFieldEncoding; + +typedef enum { + google_protobuf_FeatureSet_UTF8_VALIDATION_UNKNOWN = 0, + google_protobuf_FeatureSet_VERIFY = 2, + google_protobuf_FeatureSet_NONE = 3 +} google_protobuf_FeatureSet_Utf8Validation; + +typedef enum { + google_protobuf_FieldDescriptorProto_LABEL_OPTIONAL = 1, + google_protobuf_FieldDescriptorProto_LABEL_REQUIRED = 2, + google_protobuf_FieldDescriptorProto_LABEL_REPEATED = 3 +} google_protobuf_FieldDescriptorProto_Label; + +typedef enum { + google_protobuf_FieldDescriptorProto_TYPE_DOUBLE = 1, + google_protobuf_FieldDescriptorProto_TYPE_FLOAT = 2, + google_protobuf_FieldDescriptorProto_TYPE_INT64 = 3, + google_protobuf_FieldDescriptorProto_TYPE_UINT64 = 4, + google_protobuf_FieldDescriptorProto_TYPE_INT32 = 5, + google_protobuf_FieldDescriptorProto_TYPE_FIXED64 = 6, + google_protobuf_FieldDescriptorProto_TYPE_FIXED32 = 7, + google_protobuf_FieldDescriptorProto_TYPE_BOOL = 8, + google_protobuf_FieldDescriptorProto_TYPE_STRING = 9, + google_protobuf_FieldDescriptorProto_TYPE_GROUP = 10, + google_protobuf_FieldDescriptorProto_TYPE_MESSAGE = 11, + google_protobuf_FieldDescriptorProto_TYPE_BYTES = 12, + google_protobuf_FieldDescriptorProto_TYPE_UINT32 = 13, + google_protobuf_FieldDescriptorProto_TYPE_ENUM = 14, + google_protobuf_FieldDescriptorProto_TYPE_SFIXED32 = 15, + google_protobuf_FieldDescriptorProto_TYPE_SFIXED64 = 16, + google_protobuf_FieldDescriptorProto_TYPE_SINT32 = 17, + google_protobuf_FieldDescriptorProto_TYPE_SINT64 = 18 +} google_protobuf_FieldDescriptorProto_Type; + +typedef enum { + google_protobuf_FieldOptions_STRING = 0, + google_protobuf_FieldOptions_CORD = 1, + google_protobuf_FieldOptions_STRING_PIECE = 2 +} google_protobuf_FieldOptions_CType; + +typedef enum { + google_protobuf_FieldOptions_JS_NORMAL = 0, + google_protobuf_FieldOptions_JS_STRING = 1, + google_protobuf_FieldOptions_JS_NUMBER = 2 +} google_protobuf_FieldOptions_JSType; + +typedef enum { + google_protobuf_FieldOptions_RETENTION_UNKNOWN = 0, + google_protobuf_FieldOptions_RETENTION_RUNTIME = 1, + google_protobuf_FieldOptions_RETENTION_SOURCE = 2 +} google_protobuf_FieldOptions_OptionRetention; + +typedef enum { + google_protobuf_FieldOptions_TARGET_TYPE_UNKNOWN = 0, + google_protobuf_FieldOptions_TARGET_TYPE_FILE = 1, + google_protobuf_FieldOptions_TARGET_TYPE_EXTENSION_RANGE = 2, + google_protobuf_FieldOptions_TARGET_TYPE_MESSAGE = 3, + google_protobuf_FieldOptions_TARGET_TYPE_FIELD = 4, + google_protobuf_FieldOptions_TARGET_TYPE_ONEOF = 5, + google_protobuf_FieldOptions_TARGET_TYPE_ENUM = 6, + google_protobuf_FieldOptions_TARGET_TYPE_ENUM_ENTRY = 7, + google_protobuf_FieldOptions_TARGET_TYPE_SERVICE = 8, + google_protobuf_FieldOptions_TARGET_TYPE_METHOD = 9 +} google_protobuf_FieldOptions_OptionTargetType; + +typedef enum { + google_protobuf_FileOptions_SPEED = 1, + google_protobuf_FileOptions_CODE_SIZE = 2, + google_protobuf_FileOptions_LITE_RUNTIME = 3 +} google_protobuf_FileOptions_OptimizeMode; + +typedef enum { + google_protobuf_GeneratedCodeInfo_Annotation_NONE = 0, + google_protobuf_GeneratedCodeInfo_Annotation_SET = 1, + google_protobuf_GeneratedCodeInfo_Annotation_ALIAS = 2 +} google_protobuf_GeneratedCodeInfo_Annotation_Semantic; + +typedef enum { + google_protobuf_MethodOptions_IDEMPOTENCY_UNKNOWN = 0, + google_protobuf_MethodOptions_NO_SIDE_EFFECTS = 1, + google_protobuf_MethodOptions_IDEMPOTENT = 2 +} google_protobuf_MethodOptions_IdempotencyLevel; + + + +/* google.protobuf.FileDescriptorSet */ + +UPB_INLINE google_protobuf_FileDescriptorSet* google_protobuf_FileDescriptorSet_new(upb_Arena* arena) { + return (google_protobuf_FileDescriptorSet*)_upb_Message_New(&google__protobuf__FileDescriptorSet_msg_init, arena); +} +UPB_INLINE google_protobuf_FileDescriptorSet* google_protobuf_FileDescriptorSet_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_FileDescriptorSet* ret = google_protobuf_FileDescriptorSet_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FileDescriptorSet_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_FileDescriptorSet* google_protobuf_FileDescriptorSet_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_FileDescriptorSet* ret = google_protobuf_FileDescriptorSet_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FileDescriptorSet_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_FileDescriptorSet_serialize(const google_protobuf_FileDescriptorSet* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FileDescriptorSet_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_FileDescriptorSet_serialize_ex(const google_protobuf_FileDescriptorSet* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FileDescriptorSet_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_FileDescriptorSet_clear_file(google_protobuf_FileDescriptorSet* msg) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FileDescriptorProto* const* google_protobuf_FileDescriptorSet_file(const google_protobuf_FileDescriptorSet* msg, size_t* size) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_FileDescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FileDescriptorSet_file_upb_array(const google_protobuf_FileDescriptorSet* msg, size_t* size) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FileDescriptorSet_file_mutable_upb_array(google_protobuf_FileDescriptorSet* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE google_protobuf_FileDescriptorProto** google_protobuf_FileDescriptorSet_mutable_file(google_protobuf_FileDescriptorSet* msg, size_t* size) { + upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_FileDescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_FileDescriptorProto** google_protobuf_FileDescriptorSet_resize_file(google_protobuf_FileDescriptorSet* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_FileDescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_FileDescriptorProto* google_protobuf_FileDescriptorSet_add_file(google_protobuf_FileDescriptorSet* msg, upb_Arena* arena) { + upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_FileDescriptorProto* sub = (struct google_protobuf_FileDescriptorProto*)_upb_Message_New(&google__protobuf__FileDescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.FileDescriptorProto */ + +UPB_INLINE google_protobuf_FileDescriptorProto* google_protobuf_FileDescriptorProto_new(upb_Arena* arena) { + return (google_protobuf_FileDescriptorProto*)_upb_Message_New(&google__protobuf__FileDescriptorProto_msg_init, arena); +} +UPB_INLINE google_protobuf_FileDescriptorProto* google_protobuf_FileDescriptorProto_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_FileDescriptorProto* ret = google_protobuf_FileDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FileDescriptorProto_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_FileDescriptorProto* google_protobuf_FileDescriptorProto_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_FileDescriptorProto* ret = google_protobuf_FileDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FileDescriptorProto_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_FileDescriptorProto_serialize(const google_protobuf_FileDescriptorProto* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FileDescriptorProto_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_FileDescriptorProto_serialize_ex(const google_protobuf_FileDescriptorProto* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FileDescriptorProto_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_name(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(52, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileDescriptorProto_name(const google_protobuf_FileDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {1, UPB_SIZE(52, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileDescriptorProto_has_name(const google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(52, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_package(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(60, 32), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileDescriptorProto_package(const google_protobuf_FileDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {2, UPB_SIZE(60, 32), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileDescriptorProto_has_package(const google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(60, 32), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_dependency(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(12, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView const* google_protobuf_FileDescriptorProto_dependency(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {3, UPB_SIZE(12, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (upb_StringView const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FileDescriptorProto_dependency_upb_array(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {3, UPB_SIZE(12, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FileDescriptorProto_dependency_mutable_upb_array(google_protobuf_FileDescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {3, UPB_SIZE(12, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_message_type(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(16, 56), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_DescriptorProto* const* google_protobuf_FileDescriptorProto_message_type(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {4, UPB_SIZE(16, 56), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_DescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FileDescriptorProto_message_type_upb_array(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {4, UPB_SIZE(16, 56), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FileDescriptorProto_message_type_mutable_upb_array(google_protobuf_FileDescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {4, UPB_SIZE(16, 56), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_enum_type(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(20, 64), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_EnumDescriptorProto* const* google_protobuf_FileDescriptorProto_enum_type(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {5, UPB_SIZE(20, 64), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_EnumDescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FileDescriptorProto_enum_type_upb_array(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {5, UPB_SIZE(20, 64), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FileDescriptorProto_enum_type_mutable_upb_array(google_protobuf_FileDescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {5, UPB_SIZE(20, 64), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_service(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {6, UPB_SIZE(24, 72), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_ServiceDescriptorProto* const* google_protobuf_FileDescriptorProto_service(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {6, UPB_SIZE(24, 72), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_ServiceDescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FileDescriptorProto_service_upb_array(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {6, UPB_SIZE(24, 72), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FileDescriptorProto_service_mutable_upb_array(google_protobuf_FileDescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {6, UPB_SIZE(24, 72), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_extension(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {7, UPB_SIZE(28, 80), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FieldDescriptorProto* const* google_protobuf_FileDescriptorProto_extension(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {7, UPB_SIZE(28, 80), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_FieldDescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FileDescriptorProto_extension_upb_array(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {7, UPB_SIZE(28, 80), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FileDescriptorProto_extension_mutable_upb_array(google_protobuf_FileDescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {7, UPB_SIZE(28, 80), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_options(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {8, UPB_SIZE(32, 88), 66, 4, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FileOptions* google_protobuf_FileDescriptorProto_options(const google_protobuf_FileDescriptorProto* msg) { + const google_protobuf_FileOptions* default_val = NULL; + const google_protobuf_FileOptions* ret; + const upb_MiniTableField field = {8, UPB_SIZE(32, 88), 66, 4, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileDescriptorProto_has_options(const google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {8, UPB_SIZE(32, 88), 66, 4, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_source_code_info(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {9, UPB_SIZE(36, 96), 67, 5, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_SourceCodeInfo* google_protobuf_FileDescriptorProto_source_code_info(const google_protobuf_FileDescriptorProto* msg) { + const google_protobuf_SourceCodeInfo* default_val = NULL; + const google_protobuf_SourceCodeInfo* ret; + const upb_MiniTableField field = {9, UPB_SIZE(36, 96), 67, 5, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileDescriptorProto_has_source_code_info(const google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {9, UPB_SIZE(36, 96), 67, 5, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_public_dependency(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {10, UPB_SIZE(40, 104), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t const* google_protobuf_FileDescriptorProto_public_dependency(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {10, UPB_SIZE(40, 104), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FileDescriptorProto_public_dependency_upb_array(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {10, UPB_SIZE(40, 104), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FileDescriptorProto_public_dependency_mutable_upb_array(google_protobuf_FileDescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {10, UPB_SIZE(40, 104), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_weak_dependency(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {11, UPB_SIZE(44, 112), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t const* google_protobuf_FileDescriptorProto_weak_dependency(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {11, UPB_SIZE(44, 112), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FileDescriptorProto_weak_dependency_upb_array(const google_protobuf_FileDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {11, UPB_SIZE(44, 112), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FileDescriptorProto_weak_dependency_mutable_upb_array(google_protobuf_FileDescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {11, UPB_SIZE(44, 112), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_syntax(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {12, UPB_SIZE(68, 120), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileDescriptorProto_syntax(const google_protobuf_FileDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {12, UPB_SIZE(68, 120), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileDescriptorProto_has_syntax(const google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {12, UPB_SIZE(68, 120), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileDescriptorProto_clear_edition(google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {14, UPB_SIZE(48, 12), 69, 6, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FileDescriptorProto_edition(const google_protobuf_FileDescriptorProto* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {14, UPB_SIZE(48, 12), 69, 6, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileDescriptorProto_has_edition(const google_protobuf_FileDescriptorProto* msg) { + const upb_MiniTableField field = {14, UPB_SIZE(48, 12), 69, 6, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_FileDescriptorProto_set_name(google_protobuf_FileDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {1, UPB_SIZE(52, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileDescriptorProto_set_package(google_protobuf_FileDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {2, UPB_SIZE(60, 32), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE upb_StringView* google_protobuf_FileDescriptorProto_mutable_dependency(google_protobuf_FileDescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {3, UPB_SIZE(12, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (upb_StringView*)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE upb_StringView* google_protobuf_FileDescriptorProto_resize_dependency(google_protobuf_FileDescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {3, UPB_SIZE(12, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (upb_StringView*)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE bool google_protobuf_FileDescriptorProto_add_dependency(google_protobuf_FileDescriptorProto* msg, upb_StringView val, upb_Arena* arena) { + upb_MiniTableField field = {3, UPB_SIZE(12, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return false; + } + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &val, sizeof(val)); + return true; +} +UPB_INLINE google_protobuf_DescriptorProto** google_protobuf_FileDescriptorProto_mutable_message_type(google_protobuf_FileDescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {4, UPB_SIZE(16, 56), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_DescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_DescriptorProto** google_protobuf_FileDescriptorProto_resize_message_type(google_protobuf_FileDescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {4, UPB_SIZE(16, 56), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_DescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_DescriptorProto* google_protobuf_FileDescriptorProto_add_message_type(google_protobuf_FileDescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {4, UPB_SIZE(16, 56), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_DescriptorProto* sub = (struct google_protobuf_DescriptorProto*)_upb_Message_New(&google__protobuf__DescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE google_protobuf_EnumDescriptorProto** google_protobuf_FileDescriptorProto_mutable_enum_type(google_protobuf_FileDescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {5, UPB_SIZE(20, 64), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_EnumDescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_EnumDescriptorProto** google_protobuf_FileDescriptorProto_resize_enum_type(google_protobuf_FileDescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {5, UPB_SIZE(20, 64), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_EnumDescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_EnumDescriptorProto* google_protobuf_FileDescriptorProto_add_enum_type(google_protobuf_FileDescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {5, UPB_SIZE(20, 64), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_EnumDescriptorProto* sub = (struct google_protobuf_EnumDescriptorProto*)_upb_Message_New(&google__protobuf__EnumDescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE google_protobuf_ServiceDescriptorProto** google_protobuf_FileDescriptorProto_mutable_service(google_protobuf_FileDescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {6, UPB_SIZE(24, 72), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_ServiceDescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_ServiceDescriptorProto** google_protobuf_FileDescriptorProto_resize_service(google_protobuf_FileDescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {6, UPB_SIZE(24, 72), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_ServiceDescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_ServiceDescriptorProto* google_protobuf_FileDescriptorProto_add_service(google_protobuf_FileDescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {6, UPB_SIZE(24, 72), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_ServiceDescriptorProto* sub = (struct google_protobuf_ServiceDescriptorProto*)_upb_Message_New(&google__protobuf__ServiceDescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE google_protobuf_FieldDescriptorProto** google_protobuf_FileDescriptorProto_mutable_extension(google_protobuf_FileDescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {7, UPB_SIZE(28, 80), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_FieldDescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_FieldDescriptorProto** google_protobuf_FileDescriptorProto_resize_extension(google_protobuf_FileDescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {7, UPB_SIZE(28, 80), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_FieldDescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_FieldDescriptorProto* google_protobuf_FileDescriptorProto_add_extension(google_protobuf_FileDescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {7, UPB_SIZE(28, 80), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_FieldDescriptorProto* sub = (struct google_protobuf_FieldDescriptorProto*)_upb_Message_New(&google__protobuf__FieldDescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE void google_protobuf_FileDescriptorProto_set_options(google_protobuf_FileDescriptorProto *msg, google_protobuf_FileOptions* value) { + const upb_MiniTableField field = {8, UPB_SIZE(32, 88), 66, 4, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FileOptions* google_protobuf_FileDescriptorProto_mutable_options(google_protobuf_FileDescriptorProto* msg, upb_Arena* arena) { + struct google_protobuf_FileOptions* sub = (struct google_protobuf_FileOptions*)google_protobuf_FileDescriptorProto_options(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FileOptions*)_upb_Message_New(&google__protobuf__FileOptions_msg_init, arena); + if (sub) google_protobuf_FileDescriptorProto_set_options(msg, sub); + } + return sub; +} +UPB_INLINE void google_protobuf_FileDescriptorProto_set_source_code_info(google_protobuf_FileDescriptorProto *msg, google_protobuf_SourceCodeInfo* value) { + const upb_MiniTableField field = {9, UPB_SIZE(36, 96), 67, 5, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_SourceCodeInfo* google_protobuf_FileDescriptorProto_mutable_source_code_info(google_protobuf_FileDescriptorProto* msg, upb_Arena* arena) { + struct google_protobuf_SourceCodeInfo* sub = (struct google_protobuf_SourceCodeInfo*)google_protobuf_FileDescriptorProto_source_code_info(msg); + if (sub == NULL) { + sub = (struct google_protobuf_SourceCodeInfo*)_upb_Message_New(&google__protobuf__SourceCodeInfo_msg_init, arena); + if (sub) google_protobuf_FileDescriptorProto_set_source_code_info(msg, sub); + } + return sub; +} +UPB_INLINE int32_t* google_protobuf_FileDescriptorProto_mutable_public_dependency(google_protobuf_FileDescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {10, UPB_SIZE(40, 104), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t*)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE int32_t* google_protobuf_FileDescriptorProto_resize_public_dependency(google_protobuf_FileDescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {10, UPB_SIZE(40, 104), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (int32_t*)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE bool google_protobuf_FileDescriptorProto_add_public_dependency(google_protobuf_FileDescriptorProto* msg, int32_t val, upb_Arena* arena) { + upb_MiniTableField field = {10, UPB_SIZE(40, 104), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return false; + } + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &val, sizeof(val)); + return true; +} +UPB_INLINE int32_t* google_protobuf_FileDescriptorProto_mutable_weak_dependency(google_protobuf_FileDescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {11, UPB_SIZE(44, 112), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t*)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE int32_t* google_protobuf_FileDescriptorProto_resize_weak_dependency(google_protobuf_FileDescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {11, UPB_SIZE(44, 112), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (int32_t*)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE bool google_protobuf_FileDescriptorProto_add_weak_dependency(google_protobuf_FileDescriptorProto* msg, int32_t val, upb_Arena* arena) { + upb_MiniTableField field = {11, UPB_SIZE(44, 112), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return false; + } + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &val, sizeof(val)); + return true; +} +UPB_INLINE void google_protobuf_FileDescriptorProto_set_syntax(google_protobuf_FileDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {12, UPB_SIZE(68, 120), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileDescriptorProto_set_edition(google_protobuf_FileDescriptorProto *msg, int32_t value) { + const upb_MiniTableField field = {14, UPB_SIZE(48, 12), 69, 6, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.DescriptorProto */ + +UPB_INLINE google_protobuf_DescriptorProto* google_protobuf_DescriptorProto_new(upb_Arena* arena) { + return (google_protobuf_DescriptorProto*)_upb_Message_New(&google__protobuf__DescriptorProto_msg_init, arena); +} +UPB_INLINE google_protobuf_DescriptorProto* google_protobuf_DescriptorProto_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_DescriptorProto* ret = google_protobuf_DescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__DescriptorProto_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_DescriptorProto* google_protobuf_DescriptorProto_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_DescriptorProto* ret = google_protobuf_DescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__DescriptorProto_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_DescriptorProto_serialize(const google_protobuf_DescriptorProto* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__DescriptorProto_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_DescriptorProto_serialize_ex(const google_protobuf_DescriptorProto* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__DescriptorProto_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_DescriptorProto_clear_name(google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(48, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_DescriptorProto_name(const google_protobuf_DescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {1, UPB_SIZE(48, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_DescriptorProto_has_name(const google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(48, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_DescriptorProto_clear_field(google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FieldDescriptorProto* const* google_protobuf_DescriptorProto_field(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_FieldDescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_DescriptorProto_field_upb_array(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_DescriptorProto_field_mutable_upb_array(google_protobuf_DescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_DescriptorProto_clear_nested_type(google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_DescriptorProto* const* google_protobuf_DescriptorProto_nested_type(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_DescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_DescriptorProto_nested_type_upb_array(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_DescriptorProto_nested_type_mutable_upb_array(google_protobuf_DescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_DescriptorProto_clear_enum_type(google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_EnumDescriptorProto* const* google_protobuf_DescriptorProto_enum_type(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_EnumDescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_DescriptorProto_enum_type_upb_array(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_DescriptorProto_enum_type_mutable_upb_array(google_protobuf_DescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_DescriptorProto_clear_extension_range(google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_DescriptorProto_ExtensionRange* const* google_protobuf_DescriptorProto_extension_range(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_DescriptorProto_ExtensionRange* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_DescriptorProto_extension_range_upb_array(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_DescriptorProto_extension_range_mutable_upb_array(google_protobuf_DescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_DescriptorProto_clear_extension(google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {6, UPB_SIZE(28, 64), 0, 4, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FieldDescriptorProto* const* google_protobuf_DescriptorProto_extension(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {6, UPB_SIZE(28, 64), 0, 4, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_FieldDescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_DescriptorProto_extension_upb_array(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {6, UPB_SIZE(28, 64), 0, 4, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_DescriptorProto_extension_mutable_upb_array(google_protobuf_DescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {6, UPB_SIZE(28, 64), 0, 4, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_DescriptorProto_clear_options(google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {7, UPB_SIZE(32, 72), 65, 5, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_MessageOptions* google_protobuf_DescriptorProto_options(const google_protobuf_DescriptorProto* msg) { + const google_protobuf_MessageOptions* default_val = NULL; + const google_protobuf_MessageOptions* ret; + const upb_MiniTableField field = {7, UPB_SIZE(32, 72), 65, 5, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_DescriptorProto_has_options(const google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {7, UPB_SIZE(32, 72), 65, 5, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_DescriptorProto_clear_oneof_decl(google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {8, UPB_SIZE(36, 80), 0, 6, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_OneofDescriptorProto* const* google_protobuf_DescriptorProto_oneof_decl(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {8, UPB_SIZE(36, 80), 0, 6, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_OneofDescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_DescriptorProto_oneof_decl_upb_array(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {8, UPB_SIZE(36, 80), 0, 6, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_DescriptorProto_oneof_decl_mutable_upb_array(google_protobuf_DescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {8, UPB_SIZE(36, 80), 0, 6, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_DescriptorProto_clear_reserved_range(google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {9, UPB_SIZE(40, 88), 0, 7, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_DescriptorProto_ReservedRange* const* google_protobuf_DescriptorProto_reserved_range(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {9, UPB_SIZE(40, 88), 0, 7, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_DescriptorProto_ReservedRange* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_DescriptorProto_reserved_range_upb_array(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {9, UPB_SIZE(40, 88), 0, 7, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_DescriptorProto_reserved_range_mutable_upb_array(google_protobuf_DescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {9, UPB_SIZE(40, 88), 0, 7, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_DescriptorProto_clear_reserved_name(google_protobuf_DescriptorProto* msg) { + const upb_MiniTableField field = {10, UPB_SIZE(44, 96), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView const* google_protobuf_DescriptorProto_reserved_name(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {10, UPB_SIZE(44, 96), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (upb_StringView const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_DescriptorProto_reserved_name_upb_array(const google_protobuf_DescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {10, UPB_SIZE(44, 96), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_DescriptorProto_reserved_name_mutable_upb_array(google_protobuf_DescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {10, UPB_SIZE(44, 96), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE void google_protobuf_DescriptorProto_set_name(google_protobuf_DescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {1, UPB_SIZE(48, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE google_protobuf_FieldDescriptorProto** google_protobuf_DescriptorProto_mutable_field(google_protobuf_DescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_FieldDescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_FieldDescriptorProto** google_protobuf_DescriptorProto_resize_field(google_protobuf_DescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_FieldDescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_FieldDescriptorProto* google_protobuf_DescriptorProto_add_field(google_protobuf_DescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_FieldDescriptorProto* sub = (struct google_protobuf_FieldDescriptorProto*)_upb_Message_New(&google__protobuf__FieldDescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE google_protobuf_DescriptorProto** google_protobuf_DescriptorProto_mutable_nested_type(google_protobuf_DescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {3, UPB_SIZE(16, 40), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_DescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_DescriptorProto** google_protobuf_DescriptorProto_resize_nested_type(google_protobuf_DescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {3, UPB_SIZE(16, 40), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_DescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_DescriptorProto* google_protobuf_DescriptorProto_add_nested_type(google_protobuf_DescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {3, UPB_SIZE(16, 40), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_DescriptorProto* sub = (struct google_protobuf_DescriptorProto*)_upb_Message_New(&google__protobuf__DescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE google_protobuf_EnumDescriptorProto** google_protobuf_DescriptorProto_mutable_enum_type(google_protobuf_DescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_EnumDescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_EnumDescriptorProto** google_protobuf_DescriptorProto_resize_enum_type(google_protobuf_DescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_EnumDescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_EnumDescriptorProto* google_protobuf_DescriptorProto_add_enum_type(google_protobuf_DescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_EnumDescriptorProto* sub = (struct google_protobuf_EnumDescriptorProto*)_upb_Message_New(&google__protobuf__EnumDescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE google_protobuf_DescriptorProto_ExtensionRange** google_protobuf_DescriptorProto_mutable_extension_range(google_protobuf_DescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_DescriptorProto_ExtensionRange**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_DescriptorProto_ExtensionRange** google_protobuf_DescriptorProto_resize_extension_range(google_protobuf_DescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_DescriptorProto_ExtensionRange**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_DescriptorProto_ExtensionRange* google_protobuf_DescriptorProto_add_extension_range(google_protobuf_DescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_DescriptorProto_ExtensionRange* sub = (struct google_protobuf_DescriptorProto_ExtensionRange*)_upb_Message_New(&google__protobuf__DescriptorProto__ExtensionRange_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE google_protobuf_FieldDescriptorProto** google_protobuf_DescriptorProto_mutable_extension(google_protobuf_DescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {6, UPB_SIZE(28, 64), 0, 4, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_FieldDescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_FieldDescriptorProto** google_protobuf_DescriptorProto_resize_extension(google_protobuf_DescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {6, UPB_SIZE(28, 64), 0, 4, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_FieldDescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_FieldDescriptorProto* google_protobuf_DescriptorProto_add_extension(google_protobuf_DescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {6, UPB_SIZE(28, 64), 0, 4, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_FieldDescriptorProto* sub = (struct google_protobuf_FieldDescriptorProto*)_upb_Message_New(&google__protobuf__FieldDescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE void google_protobuf_DescriptorProto_set_options(google_protobuf_DescriptorProto *msg, google_protobuf_MessageOptions* value) { + const upb_MiniTableField field = {7, UPB_SIZE(32, 72), 65, 5, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_MessageOptions* google_protobuf_DescriptorProto_mutable_options(google_protobuf_DescriptorProto* msg, upb_Arena* arena) { + struct google_protobuf_MessageOptions* sub = (struct google_protobuf_MessageOptions*)google_protobuf_DescriptorProto_options(msg); + if (sub == NULL) { + sub = (struct google_protobuf_MessageOptions*)_upb_Message_New(&google__protobuf__MessageOptions_msg_init, arena); + if (sub) google_protobuf_DescriptorProto_set_options(msg, sub); + } + return sub; +} +UPB_INLINE google_protobuf_OneofDescriptorProto** google_protobuf_DescriptorProto_mutable_oneof_decl(google_protobuf_DescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {8, UPB_SIZE(36, 80), 0, 6, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_OneofDescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_OneofDescriptorProto** google_protobuf_DescriptorProto_resize_oneof_decl(google_protobuf_DescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {8, UPB_SIZE(36, 80), 0, 6, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_OneofDescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_OneofDescriptorProto* google_protobuf_DescriptorProto_add_oneof_decl(google_protobuf_DescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {8, UPB_SIZE(36, 80), 0, 6, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_OneofDescriptorProto* sub = (struct google_protobuf_OneofDescriptorProto*)_upb_Message_New(&google__protobuf__OneofDescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE google_protobuf_DescriptorProto_ReservedRange** google_protobuf_DescriptorProto_mutable_reserved_range(google_protobuf_DescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {9, UPB_SIZE(40, 88), 0, 7, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_DescriptorProto_ReservedRange**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_DescriptorProto_ReservedRange** google_protobuf_DescriptorProto_resize_reserved_range(google_protobuf_DescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {9, UPB_SIZE(40, 88), 0, 7, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_DescriptorProto_ReservedRange**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_DescriptorProto_ReservedRange* google_protobuf_DescriptorProto_add_reserved_range(google_protobuf_DescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {9, UPB_SIZE(40, 88), 0, 7, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_DescriptorProto_ReservedRange* sub = (struct google_protobuf_DescriptorProto_ReservedRange*)_upb_Message_New(&google__protobuf__DescriptorProto__ReservedRange_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE upb_StringView* google_protobuf_DescriptorProto_mutable_reserved_name(google_protobuf_DescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {10, UPB_SIZE(44, 96), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (upb_StringView*)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE upb_StringView* google_protobuf_DescriptorProto_resize_reserved_name(google_protobuf_DescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {10, UPB_SIZE(44, 96), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (upb_StringView*)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE bool google_protobuf_DescriptorProto_add_reserved_name(google_protobuf_DescriptorProto* msg, upb_StringView val, upb_Arena* arena) { + upb_MiniTableField field = {10, UPB_SIZE(44, 96), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return false; + } + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &val, sizeof(val)); + return true; +} + +/* google.protobuf.DescriptorProto.ExtensionRange */ + +UPB_INLINE google_protobuf_DescriptorProto_ExtensionRange* google_protobuf_DescriptorProto_ExtensionRange_new(upb_Arena* arena) { + return (google_protobuf_DescriptorProto_ExtensionRange*)_upb_Message_New(&google__protobuf__DescriptorProto__ExtensionRange_msg_init, arena); +} +UPB_INLINE google_protobuf_DescriptorProto_ExtensionRange* google_protobuf_DescriptorProto_ExtensionRange_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_DescriptorProto_ExtensionRange* ret = google_protobuf_DescriptorProto_ExtensionRange_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__DescriptorProto__ExtensionRange_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_DescriptorProto_ExtensionRange* google_protobuf_DescriptorProto_ExtensionRange_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_DescriptorProto_ExtensionRange* ret = google_protobuf_DescriptorProto_ExtensionRange_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__DescriptorProto__ExtensionRange_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_DescriptorProto_ExtensionRange_serialize(const google_protobuf_DescriptorProto_ExtensionRange* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__DescriptorProto__ExtensionRange_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_DescriptorProto_ExtensionRange_serialize_ex(const google_protobuf_DescriptorProto_ExtensionRange* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__DescriptorProto__ExtensionRange_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_DescriptorProto_ExtensionRange_clear_start(google_protobuf_DescriptorProto_ExtensionRange* msg) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_DescriptorProto_ExtensionRange_start(const google_protobuf_DescriptorProto_ExtensionRange* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_DescriptorProto_ExtensionRange_has_start(const google_protobuf_DescriptorProto_ExtensionRange* msg) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_DescriptorProto_ExtensionRange_clear_end(google_protobuf_DescriptorProto_ExtensionRange* msg) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_DescriptorProto_ExtensionRange_end(const google_protobuf_DescriptorProto_ExtensionRange* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_DescriptorProto_ExtensionRange_has_end(const google_protobuf_DescriptorProto_ExtensionRange* msg) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_DescriptorProto_ExtensionRange_clear_options(google_protobuf_DescriptorProto_ExtensionRange* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(20, 24), 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_ExtensionRangeOptions* google_protobuf_DescriptorProto_ExtensionRange_options(const google_protobuf_DescriptorProto_ExtensionRange* msg) { + const google_protobuf_ExtensionRangeOptions* default_val = NULL; + const google_protobuf_ExtensionRangeOptions* ret; + const upb_MiniTableField field = {3, UPB_SIZE(20, 24), 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_DescriptorProto_ExtensionRange_has_options(const google_protobuf_DescriptorProto_ExtensionRange* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(20, 24), 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_DescriptorProto_ExtensionRange_set_start(google_protobuf_DescriptorProto_ExtensionRange *msg, int32_t value) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_DescriptorProto_ExtensionRange_set_end(google_protobuf_DescriptorProto_ExtensionRange *msg, int32_t value) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_DescriptorProto_ExtensionRange_set_options(google_protobuf_DescriptorProto_ExtensionRange *msg, google_protobuf_ExtensionRangeOptions* value) { + const upb_MiniTableField field = {3, UPB_SIZE(20, 24), 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_ExtensionRangeOptions* google_protobuf_DescriptorProto_ExtensionRange_mutable_options(google_protobuf_DescriptorProto_ExtensionRange* msg, upb_Arena* arena) { + struct google_protobuf_ExtensionRangeOptions* sub = (struct google_protobuf_ExtensionRangeOptions*)google_protobuf_DescriptorProto_ExtensionRange_options(msg); + if (sub == NULL) { + sub = (struct google_protobuf_ExtensionRangeOptions*)_upb_Message_New(&google__protobuf__ExtensionRangeOptions_msg_init, arena); + if (sub) google_protobuf_DescriptorProto_ExtensionRange_set_options(msg, sub); + } + return sub; +} + +/* google.protobuf.DescriptorProto.ReservedRange */ + +UPB_INLINE google_protobuf_DescriptorProto_ReservedRange* google_protobuf_DescriptorProto_ReservedRange_new(upb_Arena* arena) { + return (google_protobuf_DescriptorProto_ReservedRange*)_upb_Message_New(&google__protobuf__DescriptorProto__ReservedRange_msg_init, arena); +} +UPB_INLINE google_protobuf_DescriptorProto_ReservedRange* google_protobuf_DescriptorProto_ReservedRange_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_DescriptorProto_ReservedRange* ret = google_protobuf_DescriptorProto_ReservedRange_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__DescriptorProto__ReservedRange_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_DescriptorProto_ReservedRange* google_protobuf_DescriptorProto_ReservedRange_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_DescriptorProto_ReservedRange* ret = google_protobuf_DescriptorProto_ReservedRange_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__DescriptorProto__ReservedRange_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_DescriptorProto_ReservedRange_serialize(const google_protobuf_DescriptorProto_ReservedRange* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__DescriptorProto__ReservedRange_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_DescriptorProto_ReservedRange_serialize_ex(const google_protobuf_DescriptorProto_ReservedRange* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__DescriptorProto__ReservedRange_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_DescriptorProto_ReservedRange_clear_start(google_protobuf_DescriptorProto_ReservedRange* msg) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_DescriptorProto_ReservedRange_start(const google_protobuf_DescriptorProto_ReservedRange* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_DescriptorProto_ReservedRange_has_start(const google_protobuf_DescriptorProto_ReservedRange* msg) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_DescriptorProto_ReservedRange_clear_end(google_protobuf_DescriptorProto_ReservedRange* msg) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_DescriptorProto_ReservedRange_end(const google_protobuf_DescriptorProto_ReservedRange* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_DescriptorProto_ReservedRange_has_end(const google_protobuf_DescriptorProto_ReservedRange* msg) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_DescriptorProto_ReservedRange_set_start(google_protobuf_DescriptorProto_ReservedRange *msg, int32_t value) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_DescriptorProto_ReservedRange_set_end(google_protobuf_DescriptorProto_ReservedRange *msg, int32_t value) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.ExtensionRangeOptions */ + +UPB_INLINE google_protobuf_ExtensionRangeOptions* google_protobuf_ExtensionRangeOptions_new(upb_Arena* arena) { + return (google_protobuf_ExtensionRangeOptions*)_upb_Message_New(&google__protobuf__ExtensionRangeOptions_msg_init, arena); +} +UPB_INLINE google_protobuf_ExtensionRangeOptions* google_protobuf_ExtensionRangeOptions_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_ExtensionRangeOptions* ret = google_protobuf_ExtensionRangeOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__ExtensionRangeOptions_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_ExtensionRangeOptions* google_protobuf_ExtensionRangeOptions_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_ExtensionRangeOptions* ret = google_protobuf_ExtensionRangeOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__ExtensionRangeOptions_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_ExtensionRangeOptions_serialize(const google_protobuf_ExtensionRangeOptions* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__ExtensionRangeOptions_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_ExtensionRangeOptions_serialize_ex(const google_protobuf_ExtensionRangeOptions* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__ExtensionRangeOptions_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_clear_declaration(google_protobuf_ExtensionRangeOptions* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_ExtensionRangeOptions_Declaration* const* google_protobuf_ExtensionRangeOptions_declaration(const google_protobuf_ExtensionRangeOptions* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_ExtensionRangeOptions_Declaration* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_ExtensionRangeOptions_declaration_upb_array(const google_protobuf_ExtensionRangeOptions* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_ExtensionRangeOptions_declaration_mutable_upb_array(google_protobuf_ExtensionRangeOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_clear_verification(google_protobuf_ExtensionRangeOptions* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 12), 64, 3, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_ExtensionRangeOptions_verification(const google_protobuf_ExtensionRangeOptions* msg) { + int32_t default_val = 1; + int32_t ret; + const upb_MiniTableField field = {3, UPB_SIZE(16, 12), 64, 3, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_ExtensionRangeOptions_has_verification(const google_protobuf_ExtensionRangeOptions* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 12), 64, 3, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_clear_features(google_protobuf_ExtensionRangeOptions* msg) { + const upb_MiniTableField field = {50, UPB_SIZE(20, 24), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSet* google_protobuf_ExtensionRangeOptions_features(const google_protobuf_ExtensionRangeOptions* msg) { + const google_protobuf_FeatureSet* default_val = NULL; + const google_protobuf_FeatureSet* ret; + const upb_MiniTableField field = {50, UPB_SIZE(20, 24), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_ExtensionRangeOptions_has_features(const google_protobuf_ExtensionRangeOptions* msg) { + const upb_MiniTableField field = {50, UPB_SIZE(20, 24), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_clear_uninterpreted_option(google_protobuf_ExtensionRangeOptions* msg) { + const upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_UninterpretedOption* const* google_protobuf_ExtensionRangeOptions_uninterpreted_option(const google_protobuf_ExtensionRangeOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_UninterpretedOption* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_ExtensionRangeOptions_uninterpreted_option_upb_array(const google_protobuf_ExtensionRangeOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_ExtensionRangeOptions_uninterpreted_option_mutable_upb_array(google_protobuf_ExtensionRangeOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE google_protobuf_ExtensionRangeOptions_Declaration** google_protobuf_ExtensionRangeOptions_mutable_declaration(google_protobuf_ExtensionRangeOptions* msg, size_t* size) { + upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_ExtensionRangeOptions_Declaration**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_ExtensionRangeOptions_Declaration** google_protobuf_ExtensionRangeOptions_resize_declaration(google_protobuf_ExtensionRangeOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_ExtensionRangeOptions_Declaration**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_ExtensionRangeOptions_Declaration* google_protobuf_ExtensionRangeOptions_add_declaration(google_protobuf_ExtensionRangeOptions* msg, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_ExtensionRangeOptions_Declaration* sub = (struct google_protobuf_ExtensionRangeOptions_Declaration*)_upb_Message_New(&google__protobuf__ExtensionRangeOptions__Declaration_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_set_verification(google_protobuf_ExtensionRangeOptions *msg, int32_t value) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 12), 64, 3, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_set_features(google_protobuf_ExtensionRangeOptions *msg, google_protobuf_FeatureSet* value) { + const upb_MiniTableField field = {50, UPB_SIZE(20, 24), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FeatureSet* google_protobuf_ExtensionRangeOptions_mutable_features(google_protobuf_ExtensionRangeOptions* msg, upb_Arena* arena) { + struct google_protobuf_FeatureSet* sub = (struct google_protobuf_FeatureSet*)google_protobuf_ExtensionRangeOptions_features(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); + if (sub) google_protobuf_ExtensionRangeOptions_set_features(msg, sub); + } + return sub; +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_ExtensionRangeOptions_mutable_uninterpreted_option(google_protobuf_ExtensionRangeOptions* msg, size_t* size) { + upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_UninterpretedOption**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_ExtensionRangeOptions_resize_uninterpreted_option(google_protobuf_ExtensionRangeOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_UninterpretedOption**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_UninterpretedOption* google_protobuf_ExtensionRangeOptions_add_uninterpreted_option(google_protobuf_ExtensionRangeOptions* msg, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_UninterpretedOption* sub = (struct google_protobuf_UninterpretedOption*)_upb_Message_New(&google__protobuf__UninterpretedOption_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.ExtensionRangeOptions.Declaration */ + +UPB_INLINE google_protobuf_ExtensionRangeOptions_Declaration* google_protobuf_ExtensionRangeOptions_Declaration_new(upb_Arena* arena) { + return (google_protobuf_ExtensionRangeOptions_Declaration*)_upb_Message_New(&google__protobuf__ExtensionRangeOptions__Declaration_msg_init, arena); +} +UPB_INLINE google_protobuf_ExtensionRangeOptions_Declaration* google_protobuf_ExtensionRangeOptions_Declaration_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_ExtensionRangeOptions_Declaration* ret = google_protobuf_ExtensionRangeOptions_Declaration_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__ExtensionRangeOptions__Declaration_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_ExtensionRangeOptions_Declaration* google_protobuf_ExtensionRangeOptions_Declaration_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_ExtensionRangeOptions_Declaration* ret = google_protobuf_ExtensionRangeOptions_Declaration_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__ExtensionRangeOptions__Declaration_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_ExtensionRangeOptions_Declaration_serialize(const google_protobuf_ExtensionRangeOptions_Declaration* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__ExtensionRangeOptions__Declaration_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_ExtensionRangeOptions_Declaration_serialize_ex(const google_protobuf_ExtensionRangeOptions_Declaration* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__ExtensionRangeOptions__Declaration_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_Declaration_clear_number(google_protobuf_ExtensionRangeOptions_Declaration* msg) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_ExtensionRangeOptions_Declaration_number(const google_protobuf_ExtensionRangeOptions_Declaration* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_ExtensionRangeOptions_Declaration_has_number(const google_protobuf_ExtensionRangeOptions_Declaration* msg) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_Declaration_clear_full_name(google_protobuf_ExtensionRangeOptions_Declaration* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(20, 24), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_ExtensionRangeOptions_Declaration_full_name(const google_protobuf_ExtensionRangeOptions_Declaration* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {2, UPB_SIZE(20, 24), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_ExtensionRangeOptions_Declaration_has_full_name(const google_protobuf_ExtensionRangeOptions_Declaration* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(20, 24), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_Declaration_clear_type(google_protobuf_ExtensionRangeOptions_Declaration* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(28, 40), 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_ExtensionRangeOptions_Declaration_type(const google_protobuf_ExtensionRangeOptions_Declaration* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {3, UPB_SIZE(28, 40), 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_ExtensionRangeOptions_Declaration_has_type(const google_protobuf_ExtensionRangeOptions_Declaration* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(28, 40), 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_Declaration_clear_reserved(google_protobuf_ExtensionRangeOptions_Declaration* msg) { + const upb_MiniTableField field = {5, 16, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_ExtensionRangeOptions_Declaration_reserved(const google_protobuf_ExtensionRangeOptions_Declaration* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {5, 16, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_ExtensionRangeOptions_Declaration_has_reserved(const google_protobuf_ExtensionRangeOptions_Declaration* msg) { + const upb_MiniTableField field = {5, 16, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_Declaration_clear_repeated(google_protobuf_ExtensionRangeOptions_Declaration* msg) { + const upb_MiniTableField field = {6, 17, 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_ExtensionRangeOptions_Declaration_repeated(const google_protobuf_ExtensionRangeOptions_Declaration* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {6, 17, 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_ExtensionRangeOptions_Declaration_has_repeated(const google_protobuf_ExtensionRangeOptions_Declaration* msg) { + const upb_MiniTableField field = {6, 17, 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_ExtensionRangeOptions_Declaration_set_number(google_protobuf_ExtensionRangeOptions_Declaration *msg, int32_t value) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_Declaration_set_full_name(google_protobuf_ExtensionRangeOptions_Declaration *msg, upb_StringView value) { + const upb_MiniTableField field = {2, UPB_SIZE(20, 24), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_Declaration_set_type(google_protobuf_ExtensionRangeOptions_Declaration *msg, upb_StringView value) { + const upb_MiniTableField field = {3, UPB_SIZE(28, 40), 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_Declaration_set_reserved(google_protobuf_ExtensionRangeOptions_Declaration *msg, bool value) { + const upb_MiniTableField field = {5, 16, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_ExtensionRangeOptions_Declaration_set_repeated(google_protobuf_ExtensionRangeOptions_Declaration *msg, bool value) { + const upb_MiniTableField field = {6, 17, 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.FieldDescriptorProto */ + +UPB_INLINE google_protobuf_FieldDescriptorProto* google_protobuf_FieldDescriptorProto_new(upb_Arena* arena) { + return (google_protobuf_FieldDescriptorProto*)_upb_Message_New(&google__protobuf__FieldDescriptorProto_msg_init, arena); +} +UPB_INLINE google_protobuf_FieldDescriptorProto* google_protobuf_FieldDescriptorProto_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_FieldDescriptorProto* ret = google_protobuf_FieldDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FieldDescriptorProto_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_FieldDescriptorProto* google_protobuf_FieldDescriptorProto_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_FieldDescriptorProto* ret = google_protobuf_FieldDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FieldDescriptorProto_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_FieldDescriptorProto_serialize(const google_protobuf_FieldDescriptorProto* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FieldDescriptorProto_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_FieldDescriptorProto_serialize_ex(const google_protobuf_FieldDescriptorProto* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FieldDescriptorProto_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_clear_name(google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(36, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FieldDescriptorProto_name(const google_protobuf_FieldDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {1, UPB_SIZE(36, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_has_name(const google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(36, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_clear_extendee(google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(44, 48), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FieldDescriptorProto_extendee(const google_protobuf_FieldDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {2, UPB_SIZE(44, 48), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_has_extendee(const google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(44, 48), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_clear_number(google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {3, 12, 66, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FieldDescriptorProto_number(const google_protobuf_FieldDescriptorProto* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {3, 12, 66, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_has_number(const google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {3, 12, 66, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_clear_label(google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {4, 16, 67, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FieldDescriptorProto_label(const google_protobuf_FieldDescriptorProto* msg) { + int32_t default_val = 1; + int32_t ret; + const upb_MiniTableField field = {4, 16, 67, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_has_label(const google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {4, 16, 67, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_clear_type(google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {5, 20, 68, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FieldDescriptorProto_type(const google_protobuf_FieldDescriptorProto* msg) { + int32_t default_val = 1; + int32_t ret; + const upb_MiniTableField field = {5, 20, 68, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_has_type(const google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {5, 20, 68, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_clear_type_name(google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {6, UPB_SIZE(52, 64), 69, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FieldDescriptorProto_type_name(const google_protobuf_FieldDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {6, UPB_SIZE(52, 64), 69, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_has_type_name(const google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {6, UPB_SIZE(52, 64), 69, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_clear_default_value(google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {7, UPB_SIZE(60, 80), 70, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FieldDescriptorProto_default_value(const google_protobuf_FieldDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {7, UPB_SIZE(60, 80), 70, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_has_default_value(const google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {7, UPB_SIZE(60, 80), 70, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_clear_options(google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {8, UPB_SIZE(24, 96), 71, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FieldOptions* google_protobuf_FieldDescriptorProto_options(const google_protobuf_FieldDescriptorProto* msg) { + const google_protobuf_FieldOptions* default_val = NULL; + const google_protobuf_FieldOptions* ret; + const upb_MiniTableField field = {8, UPB_SIZE(24, 96), 71, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_has_options(const google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {8, UPB_SIZE(24, 96), 71, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_clear_oneof_index(google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {9, UPB_SIZE(28, 24), 72, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FieldDescriptorProto_oneof_index(const google_protobuf_FieldDescriptorProto* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {9, UPB_SIZE(28, 24), 72, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_has_oneof_index(const google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {9, UPB_SIZE(28, 24), 72, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_clear_json_name(google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {10, UPB_SIZE(68, 104), 73, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FieldDescriptorProto_json_name(const google_protobuf_FieldDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {10, UPB_SIZE(68, 104), 73, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_has_json_name(const google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {10, UPB_SIZE(68, 104), 73, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_clear_proto3_optional(google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {17, UPB_SIZE(32, 28), 74, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_proto3_optional(const google_protobuf_FieldDescriptorProto* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {17, UPB_SIZE(32, 28), 74, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldDescriptorProto_has_proto3_optional(const google_protobuf_FieldDescriptorProto* msg) { + const upb_MiniTableField field = {17, UPB_SIZE(32, 28), 74, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_FieldDescriptorProto_set_name(google_protobuf_FieldDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {1, UPB_SIZE(36, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_set_extendee(google_protobuf_FieldDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {2, UPB_SIZE(44, 48), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_set_number(google_protobuf_FieldDescriptorProto *msg, int32_t value) { + const upb_MiniTableField field = {3, 12, 66, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_set_label(google_protobuf_FieldDescriptorProto *msg, int32_t value) { + const upb_MiniTableField field = {4, 16, 67, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_set_type(google_protobuf_FieldDescriptorProto *msg, int32_t value) { + const upb_MiniTableField field = {5, 20, 68, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_set_type_name(google_protobuf_FieldDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {6, UPB_SIZE(52, 64), 69, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_set_default_value(google_protobuf_FieldDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {7, UPB_SIZE(60, 80), 70, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_set_options(google_protobuf_FieldDescriptorProto *msg, google_protobuf_FieldOptions* value) { + const upb_MiniTableField field = {8, UPB_SIZE(24, 96), 71, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FieldOptions* google_protobuf_FieldDescriptorProto_mutable_options(google_protobuf_FieldDescriptorProto* msg, upb_Arena* arena) { + struct google_protobuf_FieldOptions* sub = (struct google_protobuf_FieldOptions*)google_protobuf_FieldDescriptorProto_options(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FieldOptions*)_upb_Message_New(&google__protobuf__FieldOptions_msg_init, arena); + if (sub) google_protobuf_FieldDescriptorProto_set_options(msg, sub); + } + return sub; +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_set_oneof_index(google_protobuf_FieldDescriptorProto *msg, int32_t value) { + const upb_MiniTableField field = {9, UPB_SIZE(28, 24), 72, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_set_json_name(google_protobuf_FieldDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {10, UPB_SIZE(68, 104), 73, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldDescriptorProto_set_proto3_optional(google_protobuf_FieldDescriptorProto *msg, bool value) { + const upb_MiniTableField field = {17, UPB_SIZE(32, 28), 74, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.OneofDescriptorProto */ + +UPB_INLINE google_protobuf_OneofDescriptorProto* google_protobuf_OneofDescriptorProto_new(upb_Arena* arena) { + return (google_protobuf_OneofDescriptorProto*)_upb_Message_New(&google__protobuf__OneofDescriptorProto_msg_init, arena); +} +UPB_INLINE google_protobuf_OneofDescriptorProto* google_protobuf_OneofDescriptorProto_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_OneofDescriptorProto* ret = google_protobuf_OneofDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__OneofDescriptorProto_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_OneofDescriptorProto* google_protobuf_OneofDescriptorProto_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_OneofDescriptorProto* ret = google_protobuf_OneofDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__OneofDescriptorProto_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_OneofDescriptorProto_serialize(const google_protobuf_OneofDescriptorProto* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__OneofDescriptorProto_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_OneofDescriptorProto_serialize_ex(const google_protobuf_OneofDescriptorProto* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__OneofDescriptorProto_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_OneofDescriptorProto_clear_name(google_protobuf_OneofDescriptorProto* msg) { + const upb_MiniTableField field = {1, 16, 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_OneofDescriptorProto_name(const google_protobuf_OneofDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {1, 16, 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_OneofDescriptorProto_has_name(const google_protobuf_OneofDescriptorProto* msg) { + const upb_MiniTableField field = {1, 16, 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_OneofDescriptorProto_clear_options(google_protobuf_OneofDescriptorProto* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_OneofOptions* google_protobuf_OneofDescriptorProto_options(const google_protobuf_OneofDescriptorProto* msg) { + const google_protobuf_OneofOptions* default_val = NULL; + const google_protobuf_OneofOptions* ret; + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_OneofDescriptorProto_has_options(const google_protobuf_OneofDescriptorProto* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_OneofDescriptorProto_set_name(google_protobuf_OneofDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {1, 16, 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_OneofDescriptorProto_set_options(google_protobuf_OneofDescriptorProto *msg, google_protobuf_OneofOptions* value) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_OneofOptions* google_protobuf_OneofDescriptorProto_mutable_options(google_protobuf_OneofDescriptorProto* msg, upb_Arena* arena) { + struct google_protobuf_OneofOptions* sub = (struct google_protobuf_OneofOptions*)google_protobuf_OneofDescriptorProto_options(msg); + if (sub == NULL) { + sub = (struct google_protobuf_OneofOptions*)_upb_Message_New(&google__protobuf__OneofOptions_msg_init, arena); + if (sub) google_protobuf_OneofDescriptorProto_set_options(msg, sub); + } + return sub; +} + +/* google.protobuf.EnumDescriptorProto */ + +UPB_INLINE google_protobuf_EnumDescriptorProto* google_protobuf_EnumDescriptorProto_new(upb_Arena* arena) { + return (google_protobuf_EnumDescriptorProto*)_upb_Message_New(&google__protobuf__EnumDescriptorProto_msg_init, arena); +} +UPB_INLINE google_protobuf_EnumDescriptorProto* google_protobuf_EnumDescriptorProto_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_EnumDescriptorProto* ret = google_protobuf_EnumDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__EnumDescriptorProto_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_EnumDescriptorProto* google_protobuf_EnumDescriptorProto_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_EnumDescriptorProto* ret = google_protobuf_EnumDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__EnumDescriptorProto_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_EnumDescriptorProto_serialize(const google_protobuf_EnumDescriptorProto* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__EnumDescriptorProto_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_EnumDescriptorProto_serialize_ex(const google_protobuf_EnumDescriptorProto* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__EnumDescriptorProto_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_EnumDescriptorProto_clear_name(google_protobuf_EnumDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(28, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_EnumDescriptorProto_name(const google_protobuf_EnumDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {1, UPB_SIZE(28, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumDescriptorProto_has_name(const google_protobuf_EnumDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(28, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumDescriptorProto_clear_value(google_protobuf_EnumDescriptorProto* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_EnumValueDescriptorProto* const* google_protobuf_EnumDescriptorProto_value(const google_protobuf_EnumDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_EnumValueDescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_EnumDescriptorProto_value_upb_array(const google_protobuf_EnumDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_EnumDescriptorProto_value_mutable_upb_array(google_protobuf_EnumDescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_EnumDescriptorProto_clear_options(google_protobuf_EnumDescriptorProto* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_EnumOptions* google_protobuf_EnumDescriptorProto_options(const google_protobuf_EnumDescriptorProto* msg) { + const google_protobuf_EnumOptions* default_val = NULL; + const google_protobuf_EnumOptions* ret; + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumDescriptorProto_has_options(const google_protobuf_EnumDescriptorProto* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumDescriptorProto_clear_reserved_range(google_protobuf_EnumDescriptorProto* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_EnumDescriptorProto_EnumReservedRange* const* google_protobuf_EnumDescriptorProto_reserved_range(const google_protobuf_EnumDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_EnumDescriptorProto_EnumReservedRange* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_EnumDescriptorProto_reserved_range_upb_array(const google_protobuf_EnumDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_EnumDescriptorProto_reserved_range_mutable_upb_array(google_protobuf_EnumDescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_EnumDescriptorProto_clear_reserved_name(google_protobuf_EnumDescriptorProto* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView const* google_protobuf_EnumDescriptorProto_reserved_name(const google_protobuf_EnumDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (upb_StringView const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_EnumDescriptorProto_reserved_name_upb_array(const google_protobuf_EnumDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_EnumDescriptorProto_reserved_name_mutable_upb_array(google_protobuf_EnumDescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE void google_protobuf_EnumDescriptorProto_set_name(google_protobuf_EnumDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {1, UPB_SIZE(28, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE google_protobuf_EnumValueDescriptorProto** google_protobuf_EnumDescriptorProto_mutable_value(google_protobuf_EnumDescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_EnumValueDescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_EnumValueDescriptorProto** google_protobuf_EnumDescriptorProto_resize_value(google_protobuf_EnumDescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_EnumValueDescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_EnumValueDescriptorProto* google_protobuf_EnumDescriptorProto_add_value(google_protobuf_EnumDescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_EnumValueDescriptorProto* sub = (struct google_protobuf_EnumValueDescriptorProto*)_upb_Message_New(&google__protobuf__EnumValueDescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE void google_protobuf_EnumDescriptorProto_set_options(google_protobuf_EnumDescriptorProto *msg, google_protobuf_EnumOptions* value) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_EnumOptions* google_protobuf_EnumDescriptorProto_mutable_options(google_protobuf_EnumDescriptorProto* msg, upb_Arena* arena) { + struct google_protobuf_EnumOptions* sub = (struct google_protobuf_EnumOptions*)google_protobuf_EnumDescriptorProto_options(msg); + if (sub == NULL) { + sub = (struct google_protobuf_EnumOptions*)_upb_Message_New(&google__protobuf__EnumOptions_msg_init, arena); + if (sub) google_protobuf_EnumDescriptorProto_set_options(msg, sub); + } + return sub; +} +UPB_INLINE google_protobuf_EnumDescriptorProto_EnumReservedRange** google_protobuf_EnumDescriptorProto_mutable_reserved_range(google_protobuf_EnumDescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_EnumDescriptorProto_EnumReservedRange**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_EnumDescriptorProto_EnumReservedRange** google_protobuf_EnumDescriptorProto_resize_reserved_range(google_protobuf_EnumDescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_EnumDescriptorProto_EnumReservedRange**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_EnumDescriptorProto_EnumReservedRange* google_protobuf_EnumDescriptorProto_add_reserved_range(google_protobuf_EnumDescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_EnumDescriptorProto_EnumReservedRange* sub = (struct google_protobuf_EnumDescriptorProto_EnumReservedRange*)_upb_Message_New(&google__protobuf__EnumDescriptorProto__EnumReservedRange_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE upb_StringView* google_protobuf_EnumDescriptorProto_mutable_reserved_name(google_protobuf_EnumDescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (upb_StringView*)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE upb_StringView* google_protobuf_EnumDescriptorProto_resize_reserved_name(google_protobuf_EnumDescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (upb_StringView*)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE bool google_protobuf_EnumDescriptorProto_add_reserved_name(google_protobuf_EnumDescriptorProto* msg, upb_StringView val, upb_Arena* arena) { + upb_MiniTableField field = {5, UPB_SIZE(24, 56), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return false; + } + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &val, sizeof(val)); + return true; +} + +/* google.protobuf.EnumDescriptorProto.EnumReservedRange */ + +UPB_INLINE google_protobuf_EnumDescriptorProto_EnumReservedRange* google_protobuf_EnumDescriptorProto_EnumReservedRange_new(upb_Arena* arena) { + return (google_protobuf_EnumDescriptorProto_EnumReservedRange*)_upb_Message_New(&google__protobuf__EnumDescriptorProto__EnumReservedRange_msg_init, arena); +} +UPB_INLINE google_protobuf_EnumDescriptorProto_EnumReservedRange* google_protobuf_EnumDescriptorProto_EnumReservedRange_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_EnumDescriptorProto_EnumReservedRange* ret = google_protobuf_EnumDescriptorProto_EnumReservedRange_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__EnumDescriptorProto__EnumReservedRange_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_EnumDescriptorProto_EnumReservedRange* google_protobuf_EnumDescriptorProto_EnumReservedRange_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_EnumDescriptorProto_EnumReservedRange* ret = google_protobuf_EnumDescriptorProto_EnumReservedRange_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__EnumDescriptorProto__EnumReservedRange_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_EnumDescriptorProto_EnumReservedRange_serialize(const google_protobuf_EnumDescriptorProto_EnumReservedRange* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__EnumDescriptorProto__EnumReservedRange_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_EnumDescriptorProto_EnumReservedRange_serialize_ex(const google_protobuf_EnumDescriptorProto_EnumReservedRange* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__EnumDescriptorProto__EnumReservedRange_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_EnumDescriptorProto_EnumReservedRange_clear_start(google_protobuf_EnumDescriptorProto_EnumReservedRange* msg) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_EnumDescriptorProto_EnumReservedRange_start(const google_protobuf_EnumDescriptorProto_EnumReservedRange* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumDescriptorProto_EnumReservedRange_has_start(const google_protobuf_EnumDescriptorProto_EnumReservedRange* msg) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumDescriptorProto_EnumReservedRange_clear_end(google_protobuf_EnumDescriptorProto_EnumReservedRange* msg) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_EnumDescriptorProto_EnumReservedRange_end(const google_protobuf_EnumDescriptorProto_EnumReservedRange* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumDescriptorProto_EnumReservedRange_has_end(const google_protobuf_EnumDescriptorProto_EnumReservedRange* msg) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_EnumDescriptorProto_EnumReservedRange_set_start(google_protobuf_EnumDescriptorProto_EnumReservedRange *msg, int32_t value) { + const upb_MiniTableField field = {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_EnumDescriptorProto_EnumReservedRange_set_end(google_protobuf_EnumDescriptorProto_EnumReservedRange *msg, int32_t value) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.EnumValueDescriptorProto */ + +UPB_INLINE google_protobuf_EnumValueDescriptorProto* google_protobuf_EnumValueDescriptorProto_new(upb_Arena* arena) { + return (google_protobuf_EnumValueDescriptorProto*)_upb_Message_New(&google__protobuf__EnumValueDescriptorProto_msg_init, arena); +} +UPB_INLINE google_protobuf_EnumValueDescriptorProto* google_protobuf_EnumValueDescriptorProto_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_EnumValueDescriptorProto* ret = google_protobuf_EnumValueDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__EnumValueDescriptorProto_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_EnumValueDescriptorProto* google_protobuf_EnumValueDescriptorProto_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_EnumValueDescriptorProto* ret = google_protobuf_EnumValueDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__EnumValueDescriptorProto_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_EnumValueDescriptorProto_serialize(const google_protobuf_EnumValueDescriptorProto* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__EnumValueDescriptorProto_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_EnumValueDescriptorProto_serialize_ex(const google_protobuf_EnumValueDescriptorProto* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__EnumValueDescriptorProto_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_EnumValueDescriptorProto_clear_name(google_protobuf_EnumValueDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_EnumValueDescriptorProto_name(const google_protobuf_EnumValueDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumValueDescriptorProto_has_name(const google_protobuf_EnumValueDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumValueDescriptorProto_clear_number(google_protobuf_EnumValueDescriptorProto* msg) { + const upb_MiniTableField field = {2, 12, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_EnumValueDescriptorProto_number(const google_protobuf_EnumValueDescriptorProto* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {2, 12, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumValueDescriptorProto_has_number(const google_protobuf_EnumValueDescriptorProto* msg) { + const upb_MiniTableField field = {2, 12, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumValueDescriptorProto_clear_options(google_protobuf_EnumValueDescriptorProto* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 32), 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_EnumValueOptions* google_protobuf_EnumValueDescriptorProto_options(const google_protobuf_EnumValueDescriptorProto* msg) { + const google_protobuf_EnumValueOptions* default_val = NULL; + const google_protobuf_EnumValueOptions* ret; + const upb_MiniTableField field = {3, UPB_SIZE(16, 32), 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumValueDescriptorProto_has_options(const google_protobuf_EnumValueDescriptorProto* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 32), 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_EnumValueDescriptorProto_set_name(google_protobuf_EnumValueDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_EnumValueDescriptorProto_set_number(google_protobuf_EnumValueDescriptorProto *msg, int32_t value) { + const upb_MiniTableField field = {2, 12, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_EnumValueDescriptorProto_set_options(google_protobuf_EnumValueDescriptorProto *msg, google_protobuf_EnumValueOptions* value) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 32), 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_EnumValueOptions* google_protobuf_EnumValueDescriptorProto_mutable_options(google_protobuf_EnumValueDescriptorProto* msg, upb_Arena* arena) { + struct google_protobuf_EnumValueOptions* sub = (struct google_protobuf_EnumValueOptions*)google_protobuf_EnumValueDescriptorProto_options(msg); + if (sub == NULL) { + sub = (struct google_protobuf_EnumValueOptions*)_upb_Message_New(&google__protobuf__EnumValueOptions_msg_init, arena); + if (sub) google_protobuf_EnumValueDescriptorProto_set_options(msg, sub); + } + return sub; +} + +/* google.protobuf.ServiceDescriptorProto */ + +UPB_INLINE google_protobuf_ServiceDescriptorProto* google_protobuf_ServiceDescriptorProto_new(upb_Arena* arena) { + return (google_protobuf_ServiceDescriptorProto*)_upb_Message_New(&google__protobuf__ServiceDescriptorProto_msg_init, arena); +} +UPB_INLINE google_protobuf_ServiceDescriptorProto* google_protobuf_ServiceDescriptorProto_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_ServiceDescriptorProto* ret = google_protobuf_ServiceDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__ServiceDescriptorProto_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_ServiceDescriptorProto* google_protobuf_ServiceDescriptorProto_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_ServiceDescriptorProto* ret = google_protobuf_ServiceDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__ServiceDescriptorProto_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_ServiceDescriptorProto_serialize(const google_protobuf_ServiceDescriptorProto* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__ServiceDescriptorProto_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_ServiceDescriptorProto_serialize_ex(const google_protobuf_ServiceDescriptorProto* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__ServiceDescriptorProto_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_ServiceDescriptorProto_clear_name(google_protobuf_ServiceDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_ServiceDescriptorProto_name(const google_protobuf_ServiceDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_ServiceDescriptorProto_has_name(const google_protobuf_ServiceDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_ServiceDescriptorProto_clear_method(google_protobuf_ServiceDescriptorProto* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_MethodDescriptorProto* const* google_protobuf_ServiceDescriptorProto_method(const google_protobuf_ServiceDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_MethodDescriptorProto* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_ServiceDescriptorProto_method_upb_array(const google_protobuf_ServiceDescriptorProto* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_ServiceDescriptorProto_method_mutable_upb_array(google_protobuf_ServiceDescriptorProto* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_ServiceDescriptorProto_clear_options(google_protobuf_ServiceDescriptorProto* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_ServiceOptions* google_protobuf_ServiceDescriptorProto_options(const google_protobuf_ServiceDescriptorProto* msg) { + const google_protobuf_ServiceOptions* default_val = NULL; + const google_protobuf_ServiceOptions* ret; + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_ServiceDescriptorProto_has_options(const google_protobuf_ServiceDescriptorProto* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_ServiceDescriptorProto_set_name(google_protobuf_ServiceDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE google_protobuf_MethodDescriptorProto** google_protobuf_ServiceDescriptorProto_mutable_method(google_protobuf_ServiceDescriptorProto* msg, size_t* size) { + upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_MethodDescriptorProto**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_MethodDescriptorProto** google_protobuf_ServiceDescriptorProto_resize_method(google_protobuf_ServiceDescriptorProto* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_MethodDescriptorProto**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_MethodDescriptorProto* google_protobuf_ServiceDescriptorProto_add_method(google_protobuf_ServiceDescriptorProto* msg, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_MethodDescriptorProto* sub = (struct google_protobuf_MethodDescriptorProto*)_upb_Message_New(&google__protobuf__MethodDescriptorProto_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE void google_protobuf_ServiceDescriptorProto_set_options(google_protobuf_ServiceDescriptorProto *msg, google_protobuf_ServiceOptions* value) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 40), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_ServiceOptions* google_protobuf_ServiceDescriptorProto_mutable_options(google_protobuf_ServiceDescriptorProto* msg, upb_Arena* arena) { + struct google_protobuf_ServiceOptions* sub = (struct google_protobuf_ServiceOptions*)google_protobuf_ServiceDescriptorProto_options(msg); + if (sub == NULL) { + sub = (struct google_protobuf_ServiceOptions*)_upb_Message_New(&google__protobuf__ServiceOptions_msg_init, arena); + if (sub) google_protobuf_ServiceDescriptorProto_set_options(msg, sub); + } + return sub; +} + +/* google.protobuf.MethodDescriptorProto */ + +UPB_INLINE google_protobuf_MethodDescriptorProto* google_protobuf_MethodDescriptorProto_new(upb_Arena* arena) { + return (google_protobuf_MethodDescriptorProto*)_upb_Message_New(&google__protobuf__MethodDescriptorProto_msg_init, arena); +} +UPB_INLINE google_protobuf_MethodDescriptorProto* google_protobuf_MethodDescriptorProto_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_MethodDescriptorProto* ret = google_protobuf_MethodDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__MethodDescriptorProto_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_MethodDescriptorProto* google_protobuf_MethodDescriptorProto_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_MethodDescriptorProto* ret = google_protobuf_MethodDescriptorProto_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__MethodDescriptorProto_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_MethodDescriptorProto_serialize(const google_protobuf_MethodDescriptorProto* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__MethodDescriptorProto_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_MethodDescriptorProto_serialize_ex(const google_protobuf_MethodDescriptorProto* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__MethodDescriptorProto_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_MethodDescriptorProto_clear_name(google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_MethodDescriptorProto_name(const google_protobuf_MethodDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MethodDescriptorProto_has_name(const google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MethodDescriptorProto_clear_input_type(google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(28, 32), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_MethodDescriptorProto_input_type(const google_protobuf_MethodDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {2, UPB_SIZE(28, 32), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MethodDescriptorProto_has_input_type(const google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(28, 32), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MethodDescriptorProto_clear_output_type(google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(36, 48), 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_MethodDescriptorProto_output_type(const google_protobuf_MethodDescriptorProto* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {3, UPB_SIZE(36, 48), 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MethodDescriptorProto_has_output_type(const google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(36, 48), 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MethodDescriptorProto_clear_options(google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(12, 64), 67, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_MethodOptions* google_protobuf_MethodDescriptorProto_options(const google_protobuf_MethodDescriptorProto* msg) { + const google_protobuf_MethodOptions* default_val = NULL; + const google_protobuf_MethodOptions* ret; + const upb_MiniTableField field = {4, UPB_SIZE(12, 64), 67, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MethodDescriptorProto_has_options(const google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(12, 64), 67, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MethodDescriptorProto_clear_client_streaming(google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(16, 9), 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_MethodDescriptorProto_client_streaming(const google_protobuf_MethodDescriptorProto* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {5, UPB_SIZE(16, 9), 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MethodDescriptorProto_has_client_streaming(const google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(16, 9), 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MethodDescriptorProto_clear_server_streaming(google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {6, UPB_SIZE(17, 10), 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_MethodDescriptorProto_server_streaming(const google_protobuf_MethodDescriptorProto* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {6, UPB_SIZE(17, 10), 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MethodDescriptorProto_has_server_streaming(const google_protobuf_MethodDescriptorProto* msg) { + const upb_MiniTableField field = {6, UPB_SIZE(17, 10), 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_MethodDescriptorProto_set_name(google_protobuf_MethodDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_MethodDescriptorProto_set_input_type(google_protobuf_MethodDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {2, UPB_SIZE(28, 32), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_MethodDescriptorProto_set_output_type(google_protobuf_MethodDescriptorProto *msg, upb_StringView value) { + const upb_MiniTableField field = {3, UPB_SIZE(36, 48), 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_MethodDescriptorProto_set_options(google_protobuf_MethodDescriptorProto *msg, google_protobuf_MethodOptions* value) { + const upb_MiniTableField field = {4, UPB_SIZE(12, 64), 67, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_MethodOptions* google_protobuf_MethodDescriptorProto_mutable_options(google_protobuf_MethodDescriptorProto* msg, upb_Arena* arena) { + struct google_protobuf_MethodOptions* sub = (struct google_protobuf_MethodOptions*)google_protobuf_MethodDescriptorProto_options(msg); + if (sub == NULL) { + sub = (struct google_protobuf_MethodOptions*)_upb_Message_New(&google__protobuf__MethodOptions_msg_init, arena); + if (sub) google_protobuf_MethodDescriptorProto_set_options(msg, sub); + } + return sub; +} +UPB_INLINE void google_protobuf_MethodDescriptorProto_set_client_streaming(google_protobuf_MethodDescriptorProto *msg, bool value) { + const upb_MiniTableField field = {5, UPB_SIZE(16, 9), 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_MethodDescriptorProto_set_server_streaming(google_protobuf_MethodDescriptorProto *msg, bool value) { + const upb_MiniTableField field = {6, UPB_SIZE(17, 10), 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.FileOptions */ + +UPB_INLINE google_protobuf_FileOptions* google_protobuf_FileOptions_new(upb_Arena* arena) { + return (google_protobuf_FileOptions*)_upb_Message_New(&google__protobuf__FileOptions_msg_init, arena); +} +UPB_INLINE google_protobuf_FileOptions* google_protobuf_FileOptions_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_FileOptions* ret = google_protobuf_FileOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FileOptions_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_FileOptions* google_protobuf_FileOptions_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_FileOptions* ret = google_protobuf_FileOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FileOptions_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_FileOptions_serialize(const google_protobuf_FileOptions* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FileOptions_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_FileOptions_serialize_ex(const google_protobuf_FileOptions* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FileOptions_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_FileOptions_clear_java_package(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(32, 24), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileOptions_java_package(const google_protobuf_FileOptions* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {1, UPB_SIZE(32, 24), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_java_package(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(32, 24), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_java_outer_classname(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {8, 40, 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileOptions_java_outer_classname(const google_protobuf_FileOptions* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {8, 40, 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_java_outer_classname(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {8, 40, 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_optimize_for(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {9, 12, 66, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FileOptions_optimize_for(const google_protobuf_FileOptions* msg) { + int32_t default_val = 1; + int32_t ret; + const upb_MiniTableField field = {9, 12, 66, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_optimize_for(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {9, 12, 66, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_java_multiple_files(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {10, 16, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FileOptions_java_multiple_files(const google_protobuf_FileOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {10, 16, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_java_multiple_files(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {10, 16, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_go_package(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {11, UPB_SIZE(48, 56), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileOptions_go_package(const google_protobuf_FileOptions* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {11, UPB_SIZE(48, 56), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_go_package(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {11, UPB_SIZE(48, 56), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_cc_generic_services(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {16, 17, 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FileOptions_cc_generic_services(const google_protobuf_FileOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {16, 17, 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_cc_generic_services(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {16, 17, 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_java_generic_services(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {17, 18, 70, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FileOptions_java_generic_services(const google_protobuf_FileOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {17, 18, 70, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_java_generic_services(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {17, 18, 70, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_py_generic_services(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {18, 19, 71, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FileOptions_py_generic_services(const google_protobuf_FileOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {18, 19, 71, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_py_generic_services(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {18, 19, 71, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_java_generate_equals_and_hash(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {20, 20, 72, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FileOptions_java_generate_equals_and_hash(const google_protobuf_FileOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {20, 20, 72, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_java_generate_equals_and_hash(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {20, 20, 72, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_deprecated(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {23, 21, 73, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FileOptions_deprecated(const google_protobuf_FileOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {23, 21, 73, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_deprecated(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {23, 21, 73, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_java_string_check_utf8(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {27, 22, 74, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FileOptions_java_string_check_utf8(const google_protobuf_FileOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {27, 22, 74, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_java_string_check_utf8(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {27, 22, 74, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_cc_enable_arenas(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {31, 23, 75, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FileOptions_cc_enable_arenas(const google_protobuf_FileOptions* msg) { + bool default_val = true; + bool ret; + const upb_MiniTableField field = {31, 23, 75, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_cc_enable_arenas(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {31, 23, 75, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_objc_class_prefix(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {36, UPB_SIZE(56, 72), 76, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileOptions_objc_class_prefix(const google_protobuf_FileOptions* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {36, UPB_SIZE(56, 72), 76, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_objc_class_prefix(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {36, UPB_SIZE(56, 72), 76, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_csharp_namespace(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {37, UPB_SIZE(64, 88), 77, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileOptions_csharp_namespace(const google_protobuf_FileOptions* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {37, UPB_SIZE(64, 88), 77, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_csharp_namespace(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {37, UPB_SIZE(64, 88), 77, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_swift_prefix(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {39, UPB_SIZE(72, 104), 78, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileOptions_swift_prefix(const google_protobuf_FileOptions* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {39, UPB_SIZE(72, 104), 78, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_swift_prefix(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {39, UPB_SIZE(72, 104), 78, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_php_class_prefix(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {40, UPB_SIZE(80, 120), 79, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileOptions_php_class_prefix(const google_protobuf_FileOptions* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {40, UPB_SIZE(80, 120), 79, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_php_class_prefix(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {40, UPB_SIZE(80, 120), 79, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_php_namespace(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {41, UPB_SIZE(88, 136), 80, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileOptions_php_namespace(const google_protobuf_FileOptions* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {41, UPB_SIZE(88, 136), 80, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_php_namespace(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {41, UPB_SIZE(88, 136), 80, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_php_metadata_namespace(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {44, UPB_SIZE(96, 152), 81, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileOptions_php_metadata_namespace(const google_protobuf_FileOptions* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {44, UPB_SIZE(96, 152), 81, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_php_metadata_namespace(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {44, UPB_SIZE(96, 152), 81, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_ruby_package(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {45, UPB_SIZE(104, 168), 82, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FileOptions_ruby_package(const google_protobuf_FileOptions* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {45, UPB_SIZE(104, 168), 82, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_ruby_package(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {45, UPB_SIZE(104, 168), 82, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_features(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {50, UPB_SIZE(24, 184), 83, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSet* google_protobuf_FileOptions_features(const google_protobuf_FileOptions* msg) { + const google_protobuf_FeatureSet* default_val = NULL; + const google_protobuf_FeatureSet* ret; + const upb_MiniTableField field = {50, UPB_SIZE(24, 184), 83, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FileOptions_has_features(const google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {50, UPB_SIZE(24, 184), 83, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FileOptions_clear_uninterpreted_option(google_protobuf_FileOptions* msg) { + const upb_MiniTableField field = {999, UPB_SIZE(28, 192), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_UninterpretedOption* const* google_protobuf_FileOptions_uninterpreted_option(const google_protobuf_FileOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(28, 192), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_UninterpretedOption* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FileOptions_uninterpreted_option_upb_array(const google_protobuf_FileOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(28, 192), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FileOptions_uninterpreted_option_mutable_upb_array(google_protobuf_FileOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {999, UPB_SIZE(28, 192), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE void google_protobuf_FileOptions_set_java_package(google_protobuf_FileOptions *msg, upb_StringView value) { + const upb_MiniTableField field = {1, UPB_SIZE(32, 24), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_java_outer_classname(google_protobuf_FileOptions *msg, upb_StringView value) { + const upb_MiniTableField field = {8, 40, 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_optimize_for(google_protobuf_FileOptions *msg, int32_t value) { + const upb_MiniTableField field = {9, 12, 66, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_java_multiple_files(google_protobuf_FileOptions *msg, bool value) { + const upb_MiniTableField field = {10, 16, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_go_package(google_protobuf_FileOptions *msg, upb_StringView value) { + const upb_MiniTableField field = {11, UPB_SIZE(48, 56), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_cc_generic_services(google_protobuf_FileOptions *msg, bool value) { + const upb_MiniTableField field = {16, 17, 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_java_generic_services(google_protobuf_FileOptions *msg, bool value) { + const upb_MiniTableField field = {17, 18, 70, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_py_generic_services(google_protobuf_FileOptions *msg, bool value) { + const upb_MiniTableField field = {18, 19, 71, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_java_generate_equals_and_hash(google_protobuf_FileOptions *msg, bool value) { + const upb_MiniTableField field = {20, 20, 72, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_deprecated(google_protobuf_FileOptions *msg, bool value) { + const upb_MiniTableField field = {23, 21, 73, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_java_string_check_utf8(google_protobuf_FileOptions *msg, bool value) { + const upb_MiniTableField field = {27, 22, 74, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_cc_enable_arenas(google_protobuf_FileOptions *msg, bool value) { + const upb_MiniTableField field = {31, 23, 75, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_objc_class_prefix(google_protobuf_FileOptions *msg, upb_StringView value) { + const upb_MiniTableField field = {36, UPB_SIZE(56, 72), 76, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_csharp_namespace(google_protobuf_FileOptions *msg, upb_StringView value) { + const upb_MiniTableField field = {37, UPB_SIZE(64, 88), 77, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_swift_prefix(google_protobuf_FileOptions *msg, upb_StringView value) { + const upb_MiniTableField field = {39, UPB_SIZE(72, 104), 78, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_php_class_prefix(google_protobuf_FileOptions *msg, upb_StringView value) { + const upb_MiniTableField field = {40, UPB_SIZE(80, 120), 79, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_php_namespace(google_protobuf_FileOptions *msg, upb_StringView value) { + const upb_MiniTableField field = {41, UPB_SIZE(88, 136), 80, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_php_metadata_namespace(google_protobuf_FileOptions *msg, upb_StringView value) { + const upb_MiniTableField field = {44, UPB_SIZE(96, 152), 81, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_ruby_package(google_protobuf_FileOptions *msg, upb_StringView value) { + const upb_MiniTableField field = {45, UPB_SIZE(104, 168), 82, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FileOptions_set_features(google_protobuf_FileOptions *msg, google_protobuf_FeatureSet* value) { + const upb_MiniTableField field = {50, UPB_SIZE(24, 184), 83, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FeatureSet* google_protobuf_FileOptions_mutable_features(google_protobuf_FileOptions* msg, upb_Arena* arena) { + struct google_protobuf_FeatureSet* sub = (struct google_protobuf_FeatureSet*)google_protobuf_FileOptions_features(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); + if (sub) google_protobuf_FileOptions_set_features(msg, sub); + } + return sub; +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_FileOptions_mutable_uninterpreted_option(google_protobuf_FileOptions* msg, size_t* size) { + upb_MiniTableField field = {999, UPB_SIZE(28, 192), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_UninterpretedOption**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_FileOptions_resize_uninterpreted_option(google_protobuf_FileOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(28, 192), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_UninterpretedOption**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_UninterpretedOption* google_protobuf_FileOptions_add_uninterpreted_option(google_protobuf_FileOptions* msg, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(28, 192), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_UninterpretedOption* sub = (struct google_protobuf_UninterpretedOption*)_upb_Message_New(&google__protobuf__UninterpretedOption_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.MessageOptions */ + +UPB_INLINE google_protobuf_MessageOptions* google_protobuf_MessageOptions_new(upb_Arena* arena) { + return (google_protobuf_MessageOptions*)_upb_Message_New(&google__protobuf__MessageOptions_msg_init, arena); +} +UPB_INLINE google_protobuf_MessageOptions* google_protobuf_MessageOptions_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_MessageOptions* ret = google_protobuf_MessageOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__MessageOptions_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_MessageOptions* google_protobuf_MessageOptions_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_MessageOptions* ret = google_protobuf_MessageOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__MessageOptions_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_MessageOptions_serialize(const google_protobuf_MessageOptions* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__MessageOptions_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_MessageOptions_serialize_ex(const google_protobuf_MessageOptions* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__MessageOptions_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_MessageOptions_clear_message_set_wire_format(google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {1, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_MessageOptions_message_set_wire_format(const google_protobuf_MessageOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {1, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MessageOptions_has_message_set_wire_format(const google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {1, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MessageOptions_clear_no_standard_descriptor_accessor(google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {2, 10, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_MessageOptions_no_standard_descriptor_accessor(const google_protobuf_MessageOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {2, 10, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MessageOptions_has_no_standard_descriptor_accessor(const google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {2, 10, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MessageOptions_clear_deprecated(google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {3, 11, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_MessageOptions_deprecated(const google_protobuf_MessageOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {3, 11, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MessageOptions_has_deprecated(const google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {3, 11, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MessageOptions_clear_map_entry(google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {7, 12, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_MessageOptions_map_entry(const google_protobuf_MessageOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {7, 12, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MessageOptions_has_map_entry(const google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {7, 12, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MessageOptions_clear_deprecated_legacy_json_field_conflicts(google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {11, 13, 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_MessageOptions_deprecated_legacy_json_field_conflicts(const google_protobuf_MessageOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {11, 13, 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MessageOptions_has_deprecated_legacy_json_field_conflicts(const google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {11, 13, 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MessageOptions_clear_features(google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {12, 16, 69, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSet* google_protobuf_MessageOptions_features(const google_protobuf_MessageOptions* msg) { + const google_protobuf_FeatureSet* default_val = NULL; + const google_protobuf_FeatureSet* ret; + const upb_MiniTableField field = {12, 16, 69, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MessageOptions_has_features(const google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {12, 16, 69, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MessageOptions_clear_uninterpreted_option(google_protobuf_MessageOptions* msg) { + const upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_UninterpretedOption* const* google_protobuf_MessageOptions_uninterpreted_option(const google_protobuf_MessageOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_UninterpretedOption* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_MessageOptions_uninterpreted_option_upb_array(const google_protobuf_MessageOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_MessageOptions_uninterpreted_option_mutable_upb_array(google_protobuf_MessageOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE void google_protobuf_MessageOptions_set_message_set_wire_format(google_protobuf_MessageOptions *msg, bool value) { + const upb_MiniTableField field = {1, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_MessageOptions_set_no_standard_descriptor_accessor(google_protobuf_MessageOptions *msg, bool value) { + const upb_MiniTableField field = {2, 10, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_MessageOptions_set_deprecated(google_protobuf_MessageOptions *msg, bool value) { + const upb_MiniTableField field = {3, 11, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_MessageOptions_set_map_entry(google_protobuf_MessageOptions *msg, bool value) { + const upb_MiniTableField field = {7, 12, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_MessageOptions_set_deprecated_legacy_json_field_conflicts(google_protobuf_MessageOptions *msg, bool value) { + const upb_MiniTableField field = {11, 13, 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_MessageOptions_set_features(google_protobuf_MessageOptions *msg, google_protobuf_FeatureSet* value) { + const upb_MiniTableField field = {12, 16, 69, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FeatureSet* google_protobuf_MessageOptions_mutable_features(google_protobuf_MessageOptions* msg, upb_Arena* arena) { + struct google_protobuf_FeatureSet* sub = (struct google_protobuf_FeatureSet*)google_protobuf_MessageOptions_features(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); + if (sub) google_protobuf_MessageOptions_set_features(msg, sub); + } + return sub; +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_MessageOptions_mutable_uninterpreted_option(google_protobuf_MessageOptions* msg, size_t* size) { + upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_UninterpretedOption**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_MessageOptions_resize_uninterpreted_option(google_protobuf_MessageOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_UninterpretedOption**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_UninterpretedOption* google_protobuf_MessageOptions_add_uninterpreted_option(google_protobuf_MessageOptions* msg, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_UninterpretedOption* sub = (struct google_protobuf_UninterpretedOption*)_upb_Message_New(&google__protobuf__UninterpretedOption_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.FieldOptions */ + +UPB_INLINE google_protobuf_FieldOptions* google_protobuf_FieldOptions_new(upb_Arena* arena) { + return (google_protobuf_FieldOptions*)_upb_Message_New(&google__protobuf__FieldOptions_msg_init, arena); +} +UPB_INLINE google_protobuf_FieldOptions* google_protobuf_FieldOptions_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_FieldOptions* ret = google_protobuf_FieldOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FieldOptions_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_FieldOptions* google_protobuf_FieldOptions_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_FieldOptions* ret = google_protobuf_FieldOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FieldOptions_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_FieldOptions_serialize(const google_protobuf_FieldOptions* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FieldOptions_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_FieldOptions_serialize_ex(const google_protobuf_FieldOptions* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FieldOptions_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_FieldOptions_clear_ctype(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {1, 12, 64, 4, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FieldOptions_ctype(const google_protobuf_FieldOptions* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {1, 12, 64, 4, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_has_ctype(const google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {1, 12, 64, 4, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_clear_packed(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FieldOptions_packed(const google_protobuf_FieldOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_has_packed(const google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_clear_deprecated(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {3, 17, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FieldOptions_deprecated(const google_protobuf_FieldOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {3, 17, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_has_deprecated(const google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {3, 17, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_clear_lazy(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {5, 18, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FieldOptions_lazy(const google_protobuf_FieldOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {5, 18, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_has_lazy(const google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {5, 18, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_clear_jstype(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {6, 20, 68, 5, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FieldOptions_jstype(const google_protobuf_FieldOptions* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {6, 20, 68, 5, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_has_jstype(const google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {6, 20, 68, 5, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_clear_weak(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {10, 24, 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FieldOptions_weak(const google_protobuf_FieldOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {10, 24, 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_has_weak(const google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {10, 24, 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_clear_unverified_lazy(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {15, 25, 70, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FieldOptions_unverified_lazy(const google_protobuf_FieldOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {15, 25, 70, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_has_unverified_lazy(const google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {15, 25, 70, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_clear_debug_redact(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {16, 26, 71, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_FieldOptions_debug_redact(const google_protobuf_FieldOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {16, 26, 71, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_has_debug_redact(const google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {16, 26, 71, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_clear_retention(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {17, 28, 72, 6, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FieldOptions_retention(const google_protobuf_FieldOptions* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {17, 28, 72, 6, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_has_retention(const google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {17, 28, 72, 6, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_clear_targets(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {19, 32, 0, 7, 14, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t const* google_protobuf_FieldOptions_targets(const google_protobuf_FieldOptions* msg, size_t* size) { + const upb_MiniTableField field = {19, 32, 0, 7, 14, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FieldOptions_targets_upb_array(const google_protobuf_FieldOptions* msg, size_t* size) { + const upb_MiniTableField field = {19, 32, 0, 7, 14, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FieldOptions_targets_mutable_upb_array(google_protobuf_FieldOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {19, 32, 0, 7, 14, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_FieldOptions_clear_edition_defaults(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {20, UPB_SIZE(36, 40), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FieldOptions_EditionDefault* const* google_protobuf_FieldOptions_edition_defaults(const google_protobuf_FieldOptions* msg, size_t* size) { + const upb_MiniTableField field = {20, UPB_SIZE(36, 40), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_FieldOptions_EditionDefault* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FieldOptions_edition_defaults_upb_array(const google_protobuf_FieldOptions* msg, size_t* size) { + const upb_MiniTableField field = {20, UPB_SIZE(36, 40), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FieldOptions_edition_defaults_mutable_upb_array(google_protobuf_FieldOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {20, UPB_SIZE(36, 40), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_FieldOptions_clear_features(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {21, UPB_SIZE(40, 48), 73, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSet* google_protobuf_FieldOptions_features(const google_protobuf_FieldOptions* msg) { + const google_protobuf_FeatureSet* default_val = NULL; + const google_protobuf_FeatureSet* ret; + const upb_MiniTableField field = {21, UPB_SIZE(40, 48), 73, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_has_features(const google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {21, UPB_SIZE(40, 48), 73, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_clear_feature_support(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {22, UPB_SIZE(44, 56), 74, 2, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FieldOptions_FeatureSupport* google_protobuf_FieldOptions_feature_support(const google_protobuf_FieldOptions* msg) { + const google_protobuf_FieldOptions_FeatureSupport* default_val = NULL; + const google_protobuf_FieldOptions_FeatureSupport* ret; + const upb_MiniTableField field = {22, UPB_SIZE(44, 56), 74, 2, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_has_feature_support(const google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {22, UPB_SIZE(44, 56), 74, 2, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_clear_uninterpreted_option(google_protobuf_FieldOptions* msg) { + const upb_MiniTableField field = {999, UPB_SIZE(48, 64), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_UninterpretedOption* const* google_protobuf_FieldOptions_uninterpreted_option(const google_protobuf_FieldOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(48, 64), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_UninterpretedOption* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FieldOptions_uninterpreted_option_upb_array(const google_protobuf_FieldOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(48, 64), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FieldOptions_uninterpreted_option_mutable_upb_array(google_protobuf_FieldOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {999, UPB_SIZE(48, 64), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE void google_protobuf_FieldOptions_set_ctype(google_protobuf_FieldOptions *msg, int32_t value) { + const upb_MiniTableField field = {1, 12, 64, 4, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_set_packed(google_protobuf_FieldOptions *msg, bool value) { + const upb_MiniTableField field = {2, 16, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_set_deprecated(google_protobuf_FieldOptions *msg, bool value) { + const upb_MiniTableField field = {3, 17, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_set_lazy(google_protobuf_FieldOptions *msg, bool value) { + const upb_MiniTableField field = {5, 18, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_set_jstype(google_protobuf_FieldOptions *msg, int32_t value) { + const upb_MiniTableField field = {6, 20, 68, 5, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_set_weak(google_protobuf_FieldOptions *msg, bool value) { + const upb_MiniTableField field = {10, 24, 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_set_unverified_lazy(google_protobuf_FieldOptions *msg, bool value) { + const upb_MiniTableField field = {15, 25, 70, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_set_debug_redact(google_protobuf_FieldOptions *msg, bool value) { + const upb_MiniTableField field = {16, 26, 71, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_set_retention(google_protobuf_FieldOptions *msg, int32_t value) { + const upb_MiniTableField field = {17, 28, 72, 6, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE int32_t* google_protobuf_FieldOptions_mutable_targets(google_protobuf_FieldOptions* msg, size_t* size) { + upb_MiniTableField field = {19, 32, 0, 7, 14, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t*)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE int32_t* google_protobuf_FieldOptions_resize_targets(google_protobuf_FieldOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {19, 32, 0, 7, 14, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (int32_t*)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE bool google_protobuf_FieldOptions_add_targets(google_protobuf_FieldOptions* msg, int32_t val, upb_Arena* arena) { + upb_MiniTableField field = {19, 32, 0, 7, 14, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return false; + } + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &val, sizeof(val)); + return true; +} +UPB_INLINE google_protobuf_FieldOptions_EditionDefault** google_protobuf_FieldOptions_mutable_edition_defaults(google_protobuf_FieldOptions* msg, size_t* size) { + upb_MiniTableField field = {20, UPB_SIZE(36, 40), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_FieldOptions_EditionDefault**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_FieldOptions_EditionDefault** google_protobuf_FieldOptions_resize_edition_defaults(google_protobuf_FieldOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {20, UPB_SIZE(36, 40), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_FieldOptions_EditionDefault**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_FieldOptions_EditionDefault* google_protobuf_FieldOptions_add_edition_defaults(google_protobuf_FieldOptions* msg, upb_Arena* arena) { + upb_MiniTableField field = {20, UPB_SIZE(36, 40), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_FieldOptions_EditionDefault* sub = (struct google_protobuf_FieldOptions_EditionDefault*)_upb_Message_New(&google__protobuf__FieldOptions__EditionDefault_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE void google_protobuf_FieldOptions_set_features(google_protobuf_FieldOptions *msg, google_protobuf_FeatureSet* value) { + const upb_MiniTableField field = {21, UPB_SIZE(40, 48), 73, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FeatureSet* google_protobuf_FieldOptions_mutable_features(google_protobuf_FieldOptions* msg, upb_Arena* arena) { + struct google_protobuf_FeatureSet* sub = (struct google_protobuf_FeatureSet*)google_protobuf_FieldOptions_features(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); + if (sub) google_protobuf_FieldOptions_set_features(msg, sub); + } + return sub; +} +UPB_INLINE void google_protobuf_FieldOptions_set_feature_support(google_protobuf_FieldOptions *msg, google_protobuf_FieldOptions_FeatureSupport* value) { + const upb_MiniTableField field = {22, UPB_SIZE(44, 56), 74, 2, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FieldOptions_FeatureSupport* google_protobuf_FieldOptions_mutable_feature_support(google_protobuf_FieldOptions* msg, upb_Arena* arena) { + struct google_protobuf_FieldOptions_FeatureSupport* sub = (struct google_protobuf_FieldOptions_FeatureSupport*)google_protobuf_FieldOptions_feature_support(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FieldOptions_FeatureSupport*)_upb_Message_New(&google__protobuf__FieldOptions__FeatureSupport_msg_init, arena); + if (sub) google_protobuf_FieldOptions_set_feature_support(msg, sub); + } + return sub; +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_FieldOptions_mutable_uninterpreted_option(google_protobuf_FieldOptions* msg, size_t* size) { + upb_MiniTableField field = {999, UPB_SIZE(48, 64), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_UninterpretedOption**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_FieldOptions_resize_uninterpreted_option(google_protobuf_FieldOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(48, 64), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_UninterpretedOption**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_UninterpretedOption* google_protobuf_FieldOptions_add_uninterpreted_option(google_protobuf_FieldOptions* msg, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(48, 64), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_UninterpretedOption* sub = (struct google_protobuf_UninterpretedOption*)_upb_Message_New(&google__protobuf__UninterpretedOption_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.FieldOptions.EditionDefault */ + +UPB_INLINE google_protobuf_FieldOptions_EditionDefault* google_protobuf_FieldOptions_EditionDefault_new(upb_Arena* arena) { + return (google_protobuf_FieldOptions_EditionDefault*)_upb_Message_New(&google__protobuf__FieldOptions__EditionDefault_msg_init, arena); +} +UPB_INLINE google_protobuf_FieldOptions_EditionDefault* google_protobuf_FieldOptions_EditionDefault_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_FieldOptions_EditionDefault* ret = google_protobuf_FieldOptions_EditionDefault_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FieldOptions__EditionDefault_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_FieldOptions_EditionDefault* google_protobuf_FieldOptions_EditionDefault_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_FieldOptions_EditionDefault* ret = google_protobuf_FieldOptions_EditionDefault_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FieldOptions__EditionDefault_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_FieldOptions_EditionDefault_serialize(const google_protobuf_FieldOptions_EditionDefault* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FieldOptions__EditionDefault_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_FieldOptions_EditionDefault_serialize_ex(const google_protobuf_FieldOptions_EditionDefault* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FieldOptions__EditionDefault_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_FieldOptions_EditionDefault_clear_value(google_protobuf_FieldOptions_EditionDefault* msg) { + const upb_MiniTableField field = {2, 16, 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FieldOptions_EditionDefault_value(const google_protobuf_FieldOptions_EditionDefault* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {2, 16, 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_EditionDefault_has_value(const google_protobuf_FieldOptions_EditionDefault* msg) { + const upb_MiniTableField field = {2, 16, 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_EditionDefault_clear_edition(google_protobuf_FieldOptions_EditionDefault* msg) { + const upb_MiniTableField field = {3, 12, 65, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FieldOptions_EditionDefault_edition(const google_protobuf_FieldOptions_EditionDefault* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {3, 12, 65, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_EditionDefault_has_edition(const google_protobuf_FieldOptions_EditionDefault* msg) { + const upb_MiniTableField field = {3, 12, 65, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_FieldOptions_EditionDefault_set_value(google_protobuf_FieldOptions_EditionDefault *msg, upb_StringView value) { + const upb_MiniTableField field = {2, 16, 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_EditionDefault_set_edition(google_protobuf_FieldOptions_EditionDefault *msg, int32_t value) { + const upb_MiniTableField field = {3, 12, 65, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.FieldOptions.FeatureSupport */ + +UPB_INLINE google_protobuf_FieldOptions_FeatureSupport* google_protobuf_FieldOptions_FeatureSupport_new(upb_Arena* arena) { + return (google_protobuf_FieldOptions_FeatureSupport*)_upb_Message_New(&google__protobuf__FieldOptions__FeatureSupport_msg_init, arena); +} +UPB_INLINE google_protobuf_FieldOptions_FeatureSupport* google_protobuf_FieldOptions_FeatureSupport_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_FieldOptions_FeatureSupport* ret = google_protobuf_FieldOptions_FeatureSupport_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FieldOptions__FeatureSupport_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_FieldOptions_FeatureSupport* google_protobuf_FieldOptions_FeatureSupport_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_FieldOptions_FeatureSupport* ret = google_protobuf_FieldOptions_FeatureSupport_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FieldOptions__FeatureSupport_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_FieldOptions_FeatureSupport_serialize(const google_protobuf_FieldOptions_FeatureSupport* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FieldOptions__FeatureSupport_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_FieldOptions_FeatureSupport_serialize_ex(const google_protobuf_FieldOptions_FeatureSupport* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FieldOptions__FeatureSupport_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_FieldOptions_FeatureSupport_clear_edition_introduced(google_protobuf_FieldOptions_FeatureSupport* msg) { + const upb_MiniTableField field = {1, 12, 64, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FieldOptions_FeatureSupport_edition_introduced(const google_protobuf_FieldOptions_FeatureSupport* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {1, 12, 64, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_FeatureSupport_has_edition_introduced(const google_protobuf_FieldOptions_FeatureSupport* msg) { + const upb_MiniTableField field = {1, 12, 64, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_FeatureSupport_clear_edition_deprecated(google_protobuf_FieldOptions_FeatureSupport* msg) { + const upb_MiniTableField field = {2, 16, 65, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FieldOptions_FeatureSupport_edition_deprecated(const google_protobuf_FieldOptions_FeatureSupport* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {2, 16, 65, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_FeatureSupport_has_edition_deprecated(const google_protobuf_FieldOptions_FeatureSupport* msg) { + const upb_MiniTableField field = {2, 16, 65, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_FeatureSupport_clear_deprecation_warning(google_protobuf_FieldOptions_FeatureSupport* msg) { + const upb_MiniTableField field = {3, 24, 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_FieldOptions_FeatureSupport_deprecation_warning(const google_protobuf_FieldOptions_FeatureSupport* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {3, 24, 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_FeatureSupport_has_deprecation_warning(const google_protobuf_FieldOptions_FeatureSupport* msg) { + const upb_MiniTableField field = {3, 24, 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FieldOptions_FeatureSupport_clear_edition_removed(google_protobuf_FieldOptions_FeatureSupport* msg) { + const upb_MiniTableField field = {4, 20, 67, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FieldOptions_FeatureSupport_edition_removed(const google_protobuf_FieldOptions_FeatureSupport* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {4, 20, 67, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FieldOptions_FeatureSupport_has_edition_removed(const google_protobuf_FieldOptions_FeatureSupport* msg) { + const upb_MiniTableField field = {4, 20, 67, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_FieldOptions_FeatureSupport_set_edition_introduced(google_protobuf_FieldOptions_FeatureSupport *msg, int32_t value) { + const upb_MiniTableField field = {1, 12, 64, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_FeatureSupport_set_edition_deprecated(google_protobuf_FieldOptions_FeatureSupport *msg, int32_t value) { + const upb_MiniTableField field = {2, 16, 65, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_FeatureSupport_set_deprecation_warning(google_protobuf_FieldOptions_FeatureSupport *msg, upb_StringView value) { + const upb_MiniTableField field = {3, 24, 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FieldOptions_FeatureSupport_set_edition_removed(google_protobuf_FieldOptions_FeatureSupport *msg, int32_t value) { + const upb_MiniTableField field = {4, 20, 67, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.OneofOptions */ + +UPB_INLINE google_protobuf_OneofOptions* google_protobuf_OneofOptions_new(upb_Arena* arena) { + return (google_protobuf_OneofOptions*)_upb_Message_New(&google__protobuf__OneofOptions_msg_init, arena); +} +UPB_INLINE google_protobuf_OneofOptions* google_protobuf_OneofOptions_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_OneofOptions* ret = google_protobuf_OneofOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__OneofOptions_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_OneofOptions* google_protobuf_OneofOptions_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_OneofOptions* ret = google_protobuf_OneofOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__OneofOptions_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_OneofOptions_serialize(const google_protobuf_OneofOptions* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__OneofOptions_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_OneofOptions_serialize_ex(const google_protobuf_OneofOptions* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__OneofOptions_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_OneofOptions_clear_features(google_protobuf_OneofOptions* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 64, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSet* google_protobuf_OneofOptions_features(const google_protobuf_OneofOptions* msg) { + const google_protobuf_FeatureSet* default_val = NULL; + const google_protobuf_FeatureSet* ret; + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 64, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_OneofOptions_has_features(const google_protobuf_OneofOptions* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 64, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_OneofOptions_clear_uninterpreted_option(google_protobuf_OneofOptions* msg) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_UninterpretedOption* const* google_protobuf_OneofOptions_uninterpreted_option(const google_protobuf_OneofOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_UninterpretedOption* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_OneofOptions_uninterpreted_option_upb_array(const google_protobuf_OneofOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_OneofOptions_uninterpreted_option_mutable_upb_array(google_protobuf_OneofOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE void google_protobuf_OneofOptions_set_features(google_protobuf_OneofOptions *msg, google_protobuf_FeatureSet* value) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 64, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FeatureSet* google_protobuf_OneofOptions_mutable_features(google_protobuf_OneofOptions* msg, upb_Arena* arena) { + struct google_protobuf_FeatureSet* sub = (struct google_protobuf_FeatureSet*)google_protobuf_OneofOptions_features(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); + if (sub) google_protobuf_OneofOptions_set_features(msg, sub); + } + return sub; +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_OneofOptions_mutable_uninterpreted_option(google_protobuf_OneofOptions* msg, size_t* size) { + upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_UninterpretedOption**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_OneofOptions_resize_uninterpreted_option(google_protobuf_OneofOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_UninterpretedOption**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_UninterpretedOption* google_protobuf_OneofOptions_add_uninterpreted_option(google_protobuf_OneofOptions* msg, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_UninterpretedOption* sub = (struct google_protobuf_UninterpretedOption*)_upb_Message_New(&google__protobuf__UninterpretedOption_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.EnumOptions */ + +UPB_INLINE google_protobuf_EnumOptions* google_protobuf_EnumOptions_new(upb_Arena* arena) { + return (google_protobuf_EnumOptions*)_upb_Message_New(&google__protobuf__EnumOptions_msg_init, arena); +} +UPB_INLINE google_protobuf_EnumOptions* google_protobuf_EnumOptions_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_EnumOptions* ret = google_protobuf_EnumOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__EnumOptions_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_EnumOptions* google_protobuf_EnumOptions_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_EnumOptions* ret = google_protobuf_EnumOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__EnumOptions_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_EnumOptions_serialize(const google_protobuf_EnumOptions* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__EnumOptions_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_EnumOptions_serialize_ex(const google_protobuf_EnumOptions* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__EnumOptions_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_EnumOptions_clear_allow_alias(google_protobuf_EnumOptions* msg) { + const upb_MiniTableField field = {2, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_EnumOptions_allow_alias(const google_protobuf_EnumOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {2, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumOptions_has_allow_alias(const google_protobuf_EnumOptions* msg) { + const upb_MiniTableField field = {2, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumOptions_clear_deprecated(google_protobuf_EnumOptions* msg) { + const upb_MiniTableField field = {3, 10, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_EnumOptions_deprecated(const google_protobuf_EnumOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {3, 10, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumOptions_has_deprecated(const google_protobuf_EnumOptions* msg) { + const upb_MiniTableField field = {3, 10, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumOptions_clear_deprecated_legacy_json_field_conflicts(google_protobuf_EnumOptions* msg) { + const upb_MiniTableField field = {6, 11, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_EnumOptions_deprecated_legacy_json_field_conflicts(const google_protobuf_EnumOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {6, 11, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumOptions_has_deprecated_legacy_json_field_conflicts(const google_protobuf_EnumOptions* msg) { + const upb_MiniTableField field = {6, 11, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumOptions_clear_features(google_protobuf_EnumOptions* msg) { + const upb_MiniTableField field = {7, UPB_SIZE(12, 16), 67, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSet* google_protobuf_EnumOptions_features(const google_protobuf_EnumOptions* msg) { + const google_protobuf_FeatureSet* default_val = NULL; + const google_protobuf_FeatureSet* ret; + const upb_MiniTableField field = {7, UPB_SIZE(12, 16), 67, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumOptions_has_features(const google_protobuf_EnumOptions* msg) { + const upb_MiniTableField field = {7, UPB_SIZE(12, 16), 67, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumOptions_clear_uninterpreted_option(google_protobuf_EnumOptions* msg) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_UninterpretedOption* const* google_protobuf_EnumOptions_uninterpreted_option(const google_protobuf_EnumOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_UninterpretedOption* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_EnumOptions_uninterpreted_option_upb_array(const google_protobuf_EnumOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_EnumOptions_uninterpreted_option_mutable_upb_array(google_protobuf_EnumOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE void google_protobuf_EnumOptions_set_allow_alias(google_protobuf_EnumOptions *msg, bool value) { + const upb_MiniTableField field = {2, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_EnumOptions_set_deprecated(google_protobuf_EnumOptions *msg, bool value) { + const upb_MiniTableField field = {3, 10, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_EnumOptions_set_deprecated_legacy_json_field_conflicts(google_protobuf_EnumOptions *msg, bool value) { + const upb_MiniTableField field = {6, 11, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_EnumOptions_set_features(google_protobuf_EnumOptions *msg, google_protobuf_FeatureSet* value) { + const upb_MiniTableField field = {7, UPB_SIZE(12, 16), 67, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FeatureSet* google_protobuf_EnumOptions_mutable_features(google_protobuf_EnumOptions* msg, upb_Arena* arena) { + struct google_protobuf_FeatureSet* sub = (struct google_protobuf_FeatureSet*)google_protobuf_EnumOptions_features(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); + if (sub) google_protobuf_EnumOptions_set_features(msg, sub); + } + return sub; +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_EnumOptions_mutable_uninterpreted_option(google_protobuf_EnumOptions* msg, size_t* size) { + upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_UninterpretedOption**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_EnumOptions_resize_uninterpreted_option(google_protobuf_EnumOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_UninterpretedOption**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_UninterpretedOption* google_protobuf_EnumOptions_add_uninterpreted_option(google_protobuf_EnumOptions* msg, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_UninterpretedOption* sub = (struct google_protobuf_UninterpretedOption*)_upb_Message_New(&google__protobuf__UninterpretedOption_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.EnumValueOptions */ + +UPB_INLINE google_protobuf_EnumValueOptions* google_protobuf_EnumValueOptions_new(upb_Arena* arena) { + return (google_protobuf_EnumValueOptions*)_upb_Message_New(&google__protobuf__EnumValueOptions_msg_init, arena); +} +UPB_INLINE google_protobuf_EnumValueOptions* google_protobuf_EnumValueOptions_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_EnumValueOptions* ret = google_protobuf_EnumValueOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__EnumValueOptions_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_EnumValueOptions* google_protobuf_EnumValueOptions_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_EnumValueOptions* ret = google_protobuf_EnumValueOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__EnumValueOptions_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_EnumValueOptions_serialize(const google_protobuf_EnumValueOptions* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__EnumValueOptions_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_EnumValueOptions_serialize_ex(const google_protobuf_EnumValueOptions* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__EnumValueOptions_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_EnumValueOptions_clear_deprecated(google_protobuf_EnumValueOptions* msg) { + const upb_MiniTableField field = {1, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_EnumValueOptions_deprecated(const google_protobuf_EnumValueOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {1, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumValueOptions_has_deprecated(const google_protobuf_EnumValueOptions* msg) { + const upb_MiniTableField field = {1, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumValueOptions_clear_features(google_protobuf_EnumValueOptions* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSet* google_protobuf_EnumValueOptions_features(const google_protobuf_EnumValueOptions* msg) { + const google_protobuf_FeatureSet* default_val = NULL; + const google_protobuf_FeatureSet* ret; + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumValueOptions_has_features(const google_protobuf_EnumValueOptions* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumValueOptions_clear_debug_redact(google_protobuf_EnumValueOptions* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 10), 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_EnumValueOptions_debug_redact(const google_protobuf_EnumValueOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {3, UPB_SIZE(16, 10), 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumValueOptions_has_debug_redact(const google_protobuf_EnumValueOptions* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 10), 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumValueOptions_clear_feature_support(google_protobuf_EnumValueOptions* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 24), 67, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FieldOptions_FeatureSupport* google_protobuf_EnumValueOptions_feature_support(const google_protobuf_EnumValueOptions* msg) { + const google_protobuf_FieldOptions_FeatureSupport* default_val = NULL; + const google_protobuf_FieldOptions_FeatureSupport* ret; + const upb_MiniTableField field = {4, UPB_SIZE(20, 24), 67, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_EnumValueOptions_has_feature_support(const google_protobuf_EnumValueOptions* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 24), 67, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_EnumValueOptions_clear_uninterpreted_option(google_protobuf_EnumValueOptions* msg) { + const upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_UninterpretedOption* const* google_protobuf_EnumValueOptions_uninterpreted_option(const google_protobuf_EnumValueOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_UninterpretedOption* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_EnumValueOptions_uninterpreted_option_upb_array(const google_protobuf_EnumValueOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_EnumValueOptions_uninterpreted_option_mutable_upb_array(google_protobuf_EnumValueOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE void google_protobuf_EnumValueOptions_set_deprecated(google_protobuf_EnumValueOptions *msg, bool value) { + const upb_MiniTableField field = {1, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_EnumValueOptions_set_features(google_protobuf_EnumValueOptions *msg, google_protobuf_FeatureSet* value) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FeatureSet* google_protobuf_EnumValueOptions_mutable_features(google_protobuf_EnumValueOptions* msg, upb_Arena* arena) { + struct google_protobuf_FeatureSet* sub = (struct google_protobuf_FeatureSet*)google_protobuf_EnumValueOptions_features(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); + if (sub) google_protobuf_EnumValueOptions_set_features(msg, sub); + } + return sub; +} +UPB_INLINE void google_protobuf_EnumValueOptions_set_debug_redact(google_protobuf_EnumValueOptions *msg, bool value) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 10), 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_EnumValueOptions_set_feature_support(google_protobuf_EnumValueOptions *msg, google_protobuf_FieldOptions_FeatureSupport* value) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 24), 67, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FieldOptions_FeatureSupport* google_protobuf_EnumValueOptions_mutable_feature_support(google_protobuf_EnumValueOptions* msg, upb_Arena* arena) { + struct google_protobuf_FieldOptions_FeatureSupport* sub = (struct google_protobuf_FieldOptions_FeatureSupport*)google_protobuf_EnumValueOptions_feature_support(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FieldOptions_FeatureSupport*)_upb_Message_New(&google__protobuf__FieldOptions__FeatureSupport_msg_init, arena); + if (sub) google_protobuf_EnumValueOptions_set_feature_support(msg, sub); + } + return sub; +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_EnumValueOptions_mutable_uninterpreted_option(google_protobuf_EnumValueOptions* msg, size_t* size) { + upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_UninterpretedOption**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_EnumValueOptions_resize_uninterpreted_option(google_protobuf_EnumValueOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_UninterpretedOption**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_UninterpretedOption* google_protobuf_EnumValueOptions_add_uninterpreted_option(google_protobuf_EnumValueOptions* msg, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_UninterpretedOption* sub = (struct google_protobuf_UninterpretedOption*)_upb_Message_New(&google__protobuf__UninterpretedOption_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.ServiceOptions */ + +UPB_INLINE google_protobuf_ServiceOptions* google_protobuf_ServiceOptions_new(upb_Arena* arena) { + return (google_protobuf_ServiceOptions*)_upb_Message_New(&google__protobuf__ServiceOptions_msg_init, arena); +} +UPB_INLINE google_protobuf_ServiceOptions* google_protobuf_ServiceOptions_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_ServiceOptions* ret = google_protobuf_ServiceOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__ServiceOptions_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_ServiceOptions* google_protobuf_ServiceOptions_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_ServiceOptions* ret = google_protobuf_ServiceOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__ServiceOptions_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_ServiceOptions_serialize(const google_protobuf_ServiceOptions* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__ServiceOptions_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_ServiceOptions_serialize_ex(const google_protobuf_ServiceOptions* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__ServiceOptions_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_ServiceOptions_clear_deprecated(google_protobuf_ServiceOptions* msg) { + const upb_MiniTableField field = {33, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_ServiceOptions_deprecated(const google_protobuf_ServiceOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {33, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_ServiceOptions_has_deprecated(const google_protobuf_ServiceOptions* msg) { + const upb_MiniTableField field = {33, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_ServiceOptions_clear_features(google_protobuf_ServiceOptions* msg) { + const upb_MiniTableField field = {34, UPB_SIZE(12, 16), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSet* google_protobuf_ServiceOptions_features(const google_protobuf_ServiceOptions* msg) { + const google_protobuf_FeatureSet* default_val = NULL; + const google_protobuf_FeatureSet* ret; + const upb_MiniTableField field = {34, UPB_SIZE(12, 16), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_ServiceOptions_has_features(const google_protobuf_ServiceOptions* msg) { + const upb_MiniTableField field = {34, UPB_SIZE(12, 16), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_ServiceOptions_clear_uninterpreted_option(google_protobuf_ServiceOptions* msg) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_UninterpretedOption* const* google_protobuf_ServiceOptions_uninterpreted_option(const google_protobuf_ServiceOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_UninterpretedOption* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_ServiceOptions_uninterpreted_option_upb_array(const google_protobuf_ServiceOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_ServiceOptions_uninterpreted_option_mutable_upb_array(google_protobuf_ServiceOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE void google_protobuf_ServiceOptions_set_deprecated(google_protobuf_ServiceOptions *msg, bool value) { + const upb_MiniTableField field = {33, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_ServiceOptions_set_features(google_protobuf_ServiceOptions *msg, google_protobuf_FeatureSet* value) { + const upb_MiniTableField field = {34, UPB_SIZE(12, 16), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FeatureSet* google_protobuf_ServiceOptions_mutable_features(google_protobuf_ServiceOptions* msg, upb_Arena* arena) { + struct google_protobuf_FeatureSet* sub = (struct google_protobuf_FeatureSet*)google_protobuf_ServiceOptions_features(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); + if (sub) google_protobuf_ServiceOptions_set_features(msg, sub); + } + return sub; +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_ServiceOptions_mutable_uninterpreted_option(google_protobuf_ServiceOptions* msg, size_t* size) { + upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_UninterpretedOption**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_ServiceOptions_resize_uninterpreted_option(google_protobuf_ServiceOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_UninterpretedOption**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_UninterpretedOption* google_protobuf_ServiceOptions_add_uninterpreted_option(google_protobuf_ServiceOptions* msg, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_UninterpretedOption* sub = (struct google_protobuf_UninterpretedOption*)_upb_Message_New(&google__protobuf__UninterpretedOption_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.MethodOptions */ + +UPB_INLINE google_protobuf_MethodOptions* google_protobuf_MethodOptions_new(upb_Arena* arena) { + return (google_protobuf_MethodOptions*)_upb_Message_New(&google__protobuf__MethodOptions_msg_init, arena); +} +UPB_INLINE google_protobuf_MethodOptions* google_protobuf_MethodOptions_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_MethodOptions* ret = google_protobuf_MethodOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__MethodOptions_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_MethodOptions* google_protobuf_MethodOptions_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_MethodOptions* ret = google_protobuf_MethodOptions_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__MethodOptions_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_MethodOptions_serialize(const google_protobuf_MethodOptions* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__MethodOptions_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_MethodOptions_serialize_ex(const google_protobuf_MethodOptions* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__MethodOptions_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_MethodOptions_clear_deprecated(google_protobuf_MethodOptions* msg) { + const upb_MiniTableField field = {33, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_MethodOptions_deprecated(const google_protobuf_MethodOptions* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {33, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MethodOptions_has_deprecated(const google_protobuf_MethodOptions* msg) { + const upb_MiniTableField field = {33, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MethodOptions_clear_idempotency_level(google_protobuf_MethodOptions* msg) { + const upb_MiniTableField field = {34, 12, 65, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_MethodOptions_idempotency_level(const google_protobuf_MethodOptions* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {34, 12, 65, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MethodOptions_has_idempotency_level(const google_protobuf_MethodOptions* msg) { + const upb_MiniTableField field = {34, 12, 65, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MethodOptions_clear_features(google_protobuf_MethodOptions* msg) { + const upb_MiniTableField field = {35, 16, 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSet* google_protobuf_MethodOptions_features(const google_protobuf_MethodOptions* msg) { + const google_protobuf_FeatureSet* default_val = NULL; + const google_protobuf_FeatureSet* ret; + const upb_MiniTableField field = {35, 16, 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_MethodOptions_has_features(const google_protobuf_MethodOptions* msg) { + const upb_MiniTableField field = {35, 16, 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_MethodOptions_clear_uninterpreted_option(google_protobuf_MethodOptions* msg) { + const upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_UninterpretedOption* const* google_protobuf_MethodOptions_uninterpreted_option(const google_protobuf_MethodOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_UninterpretedOption* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_MethodOptions_uninterpreted_option_upb_array(const google_protobuf_MethodOptions* msg, size_t* size) { + const upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_MethodOptions_uninterpreted_option_mutable_upb_array(google_protobuf_MethodOptions* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE void google_protobuf_MethodOptions_set_deprecated(google_protobuf_MethodOptions *msg, bool value) { + const upb_MiniTableField field = {33, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_MethodOptions_set_idempotency_level(google_protobuf_MethodOptions *msg, int32_t value) { + const upb_MiniTableField field = {34, 12, 65, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_MethodOptions_set_features(google_protobuf_MethodOptions *msg, google_protobuf_FeatureSet* value) { + const upb_MiniTableField field = {35, 16, 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FeatureSet* google_protobuf_MethodOptions_mutable_features(google_protobuf_MethodOptions* msg, upb_Arena* arena) { + struct google_protobuf_FeatureSet* sub = (struct google_protobuf_FeatureSet*)google_protobuf_MethodOptions_features(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); + if (sub) google_protobuf_MethodOptions_set_features(msg, sub); + } + return sub; +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_MethodOptions_mutable_uninterpreted_option(google_protobuf_MethodOptions* msg, size_t* size) { + upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_UninterpretedOption**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_UninterpretedOption** google_protobuf_MethodOptions_resize_uninterpreted_option(google_protobuf_MethodOptions* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_UninterpretedOption**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_UninterpretedOption* google_protobuf_MethodOptions_add_uninterpreted_option(google_protobuf_MethodOptions* msg, upb_Arena* arena) { + upb_MiniTableField field = {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_UninterpretedOption* sub = (struct google_protobuf_UninterpretedOption*)_upb_Message_New(&google__protobuf__UninterpretedOption_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.UninterpretedOption */ + +UPB_INLINE google_protobuf_UninterpretedOption* google_protobuf_UninterpretedOption_new(upb_Arena* arena) { + return (google_protobuf_UninterpretedOption*)_upb_Message_New(&google__protobuf__UninterpretedOption_msg_init, arena); +} +UPB_INLINE google_protobuf_UninterpretedOption* google_protobuf_UninterpretedOption_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_UninterpretedOption* ret = google_protobuf_UninterpretedOption_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__UninterpretedOption_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_UninterpretedOption* google_protobuf_UninterpretedOption_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_UninterpretedOption* ret = google_protobuf_UninterpretedOption_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__UninterpretedOption_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_UninterpretedOption_serialize(const google_protobuf_UninterpretedOption* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__UninterpretedOption_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_UninterpretedOption_serialize_ex(const google_protobuf_UninterpretedOption* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__UninterpretedOption_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_UninterpretedOption_clear_name(google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_UninterpretedOption_NamePart* const* google_protobuf_UninterpretedOption_name(const google_protobuf_UninterpretedOption* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_UninterpretedOption_NamePart* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_UninterpretedOption_name_upb_array(const google_protobuf_UninterpretedOption* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_UninterpretedOption_name_mutable_upb_array(google_protobuf_UninterpretedOption* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_UninterpretedOption_clear_identifier_value(google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 24), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_UninterpretedOption_identifier_value(const google_protobuf_UninterpretedOption* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {3, UPB_SIZE(16, 24), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_UninterpretedOption_has_identifier_value(const google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 24), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_UninterpretedOption_clear_positive_int_value(google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(24, 40), 65, kUpb_NoSub, 4, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE uint64_t google_protobuf_UninterpretedOption_positive_int_value(const google_protobuf_UninterpretedOption* msg) { + uint64_t default_val = (uint64_t)0ull; + uint64_t ret; + const upb_MiniTableField field = {4, UPB_SIZE(24, 40), 65, kUpb_NoSub, 4, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_UninterpretedOption_has_positive_int_value(const google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(24, 40), 65, kUpb_NoSub, 4, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_UninterpretedOption_clear_negative_int_value(google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(32, 48), 66, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int64_t google_protobuf_UninterpretedOption_negative_int_value(const google_protobuf_UninterpretedOption* msg) { + int64_t default_val = (int64_t)0ll; + int64_t ret; + const upb_MiniTableField field = {5, UPB_SIZE(32, 48), 66, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_UninterpretedOption_has_negative_int_value(const google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(32, 48), 66, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_UninterpretedOption_clear_double_value(google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {6, UPB_SIZE(40, 56), 67, kUpb_NoSub, 1, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE double google_protobuf_UninterpretedOption_double_value(const google_protobuf_UninterpretedOption* msg) { + double default_val = 0; + double ret; + const upb_MiniTableField field = {6, UPB_SIZE(40, 56), 67, kUpb_NoSub, 1, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_UninterpretedOption_has_double_value(const google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {6, UPB_SIZE(40, 56), 67, kUpb_NoSub, 1, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_UninterpretedOption_clear_string_value(google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {7, UPB_SIZE(48, 64), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_UninterpretedOption_string_value(const google_protobuf_UninterpretedOption* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {7, UPB_SIZE(48, 64), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_UninterpretedOption_has_string_value(const google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {7, UPB_SIZE(48, 64), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_UninterpretedOption_clear_aggregate_value(google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {8, UPB_SIZE(56, 80), 69, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_UninterpretedOption_aggregate_value(const google_protobuf_UninterpretedOption* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {8, UPB_SIZE(56, 80), 69, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_UninterpretedOption_has_aggregate_value(const google_protobuf_UninterpretedOption* msg) { + const upb_MiniTableField field = {8, UPB_SIZE(56, 80), 69, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE google_protobuf_UninterpretedOption_NamePart** google_protobuf_UninterpretedOption_mutable_name(google_protobuf_UninterpretedOption* msg, size_t* size) { + upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_UninterpretedOption_NamePart**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_UninterpretedOption_NamePart** google_protobuf_UninterpretedOption_resize_name(google_protobuf_UninterpretedOption* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_UninterpretedOption_NamePart**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_UninterpretedOption_NamePart* google_protobuf_UninterpretedOption_add_name(google_protobuf_UninterpretedOption* msg, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_UninterpretedOption_NamePart* sub = (struct google_protobuf_UninterpretedOption_NamePart*)_upb_Message_New(&google__protobuf__UninterpretedOption__NamePart_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE void google_protobuf_UninterpretedOption_set_identifier_value(google_protobuf_UninterpretedOption *msg, upb_StringView value) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 24), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_UninterpretedOption_set_positive_int_value(google_protobuf_UninterpretedOption *msg, uint64_t value) { + const upb_MiniTableField field = {4, UPB_SIZE(24, 40), 65, kUpb_NoSub, 4, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_UninterpretedOption_set_negative_int_value(google_protobuf_UninterpretedOption *msg, int64_t value) { + const upb_MiniTableField field = {5, UPB_SIZE(32, 48), 66, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_UninterpretedOption_set_double_value(google_protobuf_UninterpretedOption *msg, double value) { + const upb_MiniTableField field = {6, UPB_SIZE(40, 56), 67, kUpb_NoSub, 1, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_UninterpretedOption_set_string_value(google_protobuf_UninterpretedOption *msg, upb_StringView value) { + const upb_MiniTableField field = {7, UPB_SIZE(48, 64), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_UninterpretedOption_set_aggregate_value(google_protobuf_UninterpretedOption *msg, upb_StringView value) { + const upb_MiniTableField field = {8, UPB_SIZE(56, 80), 69, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.UninterpretedOption.NamePart */ + +UPB_INLINE google_protobuf_UninterpretedOption_NamePart* google_protobuf_UninterpretedOption_NamePart_new(upb_Arena* arena) { + return (google_protobuf_UninterpretedOption_NamePart*)_upb_Message_New(&google__protobuf__UninterpretedOption__NamePart_msg_init, arena); +} +UPB_INLINE google_protobuf_UninterpretedOption_NamePart* google_protobuf_UninterpretedOption_NamePart_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_UninterpretedOption_NamePart* ret = google_protobuf_UninterpretedOption_NamePart_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__UninterpretedOption__NamePart_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_UninterpretedOption_NamePart* google_protobuf_UninterpretedOption_NamePart_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_UninterpretedOption_NamePart* ret = google_protobuf_UninterpretedOption_NamePart_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__UninterpretedOption__NamePart_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_UninterpretedOption_NamePart_serialize(const google_protobuf_UninterpretedOption_NamePart* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__UninterpretedOption__NamePart_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_UninterpretedOption_NamePart_serialize_ex(const google_protobuf_UninterpretedOption_NamePart* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__UninterpretedOption__NamePart_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_UninterpretedOption_NamePart_clear_name_part(google_protobuf_UninterpretedOption_NamePart* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_UninterpretedOption_NamePart_name_part(const google_protobuf_UninterpretedOption_NamePart* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_UninterpretedOption_NamePart_has_name_part(const google_protobuf_UninterpretedOption_NamePart* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_UninterpretedOption_NamePart_clear_is_extension(google_protobuf_UninterpretedOption_NamePart* msg) { + const upb_MiniTableField field = {2, 9, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE bool google_protobuf_UninterpretedOption_NamePart_is_extension(const google_protobuf_UninterpretedOption_NamePart* msg) { + bool default_val = false; + bool ret; + const upb_MiniTableField field = {2, 9, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_UninterpretedOption_NamePart_has_is_extension(const google_protobuf_UninterpretedOption_NamePart* msg) { + const upb_MiniTableField field = {2, 9, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_UninterpretedOption_NamePart_set_name_part(google_protobuf_UninterpretedOption_NamePart *msg, upb_StringView value) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_UninterpretedOption_NamePart_set_is_extension(google_protobuf_UninterpretedOption_NamePart *msg, bool value) { + const upb_MiniTableField field = {2, 9, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.FeatureSet */ + +UPB_INLINE google_protobuf_FeatureSet* google_protobuf_FeatureSet_new(upb_Arena* arena) { + return (google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); +} +UPB_INLINE google_protobuf_FeatureSet* google_protobuf_FeatureSet_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_FeatureSet* ret = google_protobuf_FeatureSet_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FeatureSet_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_FeatureSet* google_protobuf_FeatureSet_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_FeatureSet* ret = google_protobuf_FeatureSet_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FeatureSet_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_FeatureSet_serialize(const google_protobuf_FeatureSet* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FeatureSet_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_FeatureSet_serialize_ex(const google_protobuf_FeatureSet* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FeatureSet_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_FeatureSet_clear_field_presence(google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {1, 12, 64, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FeatureSet_field_presence(const google_protobuf_FeatureSet* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {1, 12, 64, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FeatureSet_has_field_presence(const google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {1, 12, 64, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FeatureSet_clear_enum_type(google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {2, 16, 65, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FeatureSet_enum_type(const google_protobuf_FeatureSet* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {2, 16, 65, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FeatureSet_has_enum_type(const google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {2, 16, 65, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FeatureSet_clear_repeated_field_encoding(google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {3, 20, 66, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FeatureSet_repeated_field_encoding(const google_protobuf_FeatureSet* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {3, 20, 66, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FeatureSet_has_repeated_field_encoding(const google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {3, 20, 66, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FeatureSet_clear_utf8_validation(google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {4, 24, 67, 3, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FeatureSet_utf8_validation(const google_protobuf_FeatureSet* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {4, 24, 67, 3, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FeatureSet_has_utf8_validation(const google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {4, 24, 67, 3, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FeatureSet_clear_message_encoding(google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {5, 28, 68, 4, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FeatureSet_message_encoding(const google_protobuf_FeatureSet* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {5, 28, 68, 4, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FeatureSet_has_message_encoding(const google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {5, 28, 68, 4, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FeatureSet_clear_json_format(google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {6, 32, 69, 5, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FeatureSet_json_format(const google_protobuf_FeatureSet* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {6, 32, 69, 5, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FeatureSet_has_json_format(const google_protobuf_FeatureSet* msg) { + const upb_MiniTableField field = {6, 32, 69, 5, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_FeatureSet_set_field_presence(google_protobuf_FeatureSet *msg, int32_t value) { + const upb_MiniTableField field = {1, 12, 64, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FeatureSet_set_enum_type(google_protobuf_FeatureSet *msg, int32_t value) { + const upb_MiniTableField field = {2, 16, 65, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FeatureSet_set_repeated_field_encoding(google_protobuf_FeatureSet *msg, int32_t value) { + const upb_MiniTableField field = {3, 20, 66, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FeatureSet_set_utf8_validation(google_protobuf_FeatureSet *msg, int32_t value) { + const upb_MiniTableField field = {4, 24, 67, 3, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FeatureSet_set_message_encoding(google_protobuf_FeatureSet *msg, int32_t value) { + const upb_MiniTableField field = {5, 28, 68, 4, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FeatureSet_set_json_format(google_protobuf_FeatureSet *msg, int32_t value) { + const upb_MiniTableField field = {6, 32, 69, 5, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.FeatureSetDefaults */ + +UPB_INLINE google_protobuf_FeatureSetDefaults* google_protobuf_FeatureSetDefaults_new(upb_Arena* arena) { + return (google_protobuf_FeatureSetDefaults*)_upb_Message_New(&google__protobuf__FeatureSetDefaults_msg_init, arena); +} +UPB_INLINE google_protobuf_FeatureSetDefaults* google_protobuf_FeatureSetDefaults_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_FeatureSetDefaults* ret = google_protobuf_FeatureSetDefaults_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FeatureSetDefaults_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_FeatureSetDefaults* google_protobuf_FeatureSetDefaults_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_FeatureSetDefaults* ret = google_protobuf_FeatureSetDefaults_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FeatureSetDefaults_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_FeatureSetDefaults_serialize(const google_protobuf_FeatureSetDefaults* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FeatureSetDefaults_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_FeatureSetDefaults_serialize_ex(const google_protobuf_FeatureSetDefaults* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FeatureSetDefaults_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_FeatureSetDefaults_clear_defaults(google_protobuf_FeatureSetDefaults* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* const* google_protobuf_FeatureSetDefaults_defaults(const google_protobuf_FeatureSetDefaults* msg, size_t* size) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_FeatureSetDefaults_defaults_upb_array(const google_protobuf_FeatureSetDefaults* msg, size_t* size) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_FeatureSetDefaults_defaults_mutable_upb_array(google_protobuf_FeatureSetDefaults* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_FeatureSetDefaults_clear_minimum_edition(google_protobuf_FeatureSetDefaults* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(16, 12), 64, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FeatureSetDefaults_minimum_edition(const google_protobuf_FeatureSetDefaults* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {4, UPB_SIZE(16, 12), 64, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FeatureSetDefaults_has_minimum_edition(const google_protobuf_FeatureSetDefaults* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(16, 12), 64, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FeatureSetDefaults_clear_maximum_edition(google_protobuf_FeatureSetDefaults* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(20, 16), 65, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FeatureSetDefaults_maximum_edition(const google_protobuf_FeatureSetDefaults* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {5, UPB_SIZE(20, 16), 65, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FeatureSetDefaults_has_maximum_edition(const google_protobuf_FeatureSetDefaults* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(20, 16), 65, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault** google_protobuf_FeatureSetDefaults_mutable_defaults(google_protobuf_FeatureSetDefaults* msg, size_t* size) { + upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault** google_protobuf_FeatureSetDefaults_resize_defaults(google_protobuf_FeatureSetDefaults* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* google_protobuf_FeatureSetDefaults_add_defaults(google_protobuf_FeatureSetDefaults* msg, upb_Arena* arena) { + upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* sub = (struct google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault*)_upb_Message_New(&google__protobuf__FeatureSetDefaults__FeatureSetEditionDefault_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} +UPB_INLINE void google_protobuf_FeatureSetDefaults_set_minimum_edition(google_protobuf_FeatureSetDefaults *msg, int32_t value) { + const upb_MiniTableField field = {4, UPB_SIZE(16, 12), 64, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FeatureSetDefaults_set_maximum_edition(google_protobuf_FeatureSetDefaults *msg, int32_t value) { + const upb_MiniTableField field = {5, UPB_SIZE(20, 16), 65, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault */ + +UPB_INLINE google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_new(upb_Arena* arena) { + return (google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault*)_upb_Message_New(&google__protobuf__FeatureSetDefaults__FeatureSetEditionDefault_msg_init, arena); +} +UPB_INLINE google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* ret = google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FeatureSetDefaults__FeatureSetEditionDefault_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* ret = google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__FeatureSetDefaults__FeatureSetEditionDefault_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_serialize(const google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FeatureSetDefaults__FeatureSetEditionDefault_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_serialize_ex(const google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__FeatureSetDefaults__FeatureSetEditionDefault_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_clear_edition(google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg) { + const upb_MiniTableField field = {3, 12, 64, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_edition(const google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {3, 12, 64, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_has_edition(const google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg) { + const upb_MiniTableField field = {3, 12, 64, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_clear_overridable_features(google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg) { + const upb_MiniTableField field = {4, 16, 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSet* google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_overridable_features(const google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg) { + const google_protobuf_FeatureSet* default_val = NULL; + const google_protobuf_FeatureSet* ret; + const upb_MiniTableField field = {4, 16, 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_has_overridable_features(const google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg) { + const upb_MiniTableField field = {4, 16, 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_clear_fixed_features(google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(20, 24), 66, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_FeatureSet* google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_fixed_features(const google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg) { + const google_protobuf_FeatureSet* default_val = NULL; + const google_protobuf_FeatureSet* ret; + const upb_MiniTableField field = {5, UPB_SIZE(20, 24), 66, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_has_fixed_features(const google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(20, 24), 66, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE void google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_set_edition(google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault *msg, int32_t value) { + const upb_MiniTableField field = {3, 12, 64, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_set_overridable_features(google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault *msg, google_protobuf_FeatureSet* value) { + const upb_MiniTableField field = {4, 16, 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FeatureSet* google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_mutable_overridable_features(google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg, upb_Arena* arena) { + struct google_protobuf_FeatureSet* sub = (struct google_protobuf_FeatureSet*)google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_overridable_features(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); + if (sub) google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_set_overridable_features(msg, sub); + } + return sub; +} +UPB_INLINE void google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_set_fixed_features(google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault *msg, google_protobuf_FeatureSet* value) { + const upb_MiniTableField field = {5, UPB_SIZE(20, 24), 66, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE struct google_protobuf_FeatureSet* google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_mutable_fixed_features(google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault* msg, upb_Arena* arena) { + struct google_protobuf_FeatureSet* sub = (struct google_protobuf_FeatureSet*)google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_fixed_features(msg); + if (sub == NULL) { + sub = (struct google_protobuf_FeatureSet*)_upb_Message_New(&google__protobuf__FeatureSet_msg_init, arena); + if (sub) google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_set_fixed_features(msg, sub); + } + return sub; +} + +/* google.protobuf.SourceCodeInfo */ + +UPB_INLINE google_protobuf_SourceCodeInfo* google_protobuf_SourceCodeInfo_new(upb_Arena* arena) { + return (google_protobuf_SourceCodeInfo*)_upb_Message_New(&google__protobuf__SourceCodeInfo_msg_init, arena); +} +UPB_INLINE google_protobuf_SourceCodeInfo* google_protobuf_SourceCodeInfo_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_SourceCodeInfo* ret = google_protobuf_SourceCodeInfo_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__SourceCodeInfo_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_SourceCodeInfo* google_protobuf_SourceCodeInfo_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_SourceCodeInfo* ret = google_protobuf_SourceCodeInfo_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__SourceCodeInfo_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_SourceCodeInfo_serialize(const google_protobuf_SourceCodeInfo* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__SourceCodeInfo_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_SourceCodeInfo_serialize_ex(const google_protobuf_SourceCodeInfo* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__SourceCodeInfo_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_SourceCodeInfo_clear_location(google_protobuf_SourceCodeInfo* msg) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_SourceCodeInfo_Location* const* google_protobuf_SourceCodeInfo_location(const google_protobuf_SourceCodeInfo* msg, size_t* size) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_SourceCodeInfo_Location* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_SourceCodeInfo_location_upb_array(const google_protobuf_SourceCodeInfo* msg, size_t* size) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_SourceCodeInfo_location_mutable_upb_array(google_protobuf_SourceCodeInfo* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE google_protobuf_SourceCodeInfo_Location** google_protobuf_SourceCodeInfo_mutable_location(google_protobuf_SourceCodeInfo* msg, size_t* size) { + upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_SourceCodeInfo_Location**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_SourceCodeInfo_Location** google_protobuf_SourceCodeInfo_resize_location(google_protobuf_SourceCodeInfo* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_SourceCodeInfo_Location**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_SourceCodeInfo_Location* google_protobuf_SourceCodeInfo_add_location(google_protobuf_SourceCodeInfo* msg, upb_Arena* arena) { + upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_SourceCodeInfo_Location* sub = (struct google_protobuf_SourceCodeInfo_Location*)_upb_Message_New(&google__protobuf__SourceCodeInfo__Location_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.SourceCodeInfo.Location */ + +UPB_INLINE google_protobuf_SourceCodeInfo_Location* google_protobuf_SourceCodeInfo_Location_new(upb_Arena* arena) { + return (google_protobuf_SourceCodeInfo_Location*)_upb_Message_New(&google__protobuf__SourceCodeInfo__Location_msg_init, arena); +} +UPB_INLINE google_protobuf_SourceCodeInfo_Location* google_protobuf_SourceCodeInfo_Location_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_SourceCodeInfo_Location* ret = google_protobuf_SourceCodeInfo_Location_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__SourceCodeInfo__Location_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_SourceCodeInfo_Location* google_protobuf_SourceCodeInfo_Location_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_SourceCodeInfo_Location* ret = google_protobuf_SourceCodeInfo_Location_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__SourceCodeInfo__Location_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_SourceCodeInfo_Location_serialize(const google_protobuf_SourceCodeInfo_Location* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__SourceCodeInfo__Location_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_SourceCodeInfo_Location_serialize_ex(const google_protobuf_SourceCodeInfo_Location* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__SourceCodeInfo__Location_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_SourceCodeInfo_Location_clear_path(google_protobuf_SourceCodeInfo_Location* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t const* google_protobuf_SourceCodeInfo_Location_path(const google_protobuf_SourceCodeInfo_Location* msg, size_t* size) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_SourceCodeInfo_Location_path_upb_array(const google_protobuf_SourceCodeInfo_Location* msg, size_t* size) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_SourceCodeInfo_Location_path_mutable_upb_array(google_protobuf_SourceCodeInfo_Location* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 16), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_SourceCodeInfo_Location_clear_span(google_protobuf_SourceCodeInfo_Location* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(16, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t const* google_protobuf_SourceCodeInfo_Location_span(const google_protobuf_SourceCodeInfo_Location* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(16, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_SourceCodeInfo_Location_span_upb_array(const google_protobuf_SourceCodeInfo_Location* msg, size_t* size) { + const upb_MiniTableField field = {2, UPB_SIZE(16, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_SourceCodeInfo_Location_span_mutable_upb_array(google_protobuf_SourceCodeInfo_Location* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {2, UPB_SIZE(16, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_SourceCodeInfo_Location_clear_leading_comments(google_protobuf_SourceCodeInfo_Location* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(24, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_SourceCodeInfo_Location_leading_comments(const google_protobuf_SourceCodeInfo_Location* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {3, UPB_SIZE(24, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_SourceCodeInfo_Location_has_leading_comments(const google_protobuf_SourceCodeInfo_Location* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(24, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_SourceCodeInfo_Location_clear_trailing_comments(google_protobuf_SourceCodeInfo_Location* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(32, 48), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_SourceCodeInfo_Location_trailing_comments(const google_protobuf_SourceCodeInfo_Location* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {4, UPB_SIZE(32, 48), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_SourceCodeInfo_Location_has_trailing_comments(const google_protobuf_SourceCodeInfo_Location* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(32, 48), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_SourceCodeInfo_Location_clear_leading_detached_comments(google_protobuf_SourceCodeInfo_Location* msg) { + const upb_MiniTableField field = {6, UPB_SIZE(20, 64), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView const* google_protobuf_SourceCodeInfo_Location_leading_detached_comments(const google_protobuf_SourceCodeInfo_Location* msg, size_t* size) { + const upb_MiniTableField field = {6, UPB_SIZE(20, 64), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (upb_StringView const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_SourceCodeInfo_Location_leading_detached_comments_upb_array(const google_protobuf_SourceCodeInfo_Location* msg, size_t* size) { + const upb_MiniTableField field = {6, UPB_SIZE(20, 64), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_SourceCodeInfo_Location_leading_detached_comments_mutable_upb_array(google_protobuf_SourceCodeInfo_Location* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {6, UPB_SIZE(20, 64), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE int32_t* google_protobuf_SourceCodeInfo_Location_mutable_path(google_protobuf_SourceCodeInfo_Location* msg, size_t* size) { + upb_MiniTableField field = {1, UPB_SIZE(12, 16), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t*)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE int32_t* google_protobuf_SourceCodeInfo_Location_resize_path(google_protobuf_SourceCodeInfo_Location* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {1, UPB_SIZE(12, 16), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (int32_t*)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE bool google_protobuf_SourceCodeInfo_Location_add_path(google_protobuf_SourceCodeInfo_Location* msg, int32_t val, upb_Arena* arena) { + upb_MiniTableField field = {1, UPB_SIZE(12, 16), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return false; + } + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &val, sizeof(val)); + return true; +} +UPB_INLINE int32_t* google_protobuf_SourceCodeInfo_Location_mutable_span(google_protobuf_SourceCodeInfo_Location* msg, size_t* size) { + upb_MiniTableField field = {2, UPB_SIZE(16, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t*)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE int32_t* google_protobuf_SourceCodeInfo_Location_resize_span(google_protobuf_SourceCodeInfo_Location* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(16, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (int32_t*)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE bool google_protobuf_SourceCodeInfo_Location_add_span(google_protobuf_SourceCodeInfo_Location* msg, int32_t val, upb_Arena* arena) { + upb_MiniTableField field = {2, UPB_SIZE(16, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return false; + } + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &val, sizeof(val)); + return true; +} +UPB_INLINE void google_protobuf_SourceCodeInfo_Location_set_leading_comments(google_protobuf_SourceCodeInfo_Location *msg, upb_StringView value) { + const upb_MiniTableField field = {3, UPB_SIZE(24, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_SourceCodeInfo_Location_set_trailing_comments(google_protobuf_SourceCodeInfo_Location *msg, upb_StringView value) { + const upb_MiniTableField field = {4, UPB_SIZE(32, 48), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE upb_StringView* google_protobuf_SourceCodeInfo_Location_mutable_leading_detached_comments(google_protobuf_SourceCodeInfo_Location* msg, size_t* size) { + upb_MiniTableField field = {6, UPB_SIZE(20, 64), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (upb_StringView*)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE upb_StringView* google_protobuf_SourceCodeInfo_Location_resize_leading_detached_comments(google_protobuf_SourceCodeInfo_Location* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {6, UPB_SIZE(20, 64), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (upb_StringView*)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE bool google_protobuf_SourceCodeInfo_Location_add_leading_detached_comments(google_protobuf_SourceCodeInfo_Location* msg, upb_StringView val, upb_Arena* arena) { + upb_MiniTableField field = {6, UPB_SIZE(20, 64), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return false; + } + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &val, sizeof(val)); + return true; +} + +/* google.protobuf.GeneratedCodeInfo */ + +UPB_INLINE google_protobuf_GeneratedCodeInfo* google_protobuf_GeneratedCodeInfo_new(upb_Arena* arena) { + return (google_protobuf_GeneratedCodeInfo*)_upb_Message_New(&google__protobuf__GeneratedCodeInfo_msg_init, arena); +} +UPB_INLINE google_protobuf_GeneratedCodeInfo* google_protobuf_GeneratedCodeInfo_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_GeneratedCodeInfo* ret = google_protobuf_GeneratedCodeInfo_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__GeneratedCodeInfo_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_GeneratedCodeInfo* google_protobuf_GeneratedCodeInfo_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_GeneratedCodeInfo* ret = google_protobuf_GeneratedCodeInfo_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__GeneratedCodeInfo_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_GeneratedCodeInfo_serialize(const google_protobuf_GeneratedCodeInfo* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__GeneratedCodeInfo_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_GeneratedCodeInfo_serialize_ex(const google_protobuf_GeneratedCodeInfo* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__GeneratedCodeInfo_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_GeneratedCodeInfo_clear_annotation(google_protobuf_GeneratedCodeInfo* msg) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE const google_protobuf_GeneratedCodeInfo_Annotation* const* google_protobuf_GeneratedCodeInfo_annotation(const google_protobuf_GeneratedCodeInfo* msg, size_t* size) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (const google_protobuf_GeneratedCodeInfo_Annotation* const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_GeneratedCodeInfo_annotation_upb_array(const google_protobuf_GeneratedCodeInfo* msg, size_t* size) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_GeneratedCodeInfo_annotation_mutable_upb_array(google_protobuf_GeneratedCodeInfo* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} + +UPB_INLINE google_protobuf_GeneratedCodeInfo_Annotation** google_protobuf_GeneratedCodeInfo_mutable_annotation(google_protobuf_GeneratedCodeInfo* msg, size_t* size) { + upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (google_protobuf_GeneratedCodeInfo_Annotation**)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE google_protobuf_GeneratedCodeInfo_Annotation** google_protobuf_GeneratedCodeInfo_resize_annotation(google_protobuf_GeneratedCodeInfo* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (google_protobuf_GeneratedCodeInfo_Annotation**)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE struct google_protobuf_GeneratedCodeInfo_Annotation* google_protobuf_GeneratedCodeInfo_add_annotation(google_protobuf_GeneratedCodeInfo* msg, upb_Arena* arena) { + upb_MiniTableField field = {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return NULL; + } + struct google_protobuf_GeneratedCodeInfo_Annotation* sub = (struct google_protobuf_GeneratedCodeInfo_Annotation*)_upb_Message_New(&google__protobuf__GeneratedCodeInfo__Annotation_msg_init, arena); + if (!arr || !sub) return NULL; + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &sub, sizeof(sub)); + return sub; +} + +/* google.protobuf.GeneratedCodeInfo.Annotation */ + +UPB_INLINE google_protobuf_GeneratedCodeInfo_Annotation* google_protobuf_GeneratedCodeInfo_Annotation_new(upb_Arena* arena) { + return (google_protobuf_GeneratedCodeInfo_Annotation*)_upb_Message_New(&google__protobuf__GeneratedCodeInfo__Annotation_msg_init, arena); +} +UPB_INLINE google_protobuf_GeneratedCodeInfo_Annotation* google_protobuf_GeneratedCodeInfo_Annotation_parse(const char* buf, size_t size, upb_Arena* arena) { + google_protobuf_GeneratedCodeInfo_Annotation* ret = google_protobuf_GeneratedCodeInfo_Annotation_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__GeneratedCodeInfo__Annotation_msg_init, NULL, 0, arena) != + kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE google_protobuf_GeneratedCodeInfo_Annotation* google_protobuf_GeneratedCodeInfo_Annotation_parse_ex(const char* buf, size_t size, + const upb_ExtensionRegistry* extreg, + int options, upb_Arena* arena) { + google_protobuf_GeneratedCodeInfo_Annotation* ret = google_protobuf_GeneratedCodeInfo_Annotation_new(arena); + if (!ret) return NULL; + if (upb_Decode(buf, size, UPB_UPCAST(ret), &google__protobuf__GeneratedCodeInfo__Annotation_msg_init, extreg, options, + arena) != kUpb_DecodeStatus_Ok) { + return NULL; + } + return ret; +} +UPB_INLINE char* google_protobuf_GeneratedCodeInfo_Annotation_serialize(const google_protobuf_GeneratedCodeInfo_Annotation* msg, upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__GeneratedCodeInfo__Annotation_msg_init, 0, arena, &ptr, len); + return ptr; +} +UPB_INLINE char* google_protobuf_GeneratedCodeInfo_Annotation_serialize_ex(const google_protobuf_GeneratedCodeInfo_Annotation* msg, int options, + upb_Arena* arena, size_t* len) { + char* ptr; + (void)upb_Encode(UPB_UPCAST(msg), &google__protobuf__GeneratedCodeInfo__Annotation_msg_init, options, arena, &ptr, len); + return ptr; +} +UPB_INLINE void google_protobuf_GeneratedCodeInfo_Annotation_clear_path(google_protobuf_GeneratedCodeInfo_Annotation* msg) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t const* google_protobuf_GeneratedCodeInfo_Annotation_path(const google_protobuf_GeneratedCodeInfo_Annotation* msg, size_t* size) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t const*)upb_Array_DataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE const upb_Array* _google_protobuf_GeneratedCodeInfo_Annotation_path_upb_array(const google_protobuf_GeneratedCodeInfo_Annotation* msg, size_t* size) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + const upb_Array* arr = upb_Message_GetArray(UPB_UPCAST(msg), &field); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE upb_Array* _google_protobuf_GeneratedCodeInfo_Annotation_path_mutable_upb_array(google_protobuf_GeneratedCodeInfo_Annotation* msg, size_t* size, upb_Arena* arena) { + const upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray(UPB_UPCAST(msg), + &field, arena); + if (size) { + *size = arr ? arr->UPB_PRIVATE(size) : 0; + } + return arr; +} +UPB_INLINE void google_protobuf_GeneratedCodeInfo_Annotation_clear_source_file(google_protobuf_GeneratedCodeInfo_Annotation* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(28, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE upb_StringView google_protobuf_GeneratedCodeInfo_Annotation_source_file(const google_protobuf_GeneratedCodeInfo_Annotation* msg) { + upb_StringView default_val = upb_StringView_FromString(""); + upb_StringView ret; + const upb_MiniTableField field = {2, UPB_SIZE(28, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_GeneratedCodeInfo_Annotation_has_source_file(const google_protobuf_GeneratedCodeInfo_Annotation* msg) { + const upb_MiniTableField field = {2, UPB_SIZE(28, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_GeneratedCodeInfo_Annotation_clear_begin(google_protobuf_GeneratedCodeInfo_Annotation* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 12), 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_GeneratedCodeInfo_Annotation_begin(const google_protobuf_GeneratedCodeInfo_Annotation* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {3, UPB_SIZE(16, 12), 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_GeneratedCodeInfo_Annotation_has_begin(const google_protobuf_GeneratedCodeInfo_Annotation* msg) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 12), 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_GeneratedCodeInfo_Annotation_clear_end(google_protobuf_GeneratedCodeInfo_Annotation* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 16), 66, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_GeneratedCodeInfo_Annotation_end(const google_protobuf_GeneratedCodeInfo_Annotation* msg) { + int32_t default_val = (int32_t)0; + int32_t ret; + const upb_MiniTableField field = {4, UPB_SIZE(20, 16), 66, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_GeneratedCodeInfo_Annotation_has_end(const google_protobuf_GeneratedCodeInfo_Annotation* msg) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 16), 66, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE void google_protobuf_GeneratedCodeInfo_Annotation_clear_semantic(google_protobuf_GeneratedCodeInfo_Annotation* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(24, 20), 67, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_ClearBaseField(UPB_UPCAST(msg), &field); +} +UPB_INLINE int32_t google_protobuf_GeneratedCodeInfo_Annotation_semantic(const google_protobuf_GeneratedCodeInfo_Annotation* msg) { + int32_t default_val = 0; + int32_t ret; + const upb_MiniTableField field = {5, UPB_SIZE(24, 20), 67, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + _upb_Message_GetNonExtensionField(UPB_UPCAST(msg), &field, + &default_val, &ret); + return ret; +} +UPB_INLINE bool google_protobuf_GeneratedCodeInfo_Annotation_has_semantic(const google_protobuf_GeneratedCodeInfo_Annotation* msg) { + const upb_MiniTableField field = {5, UPB_SIZE(24, 20), 67, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + return upb_Message_HasBaseField(UPB_UPCAST(msg), &field); +} + +UPB_INLINE int32_t* google_protobuf_GeneratedCodeInfo_Annotation_mutable_path(google_protobuf_GeneratedCodeInfo_Annotation* msg, size_t* size) { + upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetMutableArray(UPB_UPCAST(msg), &field); + if (arr) { + if (size) *size = arr->UPB_PRIVATE(size); + return (int32_t*)upb_Array_MutableDataPtr(arr); + } else { + if (size) *size = 0; + return NULL; + } +} +UPB_INLINE int32_t* google_protobuf_GeneratedCodeInfo_Annotation_resize_path(google_protobuf_GeneratedCodeInfo_Annotation* msg, size_t size, upb_Arena* arena) { + upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + return (int32_t*)upb_Message_ResizeArrayUninitialized(UPB_UPCAST(msg), + &field, size, arena); +} +UPB_INLINE bool google_protobuf_GeneratedCodeInfo_Annotation_add_path(google_protobuf_GeneratedCodeInfo_Annotation* msg, int32_t val, upb_Arena* arena) { + upb_MiniTableField field = {1, UPB_SIZE(12, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}; + upb_Array* arr = upb_Message_GetOrCreateMutableArray( + UPB_UPCAST(msg), &field, arena); + if (!arr || !UPB_PRIVATE(_upb_Array_ResizeUninitialized)( + arr, arr->UPB_PRIVATE(size) + 1, arena)) { + return false; + } + UPB_PRIVATE(_upb_Array_Set) + (arr, arr->UPB_PRIVATE(size) - 1, &val, sizeof(val)); + return true; +} +UPB_INLINE void google_protobuf_GeneratedCodeInfo_Annotation_set_source_file(google_protobuf_GeneratedCodeInfo_Annotation *msg, upb_StringView value) { + const upb_MiniTableField field = {2, UPB_SIZE(28, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_GeneratedCodeInfo_Annotation_set_begin(google_protobuf_GeneratedCodeInfo_Annotation *msg, int32_t value) { + const upb_MiniTableField field = {3, UPB_SIZE(16, 12), 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_GeneratedCodeInfo_Annotation_set_end(google_protobuf_GeneratedCodeInfo_Annotation *msg, int32_t value) { + const upb_MiniTableField field = {4, UPB_SIZE(20, 16), 66, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} +UPB_INLINE void google_protobuf_GeneratedCodeInfo_Annotation_set_semantic(google_protobuf_GeneratedCodeInfo_Annotation *msg, int32_t value) { + const upb_MiniTableField field = {5, UPB_SIZE(24, 20), 67, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}; + upb_Message_SetBaseField((upb_Message *)msg, &field, &value); +} + +/* Max size 32 is google.protobuf.FileOptions */ +/* Max size 64 is google.protobuf.FileOptions */ +#define _UPB_MAXOPT_SIZE UPB_SIZE(112, 200) + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#include "upb/port/undef.inc" + +#endif /* GOOGLE_PROTOBUF_DESCRIPTOR_PROTO_UPB_H_ */ diff --git a/google/protobuf/descriptor.upb_minitable.c b/google/protobuf/descriptor.upb_minitable.c new file mode 100644 index 0000000..9bd32e1 --- /dev/null +++ b/google/protobuf/descriptor.upb_minitable.c @@ -0,0 +1,1364 @@ +/* This file was generated by upb_generator from the input file: + * + * google/protobuf/descriptor.proto + * + * Do not edit -- your changes will be discarded when the file is + * regenerated. */ + +#include +#include "upb/generated_code_support.h" +#include "google/protobuf/descriptor.upb_minitable.h" + +// Must be last. +#include "upb/port/def.inc" + +static const upb_MiniTableSub google_protobuf_FileDescriptorSet_submsgs[1] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FileDescriptorProto_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_FileDescriptorSet__fields[1] = { + {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__FileDescriptorSet_msg_init = { + &google_protobuf_FileDescriptorSet_submsgs[0], + &google_protobuf_FileDescriptorSet__fields[0], + 16, 1, kUpb_ExtMode_NonExtendable, 1, UPB_FASTTABLE_MASK(8), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.FileDescriptorSet", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x000800003f00000a, &upb_prm_1bt_max192b}, + }) +}; + +static const upb_MiniTableSub google_protobuf_FileDescriptorProto_submsgs[7] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__DescriptorProto_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__EnumDescriptorProto_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__ServiceDescriptorProto_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__FieldDescriptorProto_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__FileOptions_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__SourceCodeInfo_msg_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_Edition_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_FileDescriptorProto__fields[13] = { + {1, UPB_SIZE(52, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {2, UPB_SIZE(60, 32), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(12, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {4, UPB_SIZE(16, 56), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {5, UPB_SIZE(20, 64), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {6, UPB_SIZE(24, 72), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {7, UPB_SIZE(28, 80), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {8, UPB_SIZE(32, 88), 66, 4, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {9, UPB_SIZE(36, 96), 67, 5, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {10, UPB_SIZE(40, 104), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {11, UPB_SIZE(44, 112), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {12, UPB_SIZE(68, 120), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {14, UPB_SIZE(48, 12), 69, 6, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__FileDescriptorProto_msg_init = { + &google_protobuf_FileDescriptorProto_submsgs[0], + &google_protobuf_FileDescriptorProto__fields[0], + UPB_SIZE(80, 136), 13, kUpb_ExtMode_NonExtendable, 12, UPB_FASTTABLE_MASK(120), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.FileDescriptorProto", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x003000003f00001a, &upb_prs_1bt}, + {0x003800003f000022, &upb_prm_1bt_max128b}, + {0x004000003f01002a, &upb_prm_1bt_max128b}, + {0x004800003f020032, &upb_prm_1bt_max64b}, + {0x005000003f03003a, &upb_prm_1bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x006800003f000050, &upb_prv4_1bt}, + {0x007000003f000058, &upb_prv4_1bt}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_DescriptorProto_submsgs[8] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FieldDescriptorProto_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__DescriptorProto_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__EnumDescriptorProto_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__DescriptorProto__ExtensionRange_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__FieldDescriptorProto_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__MessageOptions_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__OneofDescriptorProto_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__DescriptorProto__ReservedRange_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_DescriptorProto__fields[10] = { + {1, UPB_SIZE(48, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(16, 40), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {5, UPB_SIZE(24, 56), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {6, UPB_SIZE(28, 64), 0, 4, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {7, UPB_SIZE(32, 72), 65, 5, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {8, UPB_SIZE(36, 80), 0, 6, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {9, UPB_SIZE(40, 88), 0, 7, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {10, UPB_SIZE(44, 96), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__DescriptorProto_msg_init = { + &google_protobuf_DescriptorProto_submsgs[0], + &google_protobuf_DescriptorProto__fields[0], + UPB_SIZE(56, 104), 10, kUpb_ExtMode_NonExtendable, 10, UPB_FASTTABLE_MASK(120), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.DescriptorProto", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x002000003f000012, &upb_prm_1bt_max128b}, + {0x002800003f01001a, &upb_prm_1bt_max128b}, + {0x003000003f020022, &upb_prm_1bt_max128b}, + {0x003800003f03002a, &upb_prm_1bt_max64b}, + {0x004000003f040032, &upb_prm_1bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x005000003f060042, &upb_prm_1bt_max64b}, + {0x005800003f07004a, &upb_prm_1bt_max64b}, + {0x006000003f000052, &upb_prs_1bt}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_DescriptorProto_ExtensionRange_submsgs[1] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__ExtensionRangeOptions_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_DescriptorProto_ExtensionRange__fields[3] = { + {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(20, 24), 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__DescriptorProto__ExtensionRange_msg_init = { + &google_protobuf_DescriptorProto_ExtensionRange_submsgs[0], + &google_protobuf_DescriptorProto_ExtensionRange__fields[0], + UPB_SIZE(24, 32), 3, kUpb_ExtMode_NonExtendable, 3, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.DescriptorProto.ExtensionRange", +#endif +}; + +static const upb_MiniTableField google_protobuf_DescriptorProto_ReservedRange__fields[2] = { + {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__DescriptorProto__ReservedRange_msg_init = { + NULL, + &google_protobuf_DescriptorProto_ReservedRange__fields[0], + 24, 2, kUpb_ExtMode_NonExtendable, 2, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.DescriptorProto.ReservedRange", +#endif +}; + +static const upb_MiniTableSub google_protobuf_ExtensionRangeOptions_submsgs[4] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__ExtensionRangeOptions__Declaration_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSet_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__UninterpretedOption_msg_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_ExtensionRangeOptions_VerificationState_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_ExtensionRangeOptions__fields[4] = { + {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(16, 12), 64, 3, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {50, UPB_SIZE(20, 24), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__ExtensionRangeOptions_msg_init = { + &google_protobuf_ExtensionRangeOptions_submsgs[0], + &google_protobuf_ExtensionRangeOptions__fields[0], + UPB_SIZE(32, 40), 4, kUpb_ExtMode_Extendable, 0, UPB_FASTTABLE_MASK(248), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.ExtensionRangeOptions", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x001000003f000012, &upb_prm_1bt_max64b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x002000003f023eba, &upb_prm_2bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableField google_protobuf_ExtensionRangeOptions_Declaration__fields[5] = { + {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {2, UPB_SIZE(20, 24), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(28, 40), 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {5, 16, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {6, 17, 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__ExtensionRangeOptions__Declaration_msg_init = { + NULL, + &google_protobuf_ExtensionRangeOptions_Declaration__fields[0], + UPB_SIZE(40, 56), 5, kUpb_ExtMode_NonExtendable, 3, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.ExtensionRangeOptions.Declaration", +#endif +}; + +static const upb_MiniTableSub google_protobuf_FieldDescriptorProto_submsgs[3] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FieldOptions_msg_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FieldDescriptorProto_Label_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FieldDescriptorProto_Type_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_FieldDescriptorProto__fields[11] = { + {1, UPB_SIZE(36, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {2, UPB_SIZE(44, 48), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {3, 12, 66, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {4, 16, 67, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {5, 20, 68, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {6, UPB_SIZE(52, 64), 69, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {7, UPB_SIZE(60, 80), 70, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {8, UPB_SIZE(24, 96), 71, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {9, UPB_SIZE(28, 24), 72, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {10, UPB_SIZE(68, 104), 73, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {17, UPB_SIZE(32, 28), 74, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__FieldDescriptorProto_msg_init = { + &google_protobuf_FieldDescriptorProto_submsgs[0], + &google_protobuf_FieldDescriptorProto__fields[0], + UPB_SIZE(80, 120), 11, kUpb_ExtMode_NonExtendable, 10, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.FieldDescriptorProto", +#endif +}; + +static const upb_MiniTableSub google_protobuf_OneofDescriptorProto_submsgs[1] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__OneofOptions_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_OneofDescriptorProto__fields[2] = { + {1, 16, 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {2, UPB_SIZE(12, 32), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__OneofDescriptorProto_msg_init = { + &google_protobuf_OneofDescriptorProto_submsgs[0], + &google_protobuf_OneofDescriptorProto__fields[0], + UPB_SIZE(24, 40), 2, kUpb_ExtMode_NonExtendable, 2, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.OneofDescriptorProto", +#endif +}; + +static const upb_MiniTableSub google_protobuf_EnumDescriptorProto_submsgs[3] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__EnumValueDescriptorProto_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__EnumOptions_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__EnumDescriptorProto__EnumReservedRange_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_EnumDescriptorProto__fields[5] = { + {1, UPB_SIZE(28, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(16, 40), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {4, UPB_SIZE(20, 48), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {5, UPB_SIZE(24, 56), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__EnumDescriptorProto_msg_init = { + &google_protobuf_EnumDescriptorProto_submsgs[0], + &google_protobuf_EnumDescriptorProto__fields[0], + UPB_SIZE(40, 64), 5, kUpb_ExtMode_NonExtendable, 5, UPB_FASTTABLE_MASK(56), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.EnumDescriptorProto", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x002000003f000012, &upb_prm_1bt_max64b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x003000003f020022, &upb_prm_1bt_max64b}, + {0x003800003f00002a, &upb_prs_1bt}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableField google_protobuf_EnumDescriptorProto_EnumReservedRange__fields[2] = { + {1, 12, 64, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {2, 16, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__EnumDescriptorProto__EnumReservedRange_msg_init = { + NULL, + &google_protobuf_EnumDescriptorProto_EnumReservedRange__fields[0], + 24, 2, kUpb_ExtMode_NonExtendable, 2, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.EnumDescriptorProto.EnumReservedRange", +#endif +}; + +static const upb_MiniTableSub google_protobuf_EnumValueDescriptorProto_submsgs[1] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__EnumValueOptions_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_EnumValueDescriptorProto__fields[3] = { + {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {2, 12, 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(16, 32), 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__EnumValueDescriptorProto_msg_init = { + &google_protobuf_EnumValueDescriptorProto_submsgs[0], + &google_protobuf_EnumValueDescriptorProto__fields[0], + UPB_SIZE(32, 40), 3, kUpb_ExtMode_NonExtendable, 3, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.EnumValueDescriptorProto", +#endif +}; + +static const upb_MiniTableSub google_protobuf_ServiceDescriptorProto_submsgs[2] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__MethodDescriptorProto_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__ServiceOptions_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_ServiceDescriptorProto__fields[3] = { + {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {2, UPB_SIZE(12, 32), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(16, 40), 65, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__ServiceDescriptorProto_msg_init = { + &google_protobuf_ServiceDescriptorProto_submsgs[0], + &google_protobuf_ServiceDescriptorProto__fields[0], + UPB_SIZE(32, 48), 3, kUpb_ExtMode_NonExtendable, 3, UPB_FASTTABLE_MASK(24), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.ServiceDescriptorProto", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x002000003f000012, &upb_prm_1bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_MethodDescriptorProto_submsgs[1] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__MethodOptions_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_MethodDescriptorProto__fields[6] = { + {1, UPB_SIZE(20, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {2, UPB_SIZE(28, 32), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(36, 48), 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {4, UPB_SIZE(12, 64), 67, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {5, UPB_SIZE(16, 9), 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {6, UPB_SIZE(17, 10), 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__MethodDescriptorProto_msg_init = { + &google_protobuf_MethodDescriptorProto_submsgs[0], + &google_protobuf_MethodDescriptorProto__fields[0], + UPB_SIZE(48, 72), 6, kUpb_ExtMode_NonExtendable, 6, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.MethodDescriptorProto", +#endif +}; + +static const upb_MiniTableSub google_protobuf_FileOptions_submsgs[3] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSet_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__UninterpretedOption_msg_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FileOptions_OptimizeMode_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_FileOptions__fields[21] = { + {1, UPB_SIZE(32, 24), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {8, 40, 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {9, 12, 66, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {10, 16, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {11, UPB_SIZE(48, 56), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {16, 17, 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {17, 18, 70, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {18, 19, 71, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {20, 20, 72, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {23, 21, 73, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {27, 22, 74, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {31, 23, 75, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {36, UPB_SIZE(56, 72), 76, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {37, UPB_SIZE(64, 88), 77, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {39, UPB_SIZE(72, 104), 78, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {40, UPB_SIZE(80, 120), 79, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {41, UPB_SIZE(88, 136), 80, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {44, UPB_SIZE(96, 152), 81, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {45, UPB_SIZE(104, 168), 82, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {50, UPB_SIZE(24, 184), 83, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {999, UPB_SIZE(28, 192), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__FileOptions_msg_init = { + &google_protobuf_FileOptions_submsgs[0], + &google_protobuf_FileOptions__fields[0], + UPB_SIZE(112, 200), 21, kUpb_ExtMode_Extendable, 1, UPB_FASTTABLE_MASK(248), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.FileOptions", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x00c000003f013eba, &upb_prm_2bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_MessageOptions_submsgs[2] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSet_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__UninterpretedOption_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_MessageOptions__fields[7] = { + {1, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {2, 10, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {3, 11, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {7, 12, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {11, 13, 68, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {12, 16, 69, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__MessageOptions_msg_init = { + &google_protobuf_MessageOptions_submsgs[0], + &google_protobuf_MessageOptions__fields[0], + UPB_SIZE(24, 32), 7, kUpb_ExtMode_Extendable, 3, UPB_FASTTABLE_MASK(248), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.MessageOptions", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x001800003f013eba, &upb_prm_2bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_FieldOptions_submsgs[8] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FieldOptions__EditionDefault_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSet_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__FieldOptions__FeatureSupport_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__UninterpretedOption_msg_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FieldOptions_CType_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FieldOptions_JSType_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FieldOptions_OptionRetention_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FieldOptions_OptionTargetType_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_FieldOptions__fields[14] = { + {1, 12, 64, 4, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {2, 16, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {3, 17, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {5, 18, 67, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {6, 20, 68, 5, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {10, 24, 69, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {15, 25, 70, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {16, 26, 71, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {17, 28, 72, 6, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {19, 32, 0, 7, 14, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {20, UPB_SIZE(36, 40), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {21, UPB_SIZE(40, 48), 73, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {22, UPB_SIZE(44, 56), 74, 2, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {999, UPB_SIZE(48, 64), 0, 3, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__FieldOptions_msg_init = { + &google_protobuf_FieldOptions_submsgs[0], + &google_protobuf_FieldOptions__fields[0], + UPB_SIZE(56, 72), 14, kUpb_ExtMode_Extendable, 3, UPB_FASTTABLE_MASK(248), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.FieldOptions", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x002800003f0001a2, &upb_prm_2bt_max64b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x004000003f033eba, &upb_prm_2bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_FieldOptions_EditionDefault_submsgs[1] = { + {.UPB_PRIVATE(subenum) = &google_protobuf_Edition_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_FieldOptions_EditionDefault__fields[2] = { + {2, 16, 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {3, 12, 65, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__FieldOptions__EditionDefault_msg_init = { + &google_protobuf_FieldOptions_EditionDefault_submsgs[0], + &google_protobuf_FieldOptions_EditionDefault__fields[0], + UPB_SIZE(24, 32), 2, kUpb_ExtMode_NonExtendable, 0, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.FieldOptions.EditionDefault", +#endif +}; + +static const upb_MiniTableSub google_protobuf_FieldOptions_FeatureSupport_submsgs[3] = { + {.UPB_PRIVATE(subenum) = &google_protobuf_Edition_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_Edition_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_Edition_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_FieldOptions_FeatureSupport__fields[4] = { + {1, 12, 64, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {2, 16, 65, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {3, 24, 66, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {4, 20, 67, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__FieldOptions__FeatureSupport_msg_init = { + &google_protobuf_FieldOptions_FeatureSupport_submsgs[0], + &google_protobuf_FieldOptions_FeatureSupport__fields[0], + UPB_SIZE(32, 40), 4, kUpb_ExtMode_NonExtendable, 4, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.FieldOptions.FeatureSupport", +#endif +}; + +static const upb_MiniTableSub google_protobuf_OneofOptions_submsgs[2] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSet_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__UninterpretedOption_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_OneofOptions__fields[2] = { + {1, UPB_SIZE(12, 16), 64, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__OneofOptions_msg_init = { + &google_protobuf_OneofOptions_submsgs[0], + &google_protobuf_OneofOptions__fields[0], + UPB_SIZE(24, 32), 2, kUpb_ExtMode_Extendable, 1, UPB_FASTTABLE_MASK(248), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.OneofOptions", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x001800003f013eba, &upb_prm_2bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_EnumOptions_submsgs[2] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSet_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__UninterpretedOption_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_EnumOptions__fields[5] = { + {2, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {3, 10, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {6, 11, 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {7, UPB_SIZE(12, 16), 67, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__EnumOptions_msg_init = { + &google_protobuf_EnumOptions_submsgs[0], + &google_protobuf_EnumOptions__fields[0], + UPB_SIZE(24, 32), 5, kUpb_ExtMode_Extendable, 0, UPB_FASTTABLE_MASK(248), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.EnumOptions", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x001800003f013eba, &upb_prm_2bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_EnumValueOptions_submsgs[3] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSet_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__FieldOptions__FeatureSupport_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__UninterpretedOption_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_EnumValueOptions__fields[5] = { + {1, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {2, UPB_SIZE(12, 16), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(16, 10), 66, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {4, UPB_SIZE(20, 24), 67, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {999, UPB_SIZE(24, 32), 0, 2, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__EnumValueOptions_msg_init = { + &google_protobuf_EnumValueOptions_submsgs[0], + &google_protobuf_EnumValueOptions__fields[0], + UPB_SIZE(32, 40), 5, kUpb_ExtMode_Extendable, 4, UPB_FASTTABLE_MASK(248), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.EnumValueOptions", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x002000003f023eba, &upb_prm_2bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_ServiceOptions_submsgs[2] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSet_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__UninterpretedOption_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_ServiceOptions__fields[3] = { + {33, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {34, UPB_SIZE(12, 16), 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {999, UPB_SIZE(16, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__ServiceOptions_msg_init = { + &google_protobuf_ServiceOptions_submsgs[0], + &google_protobuf_ServiceOptions__fields[0], + UPB_SIZE(24, 32), 3, kUpb_ExtMode_Extendable, 0, UPB_FASTTABLE_MASK(248), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.ServiceOptions", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x001800003f013eba, &upb_prm_2bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_MethodOptions_submsgs[3] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSet_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__UninterpretedOption_msg_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_MethodOptions_IdempotencyLevel_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_MethodOptions__fields[4] = { + {33, 9, 64, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, + {34, 12, 65, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {35, 16, 66, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {999, UPB_SIZE(20, 24), 0, 1, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__MethodOptions_msg_init = { + &google_protobuf_MethodOptions_submsgs[0], + &google_protobuf_MethodOptions__fields[0], + UPB_SIZE(24, 32), 4, kUpb_ExtMode_Extendable, 0, UPB_FASTTABLE_MASK(248), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.MethodOptions", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x001800003f013eba, &upb_prm_2bt_max128b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_UninterpretedOption_submsgs[1] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__UninterpretedOption__NamePart_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_UninterpretedOption__fields[7] = { + {2, UPB_SIZE(12, 16), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(16, 24), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {4, UPB_SIZE(24, 40), 65, kUpb_NoSub, 4, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}, + {5, UPB_SIZE(32, 48), 66, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}, + {6, UPB_SIZE(40, 56), 67, kUpb_NoSub, 1, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)}, + {7, UPB_SIZE(48, 64), 68, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {8, UPB_SIZE(56, 80), 69, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__UninterpretedOption_msg_init = { + &google_protobuf_UninterpretedOption_submsgs[0], + &google_protobuf_UninterpretedOption__fields[0], + UPB_SIZE(64, 96), 7, kUpb_ExtMode_NonExtendable, 0, UPB_FASTTABLE_MASK(24), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.UninterpretedOption", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x001000003f000012, &upb_prm_1bt_max64b}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableField google_protobuf_UninterpretedOption_NamePart__fields[2] = { + {1, UPB_SIZE(12, 16), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {2, 9, 65, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__UninterpretedOption__NamePart_msg_init = { + NULL, + &google_protobuf_UninterpretedOption_NamePart__fields[0], + UPB_SIZE(24, 32), 2, kUpb_ExtMode_NonExtendable, 2, UPB_FASTTABLE_MASK(255), 2, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.UninterpretedOption.NamePart", +#endif +}; + +static const upb_MiniTableSub google_protobuf_FeatureSet_submsgs[6] = { + {.UPB_PRIVATE(subenum) = &google_protobuf_FeatureSet_FieldPresence_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FeatureSet_EnumType_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FeatureSet_RepeatedFieldEncoding_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FeatureSet_Utf8Validation_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FeatureSet_MessageEncoding_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_FeatureSet_JsonFormat_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_FeatureSet__fields[6] = { + {1, 12, 64, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {2, 16, 65, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {3, 20, 66, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {4, 24, 67, 3, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {5, 28, 68, 4, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {6, 32, 69, 5, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__FeatureSet_msg_init = { + &google_protobuf_FeatureSet_submsgs[0], + &google_protobuf_FeatureSet__fields[0], + 40, 6, kUpb_ExtMode_Extendable, 6, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.FeatureSet", +#endif +}; + +static const upb_MiniTableSub google_protobuf_FeatureSetDefaults_submsgs[3] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSetDefaults__FeatureSetEditionDefault_msg_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_Edition_enum_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_Edition_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_FeatureSetDefaults__fields[3] = { + {1, UPB_SIZE(12, 24), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {4, UPB_SIZE(16, 12), 64, 1, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {5, UPB_SIZE(20, 16), 65, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__FeatureSetDefaults_msg_init = { + &google_protobuf_FeatureSetDefaults_submsgs[0], + &google_protobuf_FeatureSetDefaults__fields[0], + UPB_SIZE(24, 32), 3, kUpb_ExtMode_NonExtendable, 1, UPB_FASTTABLE_MASK(8), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.FeatureSetDefaults", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x001800003f00000a, &upb_prm_1bt_max64b}, + }) +}; + +static const upb_MiniTableSub google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_submsgs[3] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSet_msg_init}, + {.UPB_PRIVATE(submsg) = &google__protobuf__FeatureSet_msg_init}, + {.UPB_PRIVATE(subenum) = &google_protobuf_Edition_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault__fields[3] = { + {3, 12, 64, 2, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {4, 16, 65, 0, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {5, UPB_SIZE(20, 24), 66, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__FeatureSetDefaults__FeatureSetEditionDefault_msg_init = { + &google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_submsgs[0], + &google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault__fields[0], + UPB_SIZE(24, 32), 3, kUpb_ExtMode_NonExtendable, 0, UPB_FASTTABLE_MASK(255), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault", +#endif +}; + +static const upb_MiniTableSub google_protobuf_SourceCodeInfo_submsgs[1] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__SourceCodeInfo__Location_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_SourceCodeInfo__fields[1] = { + {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__SourceCodeInfo_msg_init = { + &google_protobuf_SourceCodeInfo_submsgs[0], + &google_protobuf_SourceCodeInfo__fields[0], + 16, 1, kUpb_ExtMode_NonExtendable, 1, UPB_FASTTABLE_MASK(8), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.SourceCodeInfo", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x000800003f00000a, &upb_prm_1bt_max128b}, + }) +}; + +static const upb_MiniTableField google_protobuf_SourceCodeInfo_Location__fields[5] = { + {1, UPB_SIZE(12, 16), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {2, UPB_SIZE(16, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(24, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {4, UPB_SIZE(32, 48), 65, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {6, UPB_SIZE(20, 64), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsAlternate | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__SourceCodeInfo__Location_msg_init = { + NULL, + &google_protobuf_SourceCodeInfo_Location__fields[0], + UPB_SIZE(40, 72), 5, kUpb_ExtMode_NonExtendable, 4, UPB_FASTTABLE_MASK(56), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.SourceCodeInfo.Location", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x001000003f00000a, &upb_ppv4_1bt}, + {0x001800003f000012, &upb_ppv4_1bt}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x004000003f000032, &upb_prs_1bt}, + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + }) +}; + +static const upb_MiniTableSub google_protobuf_GeneratedCodeInfo_submsgs[1] = { + {.UPB_PRIVATE(submsg) = &google__protobuf__GeneratedCodeInfo__Annotation_msg_init}, +}; + +static const upb_MiniTableField google_protobuf_GeneratedCodeInfo__fields[1] = { + {1, 8, 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__GeneratedCodeInfo_msg_init = { + &google_protobuf_GeneratedCodeInfo_submsgs[0], + &google_protobuf_GeneratedCodeInfo__fields[0], + 16, 1, kUpb_ExtMode_NonExtendable, 1, UPB_FASTTABLE_MASK(8), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.GeneratedCodeInfo", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x000800003f00000a, &upb_prm_1bt_max64b}, + }) +}; + +static const upb_MiniTableSub google_protobuf_GeneratedCodeInfo_Annotation_submsgs[1] = { + {.UPB_PRIVATE(subenum) = &google_protobuf_GeneratedCodeInfo_Annotation_Semantic_enum_init}, +}; + +static const upb_MiniTableField google_protobuf_GeneratedCodeInfo_Annotation__fields[5] = { + {1, UPB_SIZE(12, 24), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Array | (int)kUpb_LabelFlags_IsPacked | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)}, + {2, UPB_SIZE(28, 32), 64, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | (int)kUpb_LabelFlags_IsAlternate | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)}, + {3, UPB_SIZE(16, 12), 65, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {4, UPB_SIZE(20, 16), 66, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, + {5, UPB_SIZE(24, 20), 67, 0, 14, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)}, +}; + +const upb_MiniTable google__protobuf__GeneratedCodeInfo__Annotation_msg_init = { + &google_protobuf_GeneratedCodeInfo_Annotation_submsgs[0], + &google_protobuf_GeneratedCodeInfo_Annotation__fields[0], + UPB_SIZE(40, 48), 5, kUpb_ExtMode_NonExtendable, 5, UPB_FASTTABLE_MASK(8), 0, +#ifdef UPB_TRACING_ENABLED + "google.protobuf.GeneratedCodeInfo.Annotation", +#endif + UPB_FASTTABLE_INIT({ + {0x0000000000000000, &_upb_FastDecoder_DecodeGeneric}, + {0x001800003f00000a, &upb_ppv4_1bt}, + }) +}; + +static const upb_MiniTable *messages_layout[33] = { + &google__protobuf__FileDescriptorSet_msg_init, + &google__protobuf__FileDescriptorProto_msg_init, + &google__protobuf__DescriptorProto_msg_init, + &google__protobuf__DescriptorProto__ExtensionRange_msg_init, + &google__protobuf__DescriptorProto__ReservedRange_msg_init, + &google__protobuf__ExtensionRangeOptions_msg_init, + &google__protobuf__ExtensionRangeOptions__Declaration_msg_init, + &google__protobuf__FieldDescriptorProto_msg_init, + &google__protobuf__OneofDescriptorProto_msg_init, + &google__protobuf__EnumDescriptorProto_msg_init, + &google__protobuf__EnumDescriptorProto__EnumReservedRange_msg_init, + &google__protobuf__EnumValueDescriptorProto_msg_init, + &google__protobuf__ServiceDescriptorProto_msg_init, + &google__protobuf__MethodDescriptorProto_msg_init, + &google__protobuf__FileOptions_msg_init, + &google__protobuf__MessageOptions_msg_init, + &google__protobuf__FieldOptions_msg_init, + &google__protobuf__FieldOptions__EditionDefault_msg_init, + &google__protobuf__FieldOptions__FeatureSupport_msg_init, + &google__protobuf__OneofOptions_msg_init, + &google__protobuf__EnumOptions_msg_init, + &google__protobuf__EnumValueOptions_msg_init, + &google__protobuf__ServiceOptions_msg_init, + &google__protobuf__MethodOptions_msg_init, + &google__protobuf__UninterpretedOption_msg_init, + &google__protobuf__UninterpretedOption__NamePart_msg_init, + &google__protobuf__FeatureSet_msg_init, + &google__protobuf__FeatureSetDefaults_msg_init, + &google__protobuf__FeatureSetDefaults__FeatureSetEditionDefault_msg_init, + &google__protobuf__SourceCodeInfo_msg_init, + &google__protobuf__SourceCodeInfo__Location_msg_init, + &google__protobuf__GeneratedCodeInfo_msg_init, + &google__protobuf__GeneratedCodeInfo__Annotation_msg_init, +}; + +const upb_MiniTableEnum google_protobuf_Edition_enum_init = { + 64, + 9, + { + 0x7, + 0x0, + 0x384, + 0x3e6, + 0x3e7, + 0x3e8, + 0x3e9, + 0x1869d, + 0x1869e, + 0x1869f, + 0x7fffffff, + }, +}; + +const upb_MiniTableEnum google_protobuf_ExtensionRangeOptions_VerificationState_enum_init = { + 64, + 0, + { + 0x3, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FeatureSet_EnumType_enum_init = { + 64, + 0, + { + 0x7, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FeatureSet_FieldPresence_enum_init = { + 64, + 0, + { + 0xf, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FeatureSet_JsonFormat_enum_init = { + 64, + 0, + { + 0x7, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FeatureSet_MessageEncoding_enum_init = { + 64, + 0, + { + 0x7, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FeatureSet_RepeatedFieldEncoding_enum_init = { + 64, + 0, + { + 0x7, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FeatureSet_Utf8Validation_enum_init = { + 64, + 0, + { + 0xd, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FieldDescriptorProto_Label_enum_init = { + 64, + 0, + { + 0xe, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FieldDescriptorProto_Type_enum_init = { + 64, + 0, + { + 0x7fffe, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FieldOptions_CType_enum_init = { + 64, + 0, + { + 0x7, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FieldOptions_JSType_enum_init = { + 64, + 0, + { + 0x7, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FieldOptions_OptionRetention_enum_init = { + 64, + 0, + { + 0x7, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FieldOptions_OptionTargetType_enum_init = { + 64, + 0, + { + 0x3ff, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_FileOptions_OptimizeMode_enum_init = { + 64, + 0, + { + 0xe, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_GeneratedCodeInfo_Annotation_Semantic_enum_init = { + 64, + 0, + { + 0x7, + 0x0, + }, +}; + +const upb_MiniTableEnum google_protobuf_MethodOptions_IdempotencyLevel_enum_init = { + 64, + 0, + { + 0x7, + 0x0, + }, +}; + +static const upb_MiniTableEnum *enums_layout[17] = { + &google_protobuf_Edition_enum_init, + &google_protobuf_ExtensionRangeOptions_VerificationState_enum_init, + &google_protobuf_FeatureSet_EnumType_enum_init, + &google_protobuf_FeatureSet_FieldPresence_enum_init, + &google_protobuf_FeatureSet_JsonFormat_enum_init, + &google_protobuf_FeatureSet_MessageEncoding_enum_init, + &google_protobuf_FeatureSet_RepeatedFieldEncoding_enum_init, + &google_protobuf_FeatureSet_Utf8Validation_enum_init, + &google_protobuf_FieldDescriptorProto_Label_enum_init, + &google_protobuf_FieldDescriptorProto_Type_enum_init, + &google_protobuf_FieldOptions_CType_enum_init, + &google_protobuf_FieldOptions_JSType_enum_init, + &google_protobuf_FieldOptions_OptionRetention_enum_init, + &google_protobuf_FieldOptions_OptionTargetType_enum_init, + &google_protobuf_FileOptions_OptimizeMode_enum_init, + &google_protobuf_GeneratedCodeInfo_Annotation_Semantic_enum_init, + &google_protobuf_MethodOptions_IdempotencyLevel_enum_init, +}; + +const upb_MiniTableFile google_protobuf_descriptor_proto_upb_file_layout = { + messages_layout, + enums_layout, + NULL, + 33, + 17, + 0, +}; + +#include "upb/port/undef.inc" + diff --git a/google/protobuf/descriptor.upb_minitable.h b/google/protobuf/descriptor.upb_minitable.h new file mode 100644 index 0000000..f48a64d --- /dev/null +++ b/google/protobuf/descriptor.upb_minitable.h @@ -0,0 +1,79 @@ +/* This file was generated by upb_generator from the input file: + * + * google/protobuf/descriptor.proto + * + * Do not edit -- your changes will be discarded when the file is + * regenerated. */ + +#ifndef GOOGLE_PROTOBUF_DESCRIPTOR_PROTO_UPB_MINITABLE_H_ +#define GOOGLE_PROTOBUF_DESCRIPTOR_PROTO_UPB_MINITABLE_H_ + +#include "upb/generated_code_support.h" + +// Must be last. +#include "upb/port/def.inc" + +#ifdef __cplusplus +extern "C" { +#endif + +extern const upb_MiniTable google__protobuf__FileDescriptorSet_msg_init; +extern const upb_MiniTable google__protobuf__FileDescriptorProto_msg_init; +extern const upb_MiniTable google__protobuf__DescriptorProto_msg_init; +extern const upb_MiniTable google__protobuf__DescriptorProto__ExtensionRange_msg_init; +extern const upb_MiniTable google__protobuf__DescriptorProto__ReservedRange_msg_init; +extern const upb_MiniTable google__protobuf__ExtensionRangeOptions_msg_init; +extern const upb_MiniTable google__protobuf__ExtensionRangeOptions__Declaration_msg_init; +extern const upb_MiniTable google__protobuf__FieldDescriptorProto_msg_init; +extern const upb_MiniTable google__protobuf__OneofDescriptorProto_msg_init; +extern const upb_MiniTable google__protobuf__EnumDescriptorProto_msg_init; +extern const upb_MiniTable google__protobuf__EnumDescriptorProto__EnumReservedRange_msg_init; +extern const upb_MiniTable google__protobuf__EnumValueDescriptorProto_msg_init; +extern const upb_MiniTable google__protobuf__ServiceDescriptorProto_msg_init; +extern const upb_MiniTable google__protobuf__MethodDescriptorProto_msg_init; +extern const upb_MiniTable google__protobuf__FileOptions_msg_init; +extern const upb_MiniTable google__protobuf__MessageOptions_msg_init; +extern const upb_MiniTable google__protobuf__FieldOptions_msg_init; +extern const upb_MiniTable google__protobuf__FieldOptions__EditionDefault_msg_init; +extern const upb_MiniTable google__protobuf__FieldOptions__FeatureSupport_msg_init; +extern const upb_MiniTable google__protobuf__OneofOptions_msg_init; +extern const upb_MiniTable google__protobuf__EnumOptions_msg_init; +extern const upb_MiniTable google__protobuf__EnumValueOptions_msg_init; +extern const upb_MiniTable google__protobuf__ServiceOptions_msg_init; +extern const upb_MiniTable google__protobuf__MethodOptions_msg_init; +extern const upb_MiniTable google__protobuf__UninterpretedOption_msg_init; +extern const upb_MiniTable google__protobuf__UninterpretedOption__NamePart_msg_init; +extern const upb_MiniTable google__protobuf__FeatureSet_msg_init; +extern const upb_MiniTable google__protobuf__FeatureSetDefaults_msg_init; +extern const upb_MiniTable google__protobuf__FeatureSetDefaults__FeatureSetEditionDefault_msg_init; +extern const upb_MiniTable google__protobuf__SourceCodeInfo_msg_init; +extern const upb_MiniTable google__protobuf__SourceCodeInfo__Location_msg_init; +extern const upb_MiniTable google__protobuf__GeneratedCodeInfo_msg_init; +extern const upb_MiniTable google__protobuf__GeneratedCodeInfo__Annotation_msg_init; + +extern const upb_MiniTableEnum google_protobuf_Edition_enum_init; +extern const upb_MiniTableEnum google_protobuf_ExtensionRangeOptions_VerificationState_enum_init; +extern const upb_MiniTableEnum google_protobuf_FeatureSet_EnumType_enum_init; +extern const upb_MiniTableEnum google_protobuf_FeatureSet_FieldPresence_enum_init; +extern const upb_MiniTableEnum google_protobuf_FeatureSet_JsonFormat_enum_init; +extern const upb_MiniTableEnum google_protobuf_FeatureSet_MessageEncoding_enum_init; +extern const upb_MiniTableEnum google_protobuf_FeatureSet_RepeatedFieldEncoding_enum_init; +extern const upb_MiniTableEnum google_protobuf_FeatureSet_Utf8Validation_enum_init; +extern const upb_MiniTableEnum google_protobuf_FieldDescriptorProto_Label_enum_init; +extern const upb_MiniTableEnum google_protobuf_FieldDescriptorProto_Type_enum_init; +extern const upb_MiniTableEnum google_protobuf_FieldOptions_CType_enum_init; +extern const upb_MiniTableEnum google_protobuf_FieldOptions_JSType_enum_init; +extern const upb_MiniTableEnum google_protobuf_FieldOptions_OptionRetention_enum_init; +extern const upb_MiniTableEnum google_protobuf_FieldOptions_OptionTargetType_enum_init; +extern const upb_MiniTableEnum google_protobuf_FileOptions_OptimizeMode_enum_init; +extern const upb_MiniTableEnum google_protobuf_GeneratedCodeInfo_Annotation_Semantic_enum_init; +extern const upb_MiniTableEnum google_protobuf_MethodOptions_IdempotencyLevel_enum_init; +extern const upb_MiniTableFile google_protobuf_descriptor_proto_upb_file_layout; + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#include "upb/port/undef.inc" + +#endif /* GOOGLE_PROTOBUF_DESCRIPTOR_PROTO_UPB_MINITABLE_H_ */ diff --git a/google/protobuf/descriptor.upbdefs.c b/google/protobuf/descriptor.upbdefs.c new file mode 100644 index 0000000..47d0623 --- /dev/null +++ b/google/protobuf/descriptor.upbdefs.c @@ -0,0 +1,514 @@ +/* This file was generated by upb_generator from the input file: + * + * google/protobuf/descriptor.proto + * + * Do not edit -- your changes will be discarded when the file is + * regenerated. */ + +#include "upb/reflection/def.h" +#include "google/protobuf/descriptor.upbdefs.h" +#include "google/protobuf/descriptor.upb_minitable.h" + +static const char descriptor[12268] = {'\n', ' ', 'g', 'o', 'o', 'g', 'l', 'e', '/', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '/', 'd', 'e', 's', 'c', 'r', 'i', 'p', +'t', 'o', 'r', '.', 'p', 'r', 'o', 't', 'o', '\022', '\017', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', +'f', '\"', 'M', '\n', '\021', 'F', 'i', 'l', 'e', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'S', 'e', 't', '\022', '8', '\n', +'\004', 'f', 'i', 'l', 'e', '\030', '\001', ' ', '\003', '(', '\013', '2', '$', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', +'o', 'b', 'u', 'f', '.', 'F', 'i', 'l', 'e', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', 'R', +'\004', 'f', 'i', 'l', 'e', '\"', '\230', '\005', '\n', '\023', 'F', 'i', 'l', 'e', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', +'r', 'o', 't', 'o', '\022', '\022', '\n', '\004', 'n', 'a', 'm', 'e', '\030', '\001', ' ', '\001', '(', '\t', 'R', '\004', 'n', 'a', 'm', 'e', '\022', +'\030', '\n', '\007', 'p', 'a', 'c', 'k', 'a', 'g', 'e', '\030', '\002', ' ', '\001', '(', '\t', 'R', '\007', 'p', 'a', 'c', 'k', 'a', 'g', 'e', +'\022', '\036', '\n', '\n', 'd', 'e', 'p', 'e', 'n', 'd', 'e', 'n', 'c', 'y', '\030', '\003', ' ', '\003', '(', '\t', 'R', '\n', 'd', 'e', 'p', +'e', 'n', 'd', 'e', 'n', 'c', 'y', '\022', '+', '\n', '\021', 'p', 'u', 'b', 'l', 'i', 'c', '_', 'd', 'e', 'p', 'e', 'n', 'd', 'e', +'n', 'c', 'y', '\030', '\n', ' ', '\003', '(', '\005', 'R', '\020', 'p', 'u', 'b', 'l', 'i', 'c', 'D', 'e', 'p', 'e', 'n', 'd', 'e', 'n', +'c', 'y', '\022', '\'', '\n', '\017', 'w', 'e', 'a', 'k', '_', 'd', 'e', 'p', 'e', 'n', 'd', 'e', 'n', 'c', 'y', '\030', '\013', ' ', '\003', +'(', '\005', 'R', '\016', 'w', 'e', 'a', 'k', 'D', 'e', 'p', 'e', 'n', 'd', 'e', 'n', 'c', 'y', '\022', 'C', '\n', '\014', 'm', 'e', 's', +'s', 'a', 'g', 'e', '_', 't', 'y', 'p', 'e', '\030', '\004', ' ', '\003', '(', '\013', '2', ' ', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', +'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', 'R', +'\013', 'm', 'e', 's', 's', 'a', 'g', 'e', 'T', 'y', 'p', 'e', '\022', 'A', '\n', '\t', 'e', 'n', 'u', 'm', '_', 't', 'y', 'p', 'e', +'\030', '\005', ' ', '\003', '(', '\013', '2', '$', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', +'E', 'n', 'u', 'm', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', 'R', '\010', 'e', 'n', 'u', 'm', +'T', 'y', 'p', 'e', '\022', 'A', '\n', '\007', 's', 'e', 'r', 'v', 'i', 'c', 'e', '\030', '\006', ' ', '\003', '(', '\013', '2', '\'', '.', 'g', +'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'S', 'e', 'r', 'v', 'i', 'c', 'e', 'D', 'e', 's', +'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', 'R', '\007', 's', 'e', 'r', 'v', 'i', 'c', 'e', '\022', 'C', '\n', '\t', +'e', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', '\030', '\007', ' ', '\003', '(', '\013', '2', '%', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', +'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', 'd', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', +'r', 'o', 't', 'o', 'R', '\t', 'e', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', '\022', '6', '\n', '\007', 'o', 'p', 't', 'i', 'o', 'n', +'s', '\030', '\010', ' ', '\001', '(', '\013', '2', '\034', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', +'.', 'F', 'i', 'l', 'e', 'O', 'p', 't', 'i', 'o', 'n', 's', 'R', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\022', 'I', '\n', '\020', +'s', 'o', 'u', 'r', 'c', 'e', '_', 'c', 'o', 'd', 'e', '_', 'i', 'n', 'f', 'o', '\030', '\t', ' ', '\001', '(', '\013', '2', '\037', '.', +'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'S', 'o', 'u', 'r', 'c', 'e', 'C', 'o', 'd', +'e', 'I', 'n', 'f', 'o', 'R', '\016', 's', 'o', 'u', 'r', 'c', 'e', 'C', 'o', 'd', 'e', 'I', 'n', 'f', 'o', '\022', '\026', '\n', '\006', +'s', 'y', 'n', 't', 'a', 'x', '\030', '\014', ' ', '\001', '(', '\t', 'R', '\006', 's', 'y', 'n', 't', 'a', 'x', '\022', '2', '\n', '\007', 'e', +'d', 'i', 't', 'i', 'o', 'n', '\030', '\016', ' ', '\001', '(', '\016', '2', '\030', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', +'t', 'o', 'b', 'u', 'f', '.', 'E', 'd', 'i', 't', 'i', 'o', 'n', 'R', '\007', 'e', 'd', 'i', 't', 'i', 'o', 'n', '\"', '\271', '\006', +'\n', '\017', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', '\022', '\022', '\n', '\004', 'n', 'a', 'm', 'e', +'\030', '\001', ' ', '\001', '(', '\t', 'R', '\004', 'n', 'a', 'm', 'e', '\022', ';', '\n', '\005', 'f', 'i', 'e', 'l', 'd', '\030', '\002', ' ', '\003', +'(', '\013', '2', '%', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', +'d', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', 'R', '\005', 'f', 'i', 'e', 'l', 'd', '\022', 'C', +'\n', '\t', 'e', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', '\030', '\006', ' ', '\003', '(', '\013', '2', '%', '.', 'g', 'o', 'o', 'g', 'l', +'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', 'd', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', +'r', 'P', 'r', 'o', 't', 'o', 'R', '\t', 'e', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', '\022', 'A', '\n', '\013', 'n', 'e', 's', 't', +'e', 'd', '_', 't', 'y', 'p', 'e', '\030', '\003', ' ', '\003', '(', '\013', '2', ' ', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', +'o', 't', 'o', 'b', 'u', 'f', '.', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', 'R', '\n', 'n', +'e', 's', 't', 'e', 'd', 'T', 'y', 'p', 'e', '\022', 'A', '\n', '\t', 'e', 'n', 'u', 'm', '_', 't', 'y', 'p', 'e', '\030', '\004', ' ', +'\003', '(', '\013', '2', '$', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'E', 'n', 'u', +'m', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', 'R', '\010', 'e', 'n', 'u', 'm', 'T', 'y', 'p', +'e', '\022', 'X', '\n', '\017', 'e', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', '_', 'r', 'a', 'n', 'g', 'e', '\030', '\005', ' ', '\003', '(', +'\013', '2', '/', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'D', 'e', 's', 'c', 'r', +'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', '.', 'E', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', 'R', 'a', 'n', 'g', 'e', +'R', '\016', 'e', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', 'R', 'a', 'n', 'g', 'e', '\022', 'D', '\n', '\n', 'o', 'n', 'e', 'o', 'f', +'_', 'd', 'e', 'c', 'l', '\030', '\010', ' ', '\003', '(', '\013', '2', '%', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', +'o', 'b', 'u', 'f', '.', 'O', 'n', 'e', 'o', 'f', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', +'R', '\t', 'o', 'n', 'e', 'o', 'f', 'D', 'e', 'c', 'l', '\022', '9', '\n', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\030', '\007', ' ', +'\001', '(', '\013', '2', '\037', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'M', 'e', 's', +'s', 'a', 'g', 'e', 'O', 'p', 't', 'i', 'o', 'n', 's', 'R', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\022', 'U', '\n', '\016', 'r', +'e', 's', 'e', 'r', 'v', 'e', 'd', '_', 'r', 'a', 'n', 'g', 'e', '\030', '\t', ' ', '\003', '(', '\013', '2', '.', '.', 'g', 'o', 'o', +'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', +'o', 't', 'o', '.', 'R', 'e', 's', 'e', 'r', 'v', 'e', 'd', 'R', 'a', 'n', 'g', 'e', 'R', '\r', 'r', 'e', 's', 'e', 'r', 'v', +'e', 'd', 'R', 'a', 'n', 'g', 'e', '\022', '#', '\n', '\r', 'r', 'e', 's', 'e', 'r', 'v', 'e', 'd', '_', 'n', 'a', 'm', 'e', '\030', +'\n', ' ', '\003', '(', '\t', 'R', '\014', 'r', 'e', 's', 'e', 'r', 'v', 'e', 'd', 'N', 'a', 'm', 'e', '\032', 'z', '\n', '\016', 'E', 'x', +'t', 'e', 'n', 's', 'i', 'o', 'n', 'R', 'a', 'n', 'g', 'e', '\022', '\024', '\n', '\005', 's', 't', 'a', 'r', 't', '\030', '\001', ' ', '\001', +'(', '\005', 'R', '\005', 's', 't', 'a', 'r', 't', '\022', '\020', '\n', '\003', 'e', 'n', 'd', '\030', '\002', ' ', '\001', '(', '\005', 'R', '\003', 'e', +'n', 'd', '\022', '@', '\n', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\030', '\003', ' ', '\001', '(', '\013', '2', '&', '.', 'g', 'o', 'o', +'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'E', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', 'R', 'a', 'n', +'g', 'e', 'O', 'p', 't', 'i', 'o', 'n', 's', 'R', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\032', '7', '\n', '\r', 'R', 'e', 's', +'e', 'r', 'v', 'e', 'd', 'R', 'a', 'n', 'g', 'e', '\022', '\024', '\n', '\005', 's', 't', 'a', 'r', 't', '\030', '\001', ' ', '\001', '(', '\005', +'R', '\005', 's', 't', 'a', 'r', 't', '\022', '\020', '\n', '\003', 'e', 'n', 'd', '\030', '\002', ' ', '\001', '(', '\005', 'R', '\003', 'e', 'n', 'd', +'\"', '\314', '\004', '\n', '\025', 'E', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', 'R', 'a', 'n', 'g', 'e', 'O', 'p', 't', 'i', 'o', 'n', +'s', '\022', 'X', '\n', '\024', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', '_', 'o', 'p', 't', 'i', 'o', 'n', +'\030', '\347', '\007', ' ', '\003', '(', '\013', '2', '$', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', +'.', 'U', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', 'R', '\023', 'u', 'n', 'i', +'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', '\022', 'Y', '\n', '\013', 'd', 'e', 'c', 'l', 'a', +'r', 'a', 't', 'i', 'o', 'n', '\030', '\002', ' ', '\003', '(', '\013', '2', '2', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', +'t', 'o', 'b', 'u', 'f', '.', 'E', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', 'R', 'a', 'n', 'g', 'e', 'O', 'p', 't', 'i', 'o', +'n', 's', '.', 'D', 'e', 'c', 'l', 'a', 'r', 'a', 't', 'i', 'o', 'n', 'B', '\003', '\210', '\001', '\002', 'R', '\013', 'd', 'e', 'c', 'l', +'a', 'r', 'a', 't', 'i', 'o', 'n', '\022', '7', '\n', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\030', '2', ' ', '\001', '(', '\013', +'2', '\033', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', +'e', 'S', 'e', 't', 'R', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\022', 'm', '\n', '\014', 'v', 'e', 'r', 'i', 'f', 'i', 'c', +'a', 't', 'i', 'o', 'n', '\030', '\003', ' ', '\001', '(', '\016', '2', '8', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', +'o', 'b', 'u', 'f', '.', 'E', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', 'R', 'a', 'n', 'g', 'e', 'O', 'p', 't', 'i', 'o', 'n', +'s', '.', 'V', 'e', 'r', 'i', 'f', 'i', 'c', 'a', 't', 'i', 'o', 'n', 'S', 't', 'a', 't', 'e', ':', '\n', 'U', 'N', 'V', 'E', +'R', 'I', 'F', 'I', 'E', 'D', 'B', '\003', '\210', '\001', '\002', 'R', '\014', 'v', 'e', 'r', 'i', 'f', 'i', 'c', 'a', 't', 'i', 'o', 'n', +'\032', '\224', '\001', '\n', '\013', 'D', 'e', 'c', 'l', 'a', 'r', 'a', 't', 'i', 'o', 'n', '\022', '\026', '\n', '\006', 'n', 'u', 'm', 'b', 'e', +'r', '\030', '\001', ' ', '\001', '(', '\005', 'R', '\006', 'n', 'u', 'm', 'b', 'e', 'r', '\022', '\033', '\n', '\t', 'f', 'u', 'l', 'l', '_', 'n', +'a', 'm', 'e', '\030', '\002', ' ', '\001', '(', '\t', 'R', '\010', 'f', 'u', 'l', 'l', 'N', 'a', 'm', 'e', '\022', '\022', '\n', '\004', 't', 'y', +'p', 'e', '\030', '\003', ' ', '\001', '(', '\t', 'R', '\004', 't', 'y', 'p', 'e', '\022', '\032', '\n', '\010', 'r', 'e', 's', 'e', 'r', 'v', 'e', +'d', '\030', '\005', ' ', '\001', '(', '\010', 'R', '\010', 'r', 'e', 's', 'e', 'r', 'v', 'e', 'd', '\022', '\032', '\n', '\010', 'r', 'e', 'p', 'e', +'a', 't', 'e', 'd', '\030', '\006', ' ', '\001', '(', '\010', 'R', '\010', 'r', 'e', 'p', 'e', 'a', 't', 'e', 'd', 'J', '\004', '\010', '\004', '\020', +'\005', '\"', '4', '\n', '\021', 'V', 'e', 'r', 'i', 'f', 'i', 'c', 'a', 't', 'i', 'o', 'n', 'S', 't', 'a', 't', 'e', '\022', '\017', '\n', +'\013', 'D', 'E', 'C', 'L', 'A', 'R', 'A', 'T', 'I', 'O', 'N', '\020', '\000', '\022', '\016', '\n', '\n', 'U', 'N', 'V', 'E', 'R', 'I', 'F', +'I', 'E', 'D', '\020', '\001', '*', '\t', '\010', '\350', '\007', '\020', '\200', '\200', '\200', '\200', '\002', '\"', '\301', '\006', '\n', '\024', 'F', 'i', 'e', 'l', +'d', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', '\022', '\022', '\n', '\004', 'n', 'a', 'm', 'e', '\030', +'\001', ' ', '\001', '(', '\t', 'R', '\004', 'n', 'a', 'm', 'e', '\022', '\026', '\n', '\006', 'n', 'u', 'm', 'b', 'e', 'r', '\030', '\003', ' ', '\001', +'(', '\005', 'R', '\006', 'n', 'u', 'm', 'b', 'e', 'r', '\022', 'A', '\n', '\005', 'l', 'a', 'b', 'e', 'l', '\030', '\004', ' ', '\001', '(', '\016', +'2', '+', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', 'd', 'D', +'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', '.', 'L', 'a', 'b', 'e', 'l', 'R', '\005', 'l', 'a', 'b', +'e', 'l', '\022', '>', '\n', '\004', 't', 'y', 'p', 'e', '\030', '\005', ' ', '\001', '(', '\016', '2', '*', '.', 'g', 'o', 'o', 'g', 'l', 'e', +'.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', 'd', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', +'P', 'r', 'o', 't', 'o', '.', 'T', 'y', 'p', 'e', 'R', '\004', 't', 'y', 'p', 'e', '\022', '\033', '\n', '\t', 't', 'y', 'p', 'e', '_', +'n', 'a', 'm', 'e', '\030', '\006', ' ', '\001', '(', '\t', 'R', '\010', 't', 'y', 'p', 'e', 'N', 'a', 'm', 'e', '\022', '\032', '\n', '\010', 'e', +'x', 't', 'e', 'n', 'd', 'e', 'e', '\030', '\002', ' ', '\001', '(', '\t', 'R', '\010', 'e', 'x', 't', 'e', 'n', 'd', 'e', 'e', '\022', '#', +'\n', '\r', 'd', 'e', 'f', 'a', 'u', 'l', 't', '_', 'v', 'a', 'l', 'u', 'e', '\030', '\007', ' ', '\001', '(', '\t', 'R', '\014', 'd', 'e', +'f', 'a', 'u', 'l', 't', 'V', 'a', 'l', 'u', 'e', '\022', '\037', '\n', '\013', 'o', 'n', 'e', 'o', 'f', '_', 'i', 'n', 'd', 'e', 'x', +'\030', '\t', ' ', '\001', '(', '\005', 'R', '\n', 'o', 'n', 'e', 'o', 'f', 'I', 'n', 'd', 'e', 'x', '\022', '\033', '\n', '\t', 'j', 's', 'o', +'n', '_', 'n', 'a', 'm', 'e', '\030', '\n', ' ', '\001', '(', '\t', 'R', '\010', 'j', 's', 'o', 'n', 'N', 'a', 'm', 'e', '\022', '7', '\n', +'\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\030', '\010', ' ', '\001', '(', '\013', '2', '\035', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', +'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', 'd', 'O', 'p', 't', 'i', 'o', 'n', 's', 'R', '\007', 'o', 'p', 't', +'i', 'o', 'n', 's', '\022', '\'', '\n', '\017', 'p', 'r', 'o', 't', 'o', '3', '_', 'o', 'p', 't', 'i', 'o', 'n', 'a', 'l', '\030', '\021', +' ', '\001', '(', '\010', 'R', '\016', 'p', 'r', 'o', 't', 'o', '3', 'O', 'p', 't', 'i', 'o', 'n', 'a', 'l', '\"', '\266', '\002', '\n', '\004', +'T', 'y', 'p', 'e', '\022', '\017', '\n', '\013', 'T', 'Y', 'P', 'E', '_', 'D', 'O', 'U', 'B', 'L', 'E', '\020', '\001', '\022', '\016', '\n', '\n', +'T', 'Y', 'P', 'E', '_', 'F', 'L', 'O', 'A', 'T', '\020', '\002', '\022', '\016', '\n', '\n', 'T', 'Y', 'P', 'E', '_', 'I', 'N', 'T', '6', +'4', '\020', '\003', '\022', '\017', '\n', '\013', 'T', 'Y', 'P', 'E', '_', 'U', 'I', 'N', 'T', '6', '4', '\020', '\004', '\022', '\016', '\n', '\n', 'T', +'Y', 'P', 'E', '_', 'I', 'N', 'T', '3', '2', '\020', '\005', '\022', '\020', '\n', '\014', 'T', 'Y', 'P', 'E', '_', 'F', 'I', 'X', 'E', 'D', +'6', '4', '\020', '\006', '\022', '\020', '\n', '\014', 'T', 'Y', 'P', 'E', '_', 'F', 'I', 'X', 'E', 'D', '3', '2', '\020', '\007', '\022', '\r', '\n', +'\t', 'T', 'Y', 'P', 'E', '_', 'B', 'O', 'O', 'L', '\020', '\010', '\022', '\017', '\n', '\013', 'T', 'Y', 'P', 'E', '_', 'S', 'T', 'R', 'I', +'N', 'G', '\020', '\t', '\022', '\016', '\n', '\n', 'T', 'Y', 'P', 'E', '_', 'G', 'R', 'O', 'U', 'P', '\020', '\n', '\022', '\020', '\n', '\014', 'T', +'Y', 'P', 'E', '_', 'M', 'E', 'S', 'S', 'A', 'G', 'E', '\020', '\013', '\022', '\016', '\n', '\n', 'T', 'Y', 'P', 'E', '_', 'B', 'Y', 'T', +'E', 'S', '\020', '\014', '\022', '\017', '\n', '\013', 'T', 'Y', 'P', 'E', '_', 'U', 'I', 'N', 'T', '3', '2', '\020', '\r', '\022', '\r', '\n', '\t', +'T', 'Y', 'P', 'E', '_', 'E', 'N', 'U', 'M', '\020', '\016', '\022', '\021', '\n', '\r', 'T', 'Y', 'P', 'E', '_', 'S', 'F', 'I', 'X', 'E', +'D', '3', '2', '\020', '\017', '\022', '\021', '\n', '\r', 'T', 'Y', 'P', 'E', '_', 'S', 'F', 'I', 'X', 'E', 'D', '6', '4', '\020', '\020', '\022', +'\017', '\n', '\013', 'T', 'Y', 'P', 'E', '_', 'S', 'I', 'N', 'T', '3', '2', '\020', '\021', '\022', '\017', '\n', '\013', 'T', 'Y', 'P', 'E', '_', +'S', 'I', 'N', 'T', '6', '4', '\020', '\022', '\"', 'C', '\n', '\005', 'L', 'a', 'b', 'e', 'l', '\022', '\022', '\n', '\016', 'L', 'A', 'B', 'E', +'L', '_', 'O', 'P', 'T', 'I', 'O', 'N', 'A', 'L', '\020', '\001', '\022', '\022', '\n', '\016', 'L', 'A', 'B', 'E', 'L', '_', 'R', 'E', 'P', +'E', 'A', 'T', 'E', 'D', '\020', '\003', '\022', '\022', '\n', '\016', 'L', 'A', 'B', 'E', 'L', '_', 'R', 'E', 'Q', 'U', 'I', 'R', 'E', 'D', +'\020', '\002', '\"', 'c', '\n', '\024', 'O', 'n', 'e', 'o', 'f', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', +'o', '\022', '\022', '\n', '\004', 'n', 'a', 'm', 'e', '\030', '\001', ' ', '\001', '(', '\t', 'R', '\004', 'n', 'a', 'm', 'e', '\022', '7', '\n', '\007', +'o', 'p', 't', 'i', 'o', 'n', 's', '\030', '\002', ' ', '\001', '(', '\013', '2', '\035', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', +'o', 't', 'o', 'b', 'u', 'f', '.', 'O', 'n', 'e', 'o', 'f', 'O', 'p', 't', 'i', 'o', 'n', 's', 'R', '\007', 'o', 'p', 't', 'i', +'o', 'n', 's', '\"', '\343', '\002', '\n', '\023', 'E', 'n', 'u', 'm', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', +'t', 'o', '\022', '\022', '\n', '\004', 'n', 'a', 'm', 'e', '\030', '\001', ' ', '\001', '(', '\t', 'R', '\004', 'n', 'a', 'm', 'e', '\022', '?', '\n', +'\005', 'v', 'a', 'l', 'u', 'e', '\030', '\002', ' ', '\003', '(', '\013', '2', ')', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', +'t', 'o', 'b', 'u', 'f', '.', 'E', 'n', 'u', 'm', 'V', 'a', 'l', 'u', 'e', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', +'P', 'r', 'o', 't', 'o', 'R', '\005', 'v', 'a', 'l', 'u', 'e', '\022', '6', '\n', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\030', '\003', +' ', '\001', '(', '\013', '2', '\034', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'E', 'n', +'u', 'm', 'O', 'p', 't', 'i', 'o', 'n', 's', 'R', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\022', ']', '\n', '\016', 'r', 'e', 's', +'e', 'r', 'v', 'e', 'd', '_', 'r', 'a', 'n', 'g', 'e', '\030', '\004', ' ', '\003', '(', '\013', '2', '6', '.', 'g', 'o', 'o', 'g', 'l', +'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'E', 'n', 'u', 'm', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', +'P', 'r', 'o', 't', 'o', '.', 'E', 'n', 'u', 'm', 'R', 'e', 's', 'e', 'r', 'v', 'e', 'd', 'R', 'a', 'n', 'g', 'e', 'R', '\r', +'r', 'e', 's', 'e', 'r', 'v', 'e', 'd', 'R', 'a', 'n', 'g', 'e', '\022', '#', '\n', '\r', 'r', 'e', 's', 'e', 'r', 'v', 'e', 'd', +'_', 'n', 'a', 'm', 'e', '\030', '\005', ' ', '\003', '(', '\t', 'R', '\014', 'r', 'e', 's', 'e', 'r', 'v', 'e', 'd', 'N', 'a', 'm', 'e', +'\032', ';', '\n', '\021', 'E', 'n', 'u', 'm', 'R', 'e', 's', 'e', 'r', 'v', 'e', 'd', 'R', 'a', 'n', 'g', 'e', '\022', '\024', '\n', '\005', +'s', 't', 'a', 'r', 't', '\030', '\001', ' ', '\001', '(', '\005', 'R', '\005', 's', 't', 'a', 'r', 't', '\022', '\020', '\n', '\003', 'e', 'n', 'd', +'\030', '\002', ' ', '\001', '(', '\005', 'R', '\003', 'e', 'n', 'd', '\"', '\203', '\001', '\n', '\030', 'E', 'n', 'u', 'm', 'V', 'a', 'l', 'u', 'e', +'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', '\022', '\022', '\n', '\004', 'n', 'a', 'm', 'e', '\030', '\001', +' ', '\001', '(', '\t', 'R', '\004', 'n', 'a', 'm', 'e', '\022', '\026', '\n', '\006', 'n', 'u', 'm', 'b', 'e', 'r', '\030', '\002', ' ', '\001', '(', +'\005', 'R', '\006', 'n', 'u', 'm', 'b', 'e', 'r', '\022', ';', '\n', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\030', '\003', ' ', '\001', '(', +'\013', '2', '!', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'E', 'n', 'u', 'm', 'V', +'a', 'l', 'u', 'e', 'O', 'p', 't', 'i', 'o', 'n', 's', 'R', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\"', '\247', '\001', '\n', '\026', +'S', 'e', 'r', 'v', 'i', 'c', 'e', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', '\022', '\022', '\n', +'\004', 'n', 'a', 'm', 'e', '\030', '\001', ' ', '\001', '(', '\t', 'R', '\004', 'n', 'a', 'm', 'e', '\022', '>', '\n', '\006', 'm', 'e', 't', 'h', +'o', 'd', '\030', '\002', ' ', '\003', '(', '\013', '2', '&', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', +'f', '.', 'M', 'e', 't', 'h', 'o', 'd', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', 'R', '\006', +'m', 'e', 't', 'h', 'o', 'd', '\022', '9', '\n', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\030', '\003', ' ', '\001', '(', '\013', '2', '\037', +'.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'S', 'e', 'r', 'v', 'i', 'c', 'e', 'O', +'p', 't', 'i', 'o', 'n', 's', 'R', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\"', '\211', '\002', '\n', '\025', 'M', 'e', 't', 'h', 'o', +'d', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'P', 'r', 'o', 't', 'o', '\022', '\022', '\n', '\004', 'n', 'a', 'm', 'e', '\030', +'\001', ' ', '\001', '(', '\t', 'R', '\004', 'n', 'a', 'm', 'e', '\022', '\035', '\n', '\n', 'i', 'n', 'p', 'u', 't', '_', 't', 'y', 'p', 'e', +'\030', '\002', ' ', '\001', '(', '\t', 'R', '\t', 'i', 'n', 'p', 'u', 't', 'T', 'y', 'p', 'e', '\022', '\037', '\n', '\013', 'o', 'u', 't', 'p', +'u', 't', '_', 't', 'y', 'p', 'e', '\030', '\003', ' ', '\001', '(', '\t', 'R', '\n', 'o', 'u', 't', 'p', 'u', 't', 'T', 'y', 'p', 'e', +'\022', '8', '\n', '\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\030', '\004', ' ', '\001', '(', '\013', '2', '\036', '.', 'g', 'o', 'o', 'g', 'l', +'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'M', 'e', 't', 'h', 'o', 'd', 'O', 'p', 't', 'i', 'o', 'n', 's', 'R', +'\007', 'o', 'p', 't', 'i', 'o', 'n', 's', '\022', '0', '\n', '\020', 'c', 'l', 'i', 'e', 'n', 't', '_', 's', 't', 'r', 'e', 'a', 'm', +'i', 'n', 'g', '\030', '\005', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\017', 'c', 'l', 'i', 'e', 'n', 't', 'S', +'t', 'r', 'e', 'a', 'm', 'i', 'n', 'g', '\022', '0', '\n', '\020', 's', 'e', 'r', 'v', 'e', 'r', '_', 's', 't', 'r', 'e', 'a', 'm', +'i', 'n', 'g', '\030', '\006', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\017', 's', 'e', 'r', 'v', 'e', 'r', 'S', +'t', 'r', 'e', 'a', 'm', 'i', 'n', 'g', '\"', '\255', '\t', '\n', '\013', 'F', 'i', 'l', 'e', 'O', 'p', 't', 'i', 'o', 'n', 's', '\022', +'!', '\n', '\014', 'j', 'a', 'v', 'a', '_', 'p', 'a', 'c', 'k', 'a', 'g', 'e', '\030', '\001', ' ', '\001', '(', '\t', 'R', '\013', 'j', 'a', +'v', 'a', 'P', 'a', 'c', 'k', 'a', 'g', 'e', '\022', '0', '\n', '\024', 'j', 'a', 'v', 'a', '_', 'o', 'u', 't', 'e', 'r', '_', 'c', +'l', 'a', 's', 's', 'n', 'a', 'm', 'e', '\030', '\010', ' ', '\001', '(', '\t', 'R', '\022', 'j', 'a', 'v', 'a', 'O', 'u', 't', 'e', 'r', +'C', 'l', 'a', 's', 's', 'n', 'a', 'm', 'e', '\022', '5', '\n', '\023', 'j', 'a', 'v', 'a', '_', 'm', 'u', 'l', 't', 'i', 'p', 'l', +'e', '_', 'f', 'i', 'l', 'e', 's', '\030', '\n', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\021', 'j', 'a', 'v', +'a', 'M', 'u', 'l', 't', 'i', 'p', 'l', 'e', 'F', 'i', 'l', 'e', 's', '\022', 'D', '\n', '\035', 'j', 'a', 'v', 'a', '_', 'g', 'e', +'n', 'e', 'r', 'a', 't', 'e', '_', 'e', 'q', 'u', 'a', 'l', 's', '_', 'a', 'n', 'd', '_', 'h', 'a', 's', 'h', '\030', '\024', ' ', +'\001', '(', '\010', 'B', '\002', '\030', '\001', 'R', '\031', 'j', 'a', 'v', 'a', 'G', 'e', 'n', 'e', 'r', 'a', 't', 'e', 'E', 'q', 'u', 'a', +'l', 's', 'A', 'n', 'd', 'H', 'a', 's', 'h', '\022', ':', '\n', '\026', 'j', 'a', 'v', 'a', '_', 's', 't', 'r', 'i', 'n', 'g', '_', +'c', 'h', 'e', 'c', 'k', '_', 'u', 't', 'f', '8', '\030', '\033', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\023', +'j', 'a', 'v', 'a', 'S', 't', 'r', 'i', 'n', 'g', 'C', 'h', 'e', 'c', 'k', 'U', 't', 'f', '8', '\022', 'S', '\n', '\014', 'o', 'p', +'t', 'i', 'm', 'i', 'z', 'e', '_', 'f', 'o', 'r', '\030', '\t', ' ', '\001', '(', '\016', '2', ')', '.', 'g', 'o', 'o', 'g', 'l', 'e', +'.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'l', 'e', 'O', 'p', 't', 'i', 'o', 'n', 's', '.', 'O', 'p', 't', +'i', 'm', 'i', 'z', 'e', 'M', 'o', 'd', 'e', ':', '\005', 'S', 'P', 'E', 'E', 'D', 'R', '\013', 'o', 'p', 't', 'i', 'm', 'i', 'z', +'e', 'F', 'o', 'r', '\022', '\035', '\n', '\n', 'g', 'o', '_', 'p', 'a', 'c', 'k', 'a', 'g', 'e', '\030', '\013', ' ', '\001', '(', '\t', 'R', +'\t', 'g', 'o', 'P', 'a', 'c', 'k', 'a', 'g', 'e', '\022', '5', '\n', '\023', 'c', 'c', '_', 'g', 'e', 'n', 'e', 'r', 'i', 'c', '_', +'s', 'e', 'r', 'v', 'i', 'c', 'e', 's', '\030', '\020', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\021', 'c', 'c', +'G', 'e', 'n', 'e', 'r', 'i', 'c', 'S', 'e', 'r', 'v', 'i', 'c', 'e', 's', '\022', '9', '\n', '\025', 'j', 'a', 'v', 'a', '_', 'g', +'e', 'n', 'e', 'r', 'i', 'c', '_', 's', 'e', 'r', 'v', 'i', 'c', 'e', 's', '\030', '\021', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', +'l', 's', 'e', 'R', '\023', 'j', 'a', 'v', 'a', 'G', 'e', 'n', 'e', 'r', 'i', 'c', 'S', 'e', 'r', 'v', 'i', 'c', 'e', 's', '\022', +'5', '\n', '\023', 'p', 'y', '_', 'g', 'e', 'n', 'e', 'r', 'i', 'c', '_', 's', 'e', 'r', 'v', 'i', 'c', 'e', 's', '\030', '\022', ' ', +'\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\021', 'p', 'y', 'G', 'e', 'n', 'e', 'r', 'i', 'c', 'S', 'e', 'r', 'v', +'i', 'c', 'e', 's', '\022', '%', '\n', '\n', 'd', 'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', '\030', '\027', ' ', '\001', '(', '\010', ':', +'\005', 'f', 'a', 'l', 's', 'e', 'R', '\n', 'd', 'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', '\022', '.', '\n', '\020', 'c', 'c', '_', +'e', 'n', 'a', 'b', 'l', 'e', '_', 'a', 'r', 'e', 'n', 'a', 's', '\030', '\037', ' ', '\001', '(', '\010', ':', '\004', 't', 'r', 'u', 'e', +'R', '\016', 'c', 'c', 'E', 'n', 'a', 'b', 'l', 'e', 'A', 'r', 'e', 'n', 'a', 's', '\022', '*', '\n', '\021', 'o', 'b', 'j', 'c', '_', +'c', 'l', 'a', 's', 's', '_', 'p', 'r', 'e', 'f', 'i', 'x', '\030', '$', ' ', '\001', '(', '\t', 'R', '\017', 'o', 'b', 'j', 'c', 'C', +'l', 'a', 's', 's', 'P', 'r', 'e', 'f', 'i', 'x', '\022', ')', '\n', '\020', 'c', 's', 'h', 'a', 'r', 'p', '_', 'n', 'a', 'm', 'e', +'s', 'p', 'a', 'c', 'e', '\030', '%', ' ', '\001', '(', '\t', 'R', '\017', 'c', 's', 'h', 'a', 'r', 'p', 'N', 'a', 'm', 'e', 's', 'p', +'a', 'c', 'e', '\022', '!', '\n', '\014', 's', 'w', 'i', 'f', 't', '_', 'p', 'r', 'e', 'f', 'i', 'x', '\030', '\'', ' ', '\001', '(', '\t', +'R', '\013', 's', 'w', 'i', 'f', 't', 'P', 'r', 'e', 'f', 'i', 'x', '\022', '(', '\n', '\020', 'p', 'h', 'p', '_', 'c', 'l', 'a', 's', +'s', '_', 'p', 'r', 'e', 'f', 'i', 'x', '\030', '(', ' ', '\001', '(', '\t', 'R', '\016', 'p', 'h', 'p', 'C', 'l', 'a', 's', 's', 'P', +'r', 'e', 'f', 'i', 'x', '\022', '#', '\n', '\r', 'p', 'h', 'p', '_', 'n', 'a', 'm', 'e', 's', 'p', 'a', 'c', 'e', '\030', ')', ' ', +'\001', '(', '\t', 'R', '\014', 'p', 'h', 'p', 'N', 'a', 'm', 'e', 's', 'p', 'a', 'c', 'e', '\022', '4', '\n', '\026', 'p', 'h', 'p', '_', +'m', 'e', 't', 'a', 'd', 'a', 't', 'a', '_', 'n', 'a', 'm', 'e', 's', 'p', 'a', 'c', 'e', '\030', ',', ' ', '\001', '(', '\t', 'R', +'\024', 'p', 'h', 'p', 'M', 'e', 't', 'a', 'd', 'a', 't', 'a', 'N', 'a', 'm', 'e', 's', 'p', 'a', 'c', 'e', '\022', '!', '\n', '\014', +'r', 'u', 'b', 'y', '_', 'p', 'a', 'c', 'k', 'a', 'g', 'e', '\030', '-', ' ', '\001', '(', '\t', 'R', '\013', 'r', 'u', 'b', 'y', 'P', +'a', 'c', 'k', 'a', 'g', 'e', '\022', '7', '\n', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\030', '2', ' ', '\001', '(', '\013', '2', +'\033', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', +'S', 'e', 't', 'R', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\022', 'X', '\n', '\024', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', +'r', 'e', 't', 'e', 'd', '_', 'o', 'p', 't', 'i', 'o', 'n', '\030', '\347', '\007', ' ', '\003', '(', '\013', '2', '$', '.', 'g', 'o', 'o', +'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'U', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', +'d', 'O', 'p', 't', 'i', 'o', 'n', 'R', '\023', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', +'i', 'o', 'n', '\"', ':', '\n', '\014', 'O', 'p', 't', 'i', 'm', 'i', 'z', 'e', 'M', 'o', 'd', 'e', '\022', '\t', '\n', '\005', 'S', 'P', +'E', 'E', 'D', '\020', '\001', '\022', '\r', '\n', '\t', 'C', 'O', 'D', 'E', '_', 'S', 'I', 'Z', 'E', '\020', '\002', '\022', '\020', '\n', '\014', 'L', +'I', 'T', 'E', '_', 'R', 'U', 'N', 'T', 'I', 'M', 'E', '\020', '\003', '*', '\t', '\010', '\350', '\007', '\020', '\200', '\200', '\200', '\200', '\002', 'J', +'\004', '\010', '*', '\020', '+', 'J', '\004', '\010', '&', '\020', '\'', 'R', '\024', 'p', 'h', 'p', '_', 'g', 'e', 'n', 'e', 'r', 'i', 'c', '_', +'s', 'e', 'r', 'v', 'i', 'c', 'e', 's', '\"', '\364', '\003', '\n', '\016', 'M', 'e', 's', 's', 'a', 'g', 'e', 'O', 'p', 't', 'i', 'o', +'n', 's', '\022', '<', '\n', '\027', 'm', 'e', 's', 's', 'a', 'g', 'e', '_', 's', 'e', 't', '_', 'w', 'i', 'r', 'e', '_', 'f', 'o', +'r', 'm', 'a', 't', '\030', '\001', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\024', 'm', 'e', 's', 's', 'a', 'g', +'e', 'S', 'e', 't', 'W', 'i', 'r', 'e', 'F', 'o', 'r', 'm', 'a', 't', '\022', 'L', '\n', '\037', 'n', 'o', '_', 's', 't', 'a', 'n', +'d', 'a', 'r', 'd', '_', 'd', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', '_', 'a', 'c', 'c', 'e', 's', 's', 'o', 'r', '\030', +'\002', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\034', 'n', 'o', 'S', 't', 'a', 'n', 'd', 'a', 'r', 'd', 'D', +'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', 'A', 'c', 'c', 'e', 's', 's', 'o', 'r', '\022', '%', '\n', '\n', 'd', 'e', 'p', 'r', +'e', 'c', 'a', 't', 'e', 'd', '\030', '\003', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\n', 'd', 'e', 'p', 'r', +'e', 'c', 'a', 't', 'e', 'd', '\022', '\033', '\n', '\t', 'm', 'a', 'p', '_', 'e', 'n', 't', 'r', 'y', '\030', '\007', ' ', '\001', '(', '\010', +'R', '\010', 'm', 'a', 'p', 'E', 'n', 't', 'r', 'y', '\022', 'V', '\n', '&', 'd', 'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', '_', +'l', 'e', 'g', 'a', 'c', 'y', '_', 'j', 's', 'o', 'n', '_', 'f', 'i', 'e', 'l', 'd', '_', 'c', 'o', 'n', 'f', 'l', 'i', 'c', +'t', 's', '\030', '\013', ' ', '\001', '(', '\010', 'B', '\002', '\030', '\001', 'R', '\"', 'd', 'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', 'L', +'e', 'g', 'a', 'c', 'y', 'J', 's', 'o', 'n', 'F', 'i', 'e', 'l', 'd', 'C', 'o', 'n', 'f', 'l', 'i', 'c', 't', 's', '\022', '7', +'\n', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\030', '\014', ' ', '\001', '(', '\013', '2', '\033', '.', 'g', 'o', 'o', 'g', 'l', 'e', +'.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', 'R', '\010', 'f', 'e', 'a', +'t', 'u', 'r', 'e', 's', '\022', 'X', '\n', '\024', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', '_', 'o', 'p', +'t', 'i', 'o', 'n', '\030', '\347', '\007', ' ', '\003', '(', '\013', '2', '$', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', +'o', 'b', 'u', 'f', '.', 'U', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', 'R', +'\023', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', '*', '\t', '\010', '\350', '\007', +'\020', '\200', '\200', '\200', '\200', '\002', 'J', '\004', '\010', '\004', '\020', '\005', 'J', '\004', '\010', '\005', '\020', '\006', 'J', '\004', '\010', '\006', '\020', '\007', 'J', +'\004', '\010', '\010', '\020', '\t', 'J', '\004', '\010', '\t', '\020', '\n', '\"', '\235', '\r', '\n', '\014', 'F', 'i', 'e', 'l', 'd', 'O', 'p', 't', 'i', +'o', 'n', 's', '\022', 'A', '\n', '\005', 'c', 't', 'y', 'p', 'e', '\030', '\001', ' ', '\001', '(', '\016', '2', '#', '.', 'g', 'o', 'o', 'g', +'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', 'd', 'O', 'p', 't', 'i', 'o', 'n', 's', '.', +'C', 'T', 'y', 'p', 'e', ':', '\006', 'S', 'T', 'R', 'I', 'N', 'G', 'R', '\005', 'c', 't', 'y', 'p', 'e', '\022', '\026', '\n', '\006', 'p', +'a', 'c', 'k', 'e', 'd', '\030', '\002', ' ', '\001', '(', '\010', 'R', '\006', 'p', 'a', 'c', 'k', 'e', 'd', '\022', 'G', '\n', '\006', 'j', 's', +'t', 'y', 'p', 'e', '\030', '\006', ' ', '\001', '(', '\016', '2', '$', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', +'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', 'd', 'O', 'p', 't', 'i', 'o', 'n', 's', '.', 'J', 'S', 'T', 'y', 'p', 'e', ':', '\t', +'J', 'S', '_', 'N', 'O', 'R', 'M', 'A', 'L', 'R', '\006', 'j', 's', 't', 'y', 'p', 'e', '\022', '\031', '\n', '\004', 'l', 'a', 'z', 'y', +'\030', '\005', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\004', 'l', 'a', 'z', 'y', '\022', '.', '\n', '\017', 'u', 'n', +'v', 'e', 'r', 'i', 'f', 'i', 'e', 'd', '_', 'l', 'a', 'z', 'y', '\030', '\017', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', +'e', 'R', '\016', 'u', 'n', 'v', 'e', 'r', 'i', 'f', 'i', 'e', 'd', 'L', 'a', 'z', 'y', '\022', '%', '\n', '\n', 'd', 'e', 'p', 'r', +'e', 'c', 'a', 't', 'e', 'd', '\030', '\003', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\n', 'd', 'e', 'p', 'r', +'e', 'c', 'a', 't', 'e', 'd', '\022', '\031', '\n', '\004', 'w', 'e', 'a', 'k', '\030', '\n', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', +'s', 'e', 'R', '\004', 'w', 'e', 'a', 'k', '\022', '(', '\n', '\014', 'd', 'e', 'b', 'u', 'g', '_', 'r', 'e', 'd', 'a', 'c', 't', '\030', +'\020', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\013', 'd', 'e', 'b', 'u', 'g', 'R', 'e', 'd', 'a', 'c', 't', +'\022', 'K', '\n', '\t', 'r', 'e', 't', 'e', 'n', 't', 'i', 'o', 'n', '\030', '\021', ' ', '\001', '(', '\016', '2', '-', '.', 'g', 'o', 'o', +'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', 'd', 'O', 'p', 't', 'i', 'o', 'n', 's', +'.', 'O', 'p', 't', 'i', 'o', 'n', 'R', 'e', 't', 'e', 'n', 't', 'i', 'o', 'n', 'R', '\t', 'r', 'e', 't', 'e', 'n', 't', 'i', +'o', 'n', '\022', 'H', '\n', '\007', 't', 'a', 'r', 'g', 'e', 't', 's', '\030', '\023', ' ', '\003', '(', '\016', '2', '.', '.', 'g', 'o', 'o', +'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', 'd', 'O', 'p', 't', 'i', 'o', 'n', 's', +'.', 'O', 'p', 't', 'i', 'o', 'n', 'T', 'a', 'r', 'g', 'e', 't', 'T', 'y', 'p', 'e', 'R', '\007', 't', 'a', 'r', 'g', 'e', 't', +'s', '\022', 'W', '\n', '\020', 'e', 'd', 'i', 't', 'i', 'o', 'n', '_', 'd', 'e', 'f', 'a', 'u', 'l', 't', 's', '\030', '\024', ' ', '\003', +'(', '\013', '2', ',', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', +'d', 'O', 'p', 't', 'i', 'o', 'n', 's', '.', 'E', 'd', 'i', 't', 'i', 'o', 'n', 'D', 'e', 'f', 'a', 'u', 'l', 't', 'R', '\017', +'e', 'd', 'i', 't', 'i', 'o', 'n', 'D', 'e', 'f', 'a', 'u', 'l', 't', 's', '\022', '7', '\n', '\010', 'f', 'e', 'a', 't', 'u', 'r', +'e', 's', '\030', '\025', ' ', '\001', '(', '\013', '2', '\033', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', +'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', 'R', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\022', 'U', '\n', +'\017', 'f', 'e', 'a', 't', 'u', 'r', 'e', '_', 's', 'u', 'p', 'p', 'o', 'r', 't', '\030', '\026', ' ', '\001', '(', '\013', '2', ',', '.', +'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'i', 'e', 'l', 'd', 'O', 'p', 't', 'i', +'o', 'n', 's', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'u', 'p', 'p', 'o', 'r', 't', 'R', '\016', 'f', 'e', 'a', 't', 'u', +'r', 'e', 'S', 'u', 'p', 'p', 'o', 'r', 't', '\022', 'X', '\n', '\024', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', +'d', '_', 'o', 'p', 't', 'i', 'o', 'n', '\030', '\347', '\007', ' ', '\003', '(', '\013', '2', '$', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', +'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'U', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', +'i', 'o', 'n', 'R', '\023', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', '\032', +'Z', '\n', '\016', 'E', 'd', 'i', 't', 'i', 'o', 'n', 'D', 'e', 'f', 'a', 'u', 'l', 't', '\022', '2', '\n', '\007', 'e', 'd', 'i', 't', +'i', 'o', 'n', '\030', '\003', ' ', '\001', '(', '\016', '2', '\030', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', +'u', 'f', '.', 'E', 'd', 'i', 't', 'i', 'o', 'n', 'R', '\007', 'e', 'd', 'i', 't', 'i', 'o', 'n', '\022', '\024', '\n', '\005', 'v', 'a', +'l', 'u', 'e', '\030', '\002', ' ', '\001', '(', '\t', 'R', '\005', 'v', 'a', 'l', 'u', 'e', '\032', '\226', '\002', '\n', '\016', 'F', 'e', 'a', 't', +'u', 'r', 'e', 'S', 'u', 'p', 'p', 'o', 'r', 't', '\022', 'G', '\n', '\022', 'e', 'd', 'i', 't', 'i', 'o', 'n', '_', 'i', 'n', 't', +'r', 'o', 'd', 'u', 'c', 'e', 'd', '\030', '\001', ' ', '\001', '(', '\016', '2', '\030', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', +'o', 't', 'o', 'b', 'u', 'f', '.', 'E', 'd', 'i', 't', 'i', 'o', 'n', 'R', '\021', 'e', 'd', 'i', 't', 'i', 'o', 'n', 'I', 'n', +'t', 'r', 'o', 'd', 'u', 'c', 'e', 'd', '\022', 'G', '\n', '\022', 'e', 'd', 'i', 't', 'i', 'o', 'n', '_', 'd', 'e', 'p', 'r', 'e', +'c', 'a', 't', 'e', 'd', '\030', '\002', ' ', '\001', '(', '\016', '2', '\030', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', +'o', 'b', 'u', 'f', '.', 'E', 'd', 'i', 't', 'i', 'o', 'n', 'R', '\021', 'e', 'd', 'i', 't', 'i', 'o', 'n', 'D', 'e', 'p', 'r', +'e', 'c', 'a', 't', 'e', 'd', '\022', '/', '\n', '\023', 'd', 'e', 'p', 'r', 'e', 'c', 'a', 't', 'i', 'o', 'n', '_', 'w', 'a', 'r', +'n', 'i', 'n', 'g', '\030', '\003', ' ', '\001', '(', '\t', 'R', '\022', 'd', 'e', 'p', 'r', 'e', 'c', 'a', 't', 'i', 'o', 'n', 'W', 'a', +'r', 'n', 'i', 'n', 'g', '\022', 'A', '\n', '\017', 'e', 'd', 'i', 't', 'i', 'o', 'n', '_', 'r', 'e', 'm', 'o', 'v', 'e', 'd', '\030', +'\004', ' ', '\001', '(', '\016', '2', '\030', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'E', +'d', 'i', 't', 'i', 'o', 'n', 'R', '\016', 'e', 'd', 'i', 't', 'i', 'o', 'n', 'R', 'e', 'm', 'o', 'v', 'e', 'd', '\"', '/', '\n', +'\005', 'C', 'T', 'y', 'p', 'e', '\022', '\n', '\n', '\006', 'S', 'T', 'R', 'I', 'N', 'G', '\020', '\000', '\022', '\010', '\n', '\004', 'C', 'O', 'R', +'D', '\020', '\001', '\022', '\020', '\n', '\014', 'S', 'T', 'R', 'I', 'N', 'G', '_', 'P', 'I', 'E', 'C', 'E', '\020', '\002', '\"', '5', '\n', '\006', +'J', 'S', 'T', 'y', 'p', 'e', '\022', '\r', '\n', '\t', 'J', 'S', '_', 'N', 'O', 'R', 'M', 'A', 'L', '\020', '\000', '\022', '\r', '\n', '\t', +'J', 'S', '_', 'S', 'T', 'R', 'I', 'N', 'G', '\020', '\001', '\022', '\r', '\n', '\t', 'J', 'S', '_', 'N', 'U', 'M', 'B', 'E', 'R', '\020', +'\002', '\"', 'U', '\n', '\017', 'O', 'p', 't', 'i', 'o', 'n', 'R', 'e', 't', 'e', 'n', 't', 'i', 'o', 'n', '\022', '\025', '\n', '\021', 'R', +'E', 'T', 'E', 'N', 'T', 'I', 'O', 'N', '_', 'U', 'N', 'K', 'N', 'O', 'W', 'N', '\020', '\000', '\022', '\025', '\n', '\021', 'R', 'E', 'T', +'E', 'N', 'T', 'I', 'O', 'N', '_', 'R', 'U', 'N', 'T', 'I', 'M', 'E', '\020', '\001', '\022', '\024', '\n', '\020', 'R', 'E', 'T', 'E', 'N', +'T', 'I', 'O', 'N', '_', 'S', 'O', 'U', 'R', 'C', 'E', '\020', '\002', '\"', '\214', '\002', '\n', '\020', 'O', 'p', 't', 'i', 'o', 'n', 'T', +'a', 'r', 'g', 'e', 't', 'T', 'y', 'p', 'e', '\022', '\027', '\n', '\023', 'T', 'A', 'R', 'G', 'E', 'T', '_', 'T', 'Y', 'P', 'E', '_', +'U', 'N', 'K', 'N', 'O', 'W', 'N', '\020', '\000', '\022', '\024', '\n', '\020', 'T', 'A', 'R', 'G', 'E', 'T', '_', 'T', 'Y', 'P', 'E', '_', +'F', 'I', 'L', 'E', '\020', '\001', '\022', '\037', '\n', '\033', 'T', 'A', 'R', 'G', 'E', 'T', '_', 'T', 'Y', 'P', 'E', '_', 'E', 'X', 'T', +'E', 'N', 'S', 'I', 'O', 'N', '_', 'R', 'A', 'N', 'G', 'E', '\020', '\002', '\022', '\027', '\n', '\023', 'T', 'A', 'R', 'G', 'E', 'T', '_', +'T', 'Y', 'P', 'E', '_', 'M', 'E', 'S', 'S', 'A', 'G', 'E', '\020', '\003', '\022', '\025', '\n', '\021', 'T', 'A', 'R', 'G', 'E', 'T', '_', +'T', 'Y', 'P', 'E', '_', 'F', 'I', 'E', 'L', 'D', '\020', '\004', '\022', '\025', '\n', '\021', 'T', 'A', 'R', 'G', 'E', 'T', '_', 'T', 'Y', +'P', 'E', '_', 'O', 'N', 'E', 'O', 'F', '\020', '\005', '\022', '\024', '\n', '\020', 'T', 'A', 'R', 'G', 'E', 'T', '_', 'T', 'Y', 'P', 'E', +'_', 'E', 'N', 'U', 'M', '\020', '\006', '\022', '\032', '\n', '\026', 'T', 'A', 'R', 'G', 'E', 'T', '_', 'T', 'Y', 'P', 'E', '_', 'E', 'N', +'U', 'M', '_', 'E', 'N', 'T', 'R', 'Y', '\020', '\007', '\022', '\027', '\n', '\023', 'T', 'A', 'R', 'G', 'E', 'T', '_', 'T', 'Y', 'P', 'E', +'_', 'S', 'E', 'R', 'V', 'I', 'C', 'E', '\020', '\010', '\022', '\026', '\n', '\022', 'T', 'A', 'R', 'G', 'E', 'T', '_', 'T', 'Y', 'P', 'E', +'_', 'M', 'E', 'T', 'H', 'O', 'D', '\020', '\t', '*', '\t', '\010', '\350', '\007', '\020', '\200', '\200', '\200', '\200', '\002', 'J', '\004', '\010', '\004', '\020', +'\005', 'J', '\004', '\010', '\022', '\020', '\023', '\"', '\254', '\001', '\n', '\014', 'O', 'n', 'e', 'o', 'f', 'O', 'p', 't', 'i', 'o', 'n', 's', '\022', +'7', '\n', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\030', '\001', ' ', '\001', '(', '\013', '2', '\033', '.', 'g', 'o', 'o', 'g', 'l', +'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', 'R', '\010', 'f', 'e', +'a', 't', 'u', 'r', 'e', 's', '\022', 'X', '\n', '\024', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', '_', 'o', +'p', 't', 'i', 'o', 'n', '\030', '\347', '\007', ' ', '\003', '(', '\013', '2', '$', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', +'t', 'o', 'b', 'u', 'f', '.', 'U', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', +'R', '\023', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', '*', '\t', '\010', '\350', +'\007', '\020', '\200', '\200', '\200', '\200', '\002', '\"', '\321', '\002', '\n', '\013', 'E', 'n', 'u', 'm', 'O', 'p', 't', 'i', 'o', 'n', 's', '\022', '\037', +'\n', '\013', 'a', 'l', 'l', 'o', 'w', '_', 'a', 'l', 'i', 'a', 's', '\030', '\002', ' ', '\001', '(', '\010', 'R', '\n', 'a', 'l', 'l', 'o', +'w', 'A', 'l', 'i', 'a', 's', '\022', '%', '\n', '\n', 'd', 'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', '\030', '\003', ' ', '\001', '(', +'\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\n', 'd', 'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', '\022', 'V', '\n', '&', 'd', +'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', '_', 'l', 'e', 'g', 'a', 'c', 'y', '_', 'j', 's', 'o', 'n', '_', 'f', 'i', 'e', +'l', 'd', '_', 'c', 'o', 'n', 'f', 'l', 'i', 'c', 't', 's', '\030', '\006', ' ', '\001', '(', '\010', 'B', '\002', '\030', '\001', 'R', '\"', 'd', +'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', 'L', 'e', 'g', 'a', 'c', 'y', 'J', 's', 'o', 'n', 'F', 'i', 'e', 'l', 'd', 'C', +'o', 'n', 'f', 'l', 'i', 'c', 't', 's', '\022', '7', '\n', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\030', '\007', ' ', '\001', '(', +'\013', '2', '\033', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', +'r', 'e', 'S', 'e', 't', 'R', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\022', 'X', '\n', '\024', 'u', 'n', 'i', 'n', 't', 'e', +'r', 'p', 'r', 'e', 't', 'e', 'd', '_', 'o', 'p', 't', 'i', 'o', 'n', '\030', '\347', '\007', ' ', '\003', '(', '\013', '2', '$', '.', 'g', +'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'U', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', +'t', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', 'R', '\023', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', +'p', 't', 'i', 'o', 'n', '*', '\t', '\010', '\350', '\007', '\020', '\200', '\200', '\200', '\200', '\002', 'J', '\004', '\010', '\005', '\020', '\006', '\"', '\330', '\002', +'\n', '\020', 'E', 'n', 'u', 'm', 'V', 'a', 'l', 'u', 'e', 'O', 'p', 't', 'i', 'o', 'n', 's', '\022', '%', '\n', '\n', 'd', 'e', 'p', +'r', 'e', 'c', 'a', 't', 'e', 'd', '\030', '\001', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\n', 'd', 'e', 'p', +'r', 'e', 'c', 'a', 't', 'e', 'd', '\022', '7', '\n', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\030', '\002', ' ', '\001', '(', '\013', +'2', '\033', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', +'e', 'S', 'e', 't', 'R', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\022', '(', '\n', '\014', 'd', 'e', 'b', 'u', 'g', '_', 'r', +'e', 'd', 'a', 'c', 't', '\030', '\003', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\013', 'd', 'e', 'b', 'u', 'g', +'R', 'e', 'd', 'a', 'c', 't', '\022', 'U', '\n', '\017', 'f', 'e', 'a', 't', 'u', 'r', 'e', '_', 's', 'u', 'p', 'p', 'o', 'r', 't', +'\030', '\004', ' ', '\001', '(', '\013', '2', ',', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', +'F', 'i', 'e', 'l', 'd', 'O', 'p', 't', 'i', 'o', 'n', 's', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'u', 'p', 'p', 'o', +'r', 't', 'R', '\016', 'f', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'u', 'p', 'p', 'o', 'r', 't', '\022', 'X', '\n', '\024', 'u', 'n', 'i', +'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', '_', 'o', 'p', 't', 'i', 'o', 'n', '\030', '\347', '\007', ' ', '\003', '(', '\013', '2', +'$', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'U', 'n', 'i', 'n', 't', 'e', 'r', +'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', 'R', '\023', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', +'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', '*', '\t', '\010', '\350', '\007', '\020', '\200', '\200', '\200', '\200', '\002', '\"', '\325', '\001', '\n', '\016', 'S', +'e', 'r', 'v', 'i', 'c', 'e', 'O', 'p', 't', 'i', 'o', 'n', 's', '\022', '7', '\n', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', +'\030', '\"', ' ', '\001', '(', '\013', '2', '\033', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', +'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', 'R', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\022', '%', '\n', '\n', 'd', +'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', '\030', '!', ' ', '\001', '(', '\010', ':', '\005', 'f', 'a', 'l', 's', 'e', 'R', '\n', 'd', +'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', '\022', 'X', '\n', '\024', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', +'d', '_', 'o', 'p', 't', 'i', 'o', 'n', '\030', '\347', '\007', ' ', '\003', '(', '\013', '2', '$', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', +'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'U', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', +'i', 'o', 'n', 'R', '\023', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', '*', +'\t', '\010', '\350', '\007', '\020', '\200', '\200', '\200', '\200', '\002', '\"', '\231', '\003', '\n', '\r', 'M', 'e', 't', 'h', 'o', 'd', 'O', 'p', 't', 'i', +'o', 'n', 's', '\022', '%', '\n', '\n', 'd', 'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', '\030', '!', ' ', '\001', '(', '\010', ':', '\005', +'f', 'a', 'l', 's', 'e', 'R', '\n', 'd', 'e', 'p', 'r', 'e', 'c', 'a', 't', 'e', 'd', '\022', 'q', '\n', '\021', 'i', 'd', 'e', 'm', +'p', 'o', 't', 'e', 'n', 'c', 'y', '_', 'l', 'e', 'v', 'e', 'l', '\030', '\"', ' ', '\001', '(', '\016', '2', '/', '.', 'g', 'o', 'o', +'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'M', 'e', 't', 'h', 'o', 'd', 'O', 'p', 't', 'i', 'o', 'n', +'s', '.', 'I', 'd', 'e', 'm', 'p', 'o', 't', 'e', 'n', 'c', 'y', 'L', 'e', 'v', 'e', 'l', ':', '\023', 'I', 'D', 'E', 'M', 'P', +'O', 'T', 'E', 'N', 'C', 'Y', '_', 'U', 'N', 'K', 'N', 'O', 'W', 'N', 'R', '\020', 'i', 'd', 'e', 'm', 'p', 'o', 't', 'e', 'n', +'c', 'y', 'L', 'e', 'v', 'e', 'l', '\022', '7', '\n', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\030', '#', ' ', '\001', '(', '\013', +'2', '\033', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', +'e', 'S', 'e', 't', 'R', '\010', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\022', 'X', '\n', '\024', 'u', 'n', 'i', 'n', 't', 'e', 'r', +'p', 'r', 'e', 't', 'e', 'd', '_', 'o', 'p', 't', 'i', 'o', 'n', '\030', '\347', '\007', ' ', '\003', '(', '\013', '2', '$', '.', 'g', 'o', +'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'U', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', +'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', 'R', '\023', 'u', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', +'t', 'i', 'o', 'n', '\"', 'P', '\n', '\020', 'I', 'd', 'e', 'm', 'p', 'o', 't', 'e', 'n', 'c', 'y', 'L', 'e', 'v', 'e', 'l', '\022', +'\027', '\n', '\023', 'I', 'D', 'E', 'M', 'P', 'O', 'T', 'E', 'N', 'C', 'Y', '_', 'U', 'N', 'K', 'N', 'O', 'W', 'N', '\020', '\000', '\022', +'\023', '\n', '\017', 'N', 'O', '_', 'S', 'I', 'D', 'E', '_', 'E', 'F', 'F', 'E', 'C', 'T', 'S', '\020', '\001', '\022', '\016', '\n', '\n', 'I', +'D', 'E', 'M', 'P', 'O', 'T', 'E', 'N', 'T', '\020', '\002', '*', '\t', '\010', '\350', '\007', '\020', '\200', '\200', '\200', '\200', '\002', '\"', '\232', '\003', +'\n', '\023', 'U', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', '\022', 'A', '\n', '\004', +'n', 'a', 'm', 'e', '\030', '\002', ' ', '\003', '(', '\013', '2', '-', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', +'b', 'u', 'f', '.', 'U', 'n', 'i', 'n', 't', 'e', 'r', 'p', 'r', 'e', 't', 'e', 'd', 'O', 'p', 't', 'i', 'o', 'n', '.', 'N', +'a', 'm', 'e', 'P', 'a', 'r', 't', 'R', '\004', 'n', 'a', 'm', 'e', '\022', ')', '\n', '\020', 'i', 'd', 'e', 'n', 't', 'i', 'f', 'i', +'e', 'r', '_', 'v', 'a', 'l', 'u', 'e', '\030', '\003', ' ', '\001', '(', '\t', 'R', '\017', 'i', 'd', 'e', 'n', 't', 'i', 'f', 'i', 'e', +'r', 'V', 'a', 'l', 'u', 'e', '\022', ',', '\n', '\022', 'p', 'o', 's', 'i', 't', 'i', 'v', 'e', '_', 'i', 'n', 't', '_', 'v', 'a', +'l', 'u', 'e', '\030', '\004', ' ', '\001', '(', '\004', 'R', '\020', 'p', 'o', 's', 'i', 't', 'i', 'v', 'e', 'I', 'n', 't', 'V', 'a', 'l', +'u', 'e', '\022', ',', '\n', '\022', 'n', 'e', 'g', 'a', 't', 'i', 'v', 'e', '_', 'i', 'n', 't', '_', 'v', 'a', 'l', 'u', 'e', '\030', +'\005', ' ', '\001', '(', '\003', 'R', '\020', 'n', 'e', 'g', 'a', 't', 'i', 'v', 'e', 'I', 'n', 't', 'V', 'a', 'l', 'u', 'e', '\022', '!', +'\n', '\014', 'd', 'o', 'u', 'b', 'l', 'e', '_', 'v', 'a', 'l', 'u', 'e', '\030', '\006', ' ', '\001', '(', '\001', 'R', '\013', 'd', 'o', 'u', +'b', 'l', 'e', 'V', 'a', 'l', 'u', 'e', '\022', '!', '\n', '\014', 's', 't', 'r', 'i', 'n', 'g', '_', 'v', 'a', 'l', 'u', 'e', '\030', +'\007', ' ', '\001', '(', '\014', 'R', '\013', 's', 't', 'r', 'i', 'n', 'g', 'V', 'a', 'l', 'u', 'e', '\022', '\'', '\n', '\017', 'a', 'g', 'g', +'r', 'e', 'g', 'a', 't', 'e', '_', 'v', 'a', 'l', 'u', 'e', '\030', '\010', ' ', '\001', '(', '\t', 'R', '\016', 'a', 'g', 'g', 'r', 'e', +'g', 'a', 't', 'e', 'V', 'a', 'l', 'u', 'e', '\032', 'J', '\n', '\010', 'N', 'a', 'm', 'e', 'P', 'a', 'r', 't', '\022', '\033', '\n', '\t', +'n', 'a', 'm', 'e', '_', 'p', 'a', 'r', 't', '\030', '\001', ' ', '\002', '(', '\t', 'R', '\010', 'n', 'a', 'm', 'e', 'P', 'a', 'r', 't', +'\022', '!', '\n', '\014', 'i', 's', '_', 'e', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', '\030', '\002', ' ', '\002', '(', '\010', 'R', '\013', 'i', +'s', 'E', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', '\"', '\247', '\n', '\n', '\n', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', +'\022', '\221', '\001', '\n', '\016', 'f', 'i', 'e', 'l', 'd', '_', 'p', 'r', 'e', 's', 'e', 'n', 'c', 'e', '\030', '\001', ' ', '\001', '(', '\016', +'2', ')', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', +'e', 'S', 'e', 't', '.', 'F', 'i', 'e', 'l', 'd', 'P', 'r', 'e', 's', 'e', 'n', 'c', 'e', 'B', '?', '\210', '\001', '\001', '\230', '\001', +'\004', '\230', '\001', '\001', '\242', '\001', '\r', '\022', '\010', 'E', 'X', 'P', 'L', 'I', 'C', 'I', 'T', '\030', '\346', '\007', '\242', '\001', '\r', '\022', '\010', +'I', 'M', 'P', 'L', 'I', 'C', 'I', 'T', '\030', '\347', '\007', '\242', '\001', '\r', '\022', '\010', 'E', 'X', 'P', 'L', 'I', 'C', 'I', 'T', '\030', +'\350', '\007', '\262', '\001', '\003', '\010', '\350', '\007', 'R', '\r', 'f', 'i', 'e', 'l', 'd', 'P', 'r', 'e', 's', 'e', 'n', 'c', 'e', '\022', 'l', +'\n', '\t', 'e', 'n', 'u', 'm', '_', 't', 'y', 'p', 'e', '\030', '\002', ' ', '\001', '(', '\016', '2', '$', '.', 'g', 'o', 'o', 'g', 'l', +'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', '.', 'E', 'n', 'u', +'m', 'T', 'y', 'p', 'e', 'B', ')', '\210', '\001', '\001', '\230', '\001', '\006', '\230', '\001', '\001', '\242', '\001', '\013', '\022', '\006', 'C', 'L', 'O', 'S', +'E', 'D', '\030', '\346', '\007', '\242', '\001', '\t', '\022', '\004', 'O', 'P', 'E', 'N', '\030', '\347', '\007', '\262', '\001', '\003', '\010', '\350', '\007', 'R', '\010', +'e', 'n', 'u', 'm', 'T', 'y', 'p', 'e', '\022', '\230', '\001', '\n', '\027', 'r', 'e', 'p', 'e', 'a', 't', 'e', 'd', '_', 'f', 'i', 'e', +'l', 'd', '_', 'e', 'n', 'c', 'o', 'd', 'i', 'n', 'g', '\030', '\003', ' ', '\001', '(', '\016', '2', '1', '.', 'g', 'o', 'o', 'g', 'l', +'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', '.', 'R', 'e', 'p', +'e', 'a', 't', 'e', 'd', 'F', 'i', 'e', 'l', 'd', 'E', 'n', 'c', 'o', 'd', 'i', 'n', 'g', 'B', '-', '\210', '\001', '\001', '\230', '\001', +'\004', '\230', '\001', '\001', '\242', '\001', '\r', '\022', '\010', 'E', 'X', 'P', 'A', 'N', 'D', 'E', 'D', '\030', '\346', '\007', '\242', '\001', '\013', '\022', '\006', +'P', 'A', 'C', 'K', 'E', 'D', '\030', '\347', '\007', '\262', '\001', '\003', '\010', '\350', '\007', 'R', '\025', 'r', 'e', 'p', 'e', 'a', 't', 'e', 'd', +'F', 'i', 'e', 'l', 'd', 'E', 'n', 'c', 'o', 'd', 'i', 'n', 'g', '\022', '~', '\n', '\017', 'u', 't', 'f', '8', '_', 'v', 'a', 'l', +'i', 'd', 'a', 't', 'i', 'o', 'n', '\030', '\004', ' ', '\001', '(', '\016', '2', '*', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', +'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', '.', 'U', 't', 'f', '8', 'V', 'a', 'l', +'i', 'd', 'a', 't', 'i', 'o', 'n', 'B', ')', '\210', '\001', '\001', '\230', '\001', '\004', '\230', '\001', '\001', '\242', '\001', '\t', '\022', '\004', 'N', 'O', +'N', 'E', '\030', '\346', '\007', '\242', '\001', '\013', '\022', '\006', 'V', 'E', 'R', 'I', 'F', 'Y', '\030', '\347', '\007', '\262', '\001', '\003', '\010', '\350', '\007', +'R', '\016', 'u', 't', 'f', '8', 'V', 'a', 'l', 'i', 'd', 'a', 't', 'i', 'o', 'n', '\022', '~', '\n', '\020', 'm', 'e', 's', 's', 'a', +'g', 'e', '_', 'e', 'n', 'c', 'o', 'd', 'i', 'n', 'g', '\030', '\005', ' ', '\001', '(', '\016', '2', '+', '.', 'g', 'o', 'o', 'g', 'l', +'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', '.', 'M', 'e', 's', +'s', 'a', 'g', 'e', 'E', 'n', 'c', 'o', 'd', 'i', 'n', 'g', 'B', '&', '\210', '\001', '\001', '\230', '\001', '\004', '\230', '\001', '\001', '\242', '\001', +'\024', '\022', '\017', 'L', 'E', 'N', 'G', 'T', 'H', '_', 'P', 'R', 'E', 'F', 'I', 'X', 'E', 'D', '\030', '\346', '\007', '\262', '\001', '\003', '\010', +'\350', '\007', 'R', '\017', 'm', 'e', 's', 's', 'a', 'g', 'e', 'E', 'n', 'c', 'o', 'd', 'i', 'n', 'g', '\022', '\202', '\001', '\n', '\013', 'j', +'s', 'o', 'n', '_', 'f', 'o', 'r', 'm', 'a', 't', '\030', '\006', ' ', '\001', '(', '\016', '2', '&', '.', 'g', 'o', 'o', 'g', 'l', 'e', +'.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', '.', 'J', 's', 'o', 'n', +'F', 'o', 'r', 'm', 'a', 't', 'B', '9', '\210', '\001', '\001', '\230', '\001', '\003', '\230', '\001', '\006', '\230', '\001', '\001', '\242', '\001', '\027', '\022', '\022', +'L', 'E', 'G', 'A', 'C', 'Y', '_', 'B', 'E', 'S', 'T', '_', 'E', 'F', 'F', 'O', 'R', 'T', '\030', '\346', '\007', '\242', '\001', '\n', '\022', +'\005', 'A', 'L', 'L', 'O', 'W', '\030', '\347', '\007', '\262', '\001', '\003', '\010', '\350', '\007', 'R', '\n', 'j', 's', 'o', 'n', 'F', 'o', 'r', 'm', +'a', 't', '\"', '\\', '\n', '\r', 'F', 'i', 'e', 'l', 'd', 'P', 'r', 'e', 's', 'e', 'n', 'c', 'e', '\022', '\032', '\n', '\026', 'F', 'I', +'E', 'L', 'D', '_', 'P', 'R', 'E', 'S', 'E', 'N', 'C', 'E', '_', 'U', 'N', 'K', 'N', 'O', 'W', 'N', '\020', '\000', '\022', '\014', '\n', +'\010', 'E', 'X', 'P', 'L', 'I', 'C', 'I', 'T', '\020', '\001', '\022', '\014', '\n', '\010', 'I', 'M', 'P', 'L', 'I', 'C', 'I', 'T', '\020', '\002', +'\022', '\023', '\n', '\017', 'L', 'E', 'G', 'A', 'C', 'Y', '_', 'R', 'E', 'Q', 'U', 'I', 'R', 'E', 'D', '\020', '\003', '\"', '7', '\n', '\010', +'E', 'n', 'u', 'm', 'T', 'y', 'p', 'e', '\022', '\025', '\n', '\021', 'E', 'N', 'U', 'M', '_', 'T', 'Y', 'P', 'E', '_', 'U', 'N', 'K', +'N', 'O', 'W', 'N', '\020', '\000', '\022', '\010', '\n', '\004', 'O', 'P', 'E', 'N', '\020', '\001', '\022', '\n', '\n', '\006', 'C', 'L', 'O', 'S', 'E', +'D', '\020', '\002', '\"', 'V', '\n', '\025', 'R', 'e', 'p', 'e', 'a', 't', 'e', 'd', 'F', 'i', 'e', 'l', 'd', 'E', 'n', 'c', 'o', 'd', +'i', 'n', 'g', '\022', '#', '\n', '\037', 'R', 'E', 'P', 'E', 'A', 'T', 'E', 'D', '_', 'F', 'I', 'E', 'L', 'D', '_', 'E', 'N', 'C', +'O', 'D', 'I', 'N', 'G', '_', 'U', 'N', 'K', 'N', 'O', 'W', 'N', '\020', '\000', '\022', '\n', '\n', '\006', 'P', 'A', 'C', 'K', 'E', 'D', +'\020', '\001', '\022', '\014', '\n', '\010', 'E', 'X', 'P', 'A', 'N', 'D', 'E', 'D', '\020', '\002', '\"', 'I', '\n', '\016', 'U', 't', 'f', '8', 'V', +'a', 'l', 'i', 'd', 'a', 't', 'i', 'o', 'n', '\022', '\033', '\n', '\027', 'U', 'T', 'F', '8', '_', 'V', 'A', 'L', 'I', 'D', 'A', 'T', +'I', 'O', 'N', '_', 'U', 'N', 'K', 'N', 'O', 'W', 'N', '\020', '\000', '\022', '\n', '\n', '\006', 'V', 'E', 'R', 'I', 'F', 'Y', '\020', '\002', +'\022', '\010', '\n', '\004', 'N', 'O', 'N', 'E', '\020', '\003', '\"', '\004', '\010', '\001', '\020', '\001', '\"', 'S', '\n', '\017', 'M', 'e', 's', 's', 'a', +'g', 'e', 'E', 'n', 'c', 'o', 'd', 'i', 'n', 'g', '\022', '\034', '\n', '\030', 'M', 'E', 'S', 'S', 'A', 'G', 'E', '_', 'E', 'N', 'C', +'O', 'D', 'I', 'N', 'G', '_', 'U', 'N', 'K', 'N', 'O', 'W', 'N', '\020', '\000', '\022', '\023', '\n', '\017', 'L', 'E', 'N', 'G', 'T', 'H', +'_', 'P', 'R', 'E', 'F', 'I', 'X', 'E', 'D', '\020', '\001', '\022', '\r', '\n', '\t', 'D', 'E', 'L', 'I', 'M', 'I', 'T', 'E', 'D', '\020', +'\002', '\"', 'H', '\n', '\n', 'J', 's', 'o', 'n', 'F', 'o', 'r', 'm', 'a', 't', '\022', '\027', '\n', '\023', 'J', 'S', 'O', 'N', '_', 'F', +'O', 'R', 'M', 'A', 'T', '_', 'U', 'N', 'K', 'N', 'O', 'W', 'N', '\020', '\000', '\022', '\t', '\n', '\005', 'A', 'L', 'L', 'O', 'W', '\020', +'\001', '\022', '\026', '\n', '\022', 'L', 'E', 'G', 'A', 'C', 'Y', '_', 'B', 'E', 'S', 'T', '_', 'E', 'F', 'F', 'O', 'R', 'T', '\020', '\002', +'*', '\006', '\010', '\350', '\007', '\020', '\213', 'N', '*', '\006', '\010', '\213', 'N', '\020', '\220', 'N', '*', '\006', '\010', '\220', 'N', '\020', '\221', 'N', 'J', +'\006', '\010', '\347', '\007', '\020', '\350', '\007', '\"', '\357', '\003', '\n', '\022', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', 'D', 'e', 'f', +'a', 'u', 'l', 't', 's', '\022', 'X', '\n', '\010', 'd', 'e', 'f', 'a', 'u', 'l', 't', 's', '\030', '\001', ' ', '\003', '(', '\013', '2', '<', +'.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', +'e', 't', 'D', 'e', 'f', 'a', 'u', 'l', 't', 's', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', 'E', 'd', 'i', 't', +'i', 'o', 'n', 'D', 'e', 'f', 'a', 'u', 'l', 't', 'R', '\010', 'd', 'e', 'f', 'a', 'u', 'l', 't', 's', '\022', 'A', '\n', '\017', 'm', +'i', 'n', 'i', 'm', 'u', 'm', '_', 'e', 'd', 'i', 't', 'i', 'o', 'n', '\030', '\004', ' ', '\001', '(', '\016', '2', '\030', '.', 'g', 'o', +'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'E', 'd', 'i', 't', 'i', 'o', 'n', 'R', '\016', 'm', 'i', +'n', 'i', 'm', 'u', 'm', 'E', 'd', 'i', 't', 'i', 'o', 'n', '\022', 'A', '\n', '\017', 'm', 'a', 'x', 'i', 'm', 'u', 'm', '_', 'e', +'d', 'i', 't', 'i', 'o', 'n', '\030', '\005', ' ', '\001', '(', '\016', '2', '\030', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', +'t', 'o', 'b', 'u', 'f', '.', 'E', 'd', 'i', 't', 'i', 'o', 'n', 'R', '\016', 'm', 'a', 'x', 'i', 'm', 'u', 'm', 'E', 'd', 'i', +'t', 'i', 'o', 'n', '\032', '\370', '\001', '\n', '\030', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', 'E', 'd', 'i', 't', 'i', 'o', +'n', 'D', 'e', 'f', 'a', 'u', 'l', 't', '\022', '2', '\n', '\007', 'e', 'd', 'i', 't', 'i', 'o', 'n', '\030', '\003', ' ', '\001', '(', '\016', +'2', '\030', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'E', 'd', 'i', 't', 'i', 'o', +'n', 'R', '\007', 'e', 'd', 'i', 't', 'i', 'o', 'n', '\022', 'N', '\n', '\024', 'o', 'v', 'e', 'r', 'r', 'i', 'd', 'a', 'b', 'l', 'e', +'_', 'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\030', '\004', ' ', '\001', '(', '\013', '2', '\033', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', +'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', 'R', '\023', 'o', 'v', 'e', 'r', +'r', 'i', 'd', 'a', 'b', 'l', 'e', 'F', 'e', 'a', 't', 'u', 'r', 'e', 's', '\022', 'B', '\n', '\016', 'f', 'i', 'x', 'e', 'd', '_', +'f', 'e', 'a', 't', 'u', 'r', 'e', 's', '\030', '\005', ' ', '\001', '(', '\013', '2', '\033', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', +'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'F', 'e', 'a', 't', 'u', 'r', 'e', 'S', 'e', 't', 'R', '\r', 'f', 'i', 'x', 'e', 'd', +'F', 'e', 'a', 't', 'u', 'r', 'e', 's', 'J', '\004', '\010', '\001', '\020', '\002', 'J', '\004', '\010', '\002', '\020', '\003', 'R', '\010', 'f', 'e', 'a', +'t', 'u', 'r', 'e', 's', '\"', '\247', '\002', '\n', '\016', 'S', 'o', 'u', 'r', 'c', 'e', 'C', 'o', 'd', 'e', 'I', 'n', 'f', 'o', '\022', +'D', '\n', '\010', 'l', 'o', 'c', 'a', 't', 'i', 'o', 'n', '\030', '\001', ' ', '\003', '(', '\013', '2', '(', '.', 'g', 'o', 'o', 'g', 'l', +'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'S', 'o', 'u', 'r', 'c', 'e', 'C', 'o', 'd', 'e', 'I', 'n', 'f', 'o', +'.', 'L', 'o', 'c', 'a', 't', 'i', 'o', 'n', 'R', '\010', 'l', 'o', 'c', 'a', 't', 'i', 'o', 'n', '\032', '\316', '\001', '\n', '\010', 'L', +'o', 'c', 'a', 't', 'i', 'o', 'n', '\022', '\026', '\n', '\004', 'p', 'a', 't', 'h', '\030', '\001', ' ', '\003', '(', '\005', 'B', '\002', '\020', '\001', +'R', '\004', 'p', 'a', 't', 'h', '\022', '\026', '\n', '\004', 's', 'p', 'a', 'n', '\030', '\002', ' ', '\003', '(', '\005', 'B', '\002', '\020', '\001', 'R', +'\004', 's', 'p', 'a', 'n', '\022', ')', '\n', '\020', 'l', 'e', 'a', 'd', 'i', 'n', 'g', '_', 'c', 'o', 'm', 'm', 'e', 'n', 't', 's', +'\030', '\003', ' ', '\001', '(', '\t', 'R', '\017', 'l', 'e', 'a', 'd', 'i', 'n', 'g', 'C', 'o', 'm', 'm', 'e', 'n', 't', 's', '\022', '+', +'\n', '\021', 't', 'r', 'a', 'i', 'l', 'i', 'n', 'g', '_', 'c', 'o', 'm', 'm', 'e', 'n', 't', 's', '\030', '\004', ' ', '\001', '(', '\t', +'R', '\020', 't', 'r', 'a', 'i', 'l', 'i', 'n', 'g', 'C', 'o', 'm', 'm', 'e', 'n', 't', 's', '\022', ':', '\n', '\031', 'l', 'e', 'a', +'d', 'i', 'n', 'g', '_', 'd', 'e', 't', 'a', 'c', 'h', 'e', 'd', '_', 'c', 'o', 'm', 'm', 'e', 'n', 't', 's', '\030', '\006', ' ', +'\003', '(', '\t', 'R', '\027', 'l', 'e', 'a', 'd', 'i', 'n', 'g', 'D', 'e', 't', 'a', 'c', 'h', 'e', 'd', 'C', 'o', 'm', 'm', 'e', +'n', 't', 's', '\"', '\320', '\002', '\n', '\021', 'G', 'e', 'n', 'e', 'r', 'a', 't', 'e', 'd', 'C', 'o', 'd', 'e', 'I', 'n', 'f', 'o', +'\022', 'M', '\n', '\n', 'a', 'n', 'n', 'o', 't', 'a', 't', 'i', 'o', 'n', '\030', '\001', ' ', '\003', '(', '\013', '2', '-', '.', 'g', 'o', +'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'G', 'e', 'n', 'e', 'r', 'a', 't', 'e', 'd', 'C', 'o', +'d', 'e', 'I', 'n', 'f', 'o', '.', 'A', 'n', 'n', 'o', 't', 'a', 't', 'i', 'o', 'n', 'R', '\n', 'a', 'n', 'n', 'o', 't', 'a', +'t', 'i', 'o', 'n', '\032', '\353', '\001', '\n', '\n', 'A', 'n', 'n', 'o', 't', 'a', 't', 'i', 'o', 'n', '\022', '\026', '\n', '\004', 'p', 'a', +'t', 'h', '\030', '\001', ' ', '\003', '(', '\005', 'B', '\002', '\020', '\001', 'R', '\004', 'p', 'a', 't', 'h', '\022', '\037', '\n', '\013', 's', 'o', 'u', +'r', 'c', 'e', '_', 'f', 'i', 'l', 'e', '\030', '\002', ' ', '\001', '(', '\t', 'R', '\n', 's', 'o', 'u', 'r', 'c', 'e', 'F', 'i', 'l', +'e', '\022', '\024', '\n', '\005', 'b', 'e', 'g', 'i', 'n', '\030', '\003', ' ', '\001', '(', '\005', 'R', '\005', 'b', 'e', 'g', 'i', 'n', '\022', '\020', +'\n', '\003', 'e', 'n', 'd', '\030', '\004', ' ', '\001', '(', '\005', 'R', '\003', 'e', 'n', 'd', '\022', 'R', '\n', '\010', 's', 'e', 'm', 'a', 'n', +'t', 'i', 'c', '\030', '\005', ' ', '\001', '(', '\016', '2', '6', '.', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', +'u', 'f', '.', 'G', 'e', 'n', 'e', 'r', 'a', 't', 'e', 'd', 'C', 'o', 'd', 'e', 'I', 'n', 'f', 'o', '.', 'A', 'n', 'n', 'o', +'t', 'a', 't', 'i', 'o', 'n', '.', 'S', 'e', 'm', 'a', 'n', 't', 'i', 'c', 'R', '\010', 's', 'e', 'm', 'a', 'n', 't', 'i', 'c', +'\"', '(', '\n', '\010', 'S', 'e', 'm', 'a', 'n', 't', 'i', 'c', '\022', '\010', '\n', '\004', 'N', 'O', 'N', 'E', '\020', '\000', '\022', '\007', '\n', +'\003', 'S', 'E', 'T', '\020', '\001', '\022', '\t', '\n', '\005', 'A', 'L', 'I', 'A', 'S', '\020', '\002', '*', '\247', '\002', '\n', '\007', 'E', 'd', 'i', +'t', 'i', 'o', 'n', '\022', '\023', '\n', '\017', 'E', 'D', 'I', 'T', 'I', 'O', 'N', '_', 'U', 'N', 'K', 'N', 'O', 'W', 'N', '\020', '\000', +'\022', '\023', '\n', '\016', 'E', 'D', 'I', 'T', 'I', 'O', 'N', '_', 'L', 'E', 'G', 'A', 'C', 'Y', '\020', '\204', '\007', '\022', '\023', '\n', '\016', +'E', 'D', 'I', 'T', 'I', 'O', 'N', '_', 'P', 'R', 'O', 'T', 'O', '2', '\020', '\346', '\007', '\022', '\023', '\n', '\016', 'E', 'D', 'I', 'T', +'I', 'O', 'N', '_', 'P', 'R', 'O', 'T', 'O', '3', '\020', '\347', '\007', '\022', '\021', '\n', '\014', 'E', 'D', 'I', 'T', 'I', 'O', 'N', '_', +'2', '0', '2', '3', '\020', '\350', '\007', '\022', '\021', '\n', '\014', 'E', 'D', 'I', 'T', 'I', 'O', 'N', '_', '2', '0', '2', '4', '\020', '\351', +'\007', '\022', '\027', '\n', '\023', 'E', 'D', 'I', 'T', 'I', 'O', 'N', '_', '1', '_', 'T', 'E', 'S', 'T', '_', 'O', 'N', 'L', 'Y', '\020', +'\001', '\022', '\027', '\n', '\023', 'E', 'D', 'I', 'T', 'I', 'O', 'N', '_', '2', '_', 'T', 'E', 'S', 'T', '_', 'O', 'N', 'L', 'Y', '\020', +'\002', '\022', '\035', '\n', '\027', 'E', 'D', 'I', 'T', 'I', 'O', 'N', '_', '9', '9', '9', '9', '7', '_', 'T', 'E', 'S', 'T', '_', 'O', +'N', 'L', 'Y', '\020', '\235', '\215', '\006', '\022', '\035', '\n', '\027', 'E', 'D', 'I', 'T', 'I', 'O', 'N', '_', '9', '9', '9', '9', '8', '_', +'T', 'E', 'S', 'T', '_', 'O', 'N', 'L', 'Y', '\020', '\236', '\215', '\006', '\022', '\035', '\n', '\027', 'E', 'D', 'I', 'T', 'I', 'O', 'N', '_', +'9', '9', '9', '9', '9', '_', 'T', 'E', 'S', 'T', '_', 'O', 'N', 'L', 'Y', '\020', '\237', '\215', '\006', '\022', '\023', '\n', '\013', 'E', 'D', +'I', 'T', 'I', 'O', 'N', '_', 'M', 'A', 'X', '\020', '\377', '\377', '\377', '\377', '\007', 'B', '~', '\n', '\023', 'c', 'o', 'm', '.', 'g', 'o', +'o', 'g', 'l', 'e', '.', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', 'B', '\020', 'D', 'e', 's', 'c', 'r', 'i', 'p', 't', 'o', 'r', +'P', 'r', 'o', 't', 'o', 's', 'H', '\001', 'Z', '-', 'g', 'o', 'o', 'g', 'l', 'e', '.', 'g', 'o', 'l', 'a', 'n', 'g', '.', 'o', +'r', 'g', '/', 'p', 'r', 'o', 't', 'o', 'b', 'u', 'f', '/', 't', 'y', 'p', 'e', 's', '/', 'd', 'e', 's', 'c', 'r', 'i', 'p', +'t', 'o', 'r', 'p', 'b', '\370', '\001', '\001', '\242', '\002', '\003', 'G', 'P', 'B', '\252', '\002', '\032', 'G', 'o', 'o', 'g', 'l', 'e', '.', 'P', +'r', 'o', 't', 'o', 'b', 'u', 'f', '.', 'R', 'e', 'f', 'l', 'e', 'c', 't', 'i', 'o', 'n', +}; + +static _upb_DefPool_Init *deps[1] = { + NULL +}; + +_upb_DefPool_Init google_protobuf_descriptor_proto_upbdefinit = { + deps, + &google_protobuf_descriptor_proto_upb_file_layout, + "google/protobuf/descriptor.proto", + UPB_STRINGVIEW_INIT(descriptor, 12268) +}; diff --git a/google/protobuf/descriptor.upbdefs.h b/google/protobuf/descriptor.upbdefs.h new file mode 100644 index 0000000..9a488b5 --- /dev/null +++ b/google/protobuf/descriptor.upbdefs.h @@ -0,0 +1,192 @@ +/* This file was generated by upb_generator from the input file: + * + * google/protobuf/descriptor.proto + * + * Do not edit -- your changes will be discarded when the file is + * regenerated. */ + +#ifndef GOOGLE_PROTOBUF_DESCRIPTOR_PROTO_UPBDEFS_H_ +#define GOOGLE_PROTOBUF_DESCRIPTOR_PROTO_UPBDEFS_H_ + +#include "upb/reflection/def.h" +#include "upb/reflection/internal/def_pool.h" + +#include "upb/port/def.inc" // Must be last. +#ifdef __cplusplus +extern "C" { +#endif + +extern _upb_DefPool_Init google_protobuf_descriptor_proto_upbdefinit; + +UPB_INLINE const upb_MessageDef *google_protobuf_FileDescriptorSet_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.FileDescriptorSet"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_FileDescriptorProto_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.FileDescriptorProto"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_DescriptorProto_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.DescriptorProto"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_DescriptorProto_ExtensionRange_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.DescriptorProto.ExtensionRange"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_DescriptorProto_ReservedRange_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.DescriptorProto.ReservedRange"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_ExtensionRangeOptions_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.ExtensionRangeOptions"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_ExtensionRangeOptions_Declaration_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.ExtensionRangeOptions.Declaration"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_FieldDescriptorProto_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.FieldDescriptorProto"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_OneofDescriptorProto_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.OneofDescriptorProto"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_EnumDescriptorProto_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.EnumDescriptorProto"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_EnumDescriptorProto_EnumReservedRange_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.EnumDescriptorProto.EnumReservedRange"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_EnumValueDescriptorProto_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.EnumValueDescriptorProto"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_ServiceDescriptorProto_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.ServiceDescriptorProto"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_MethodDescriptorProto_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.MethodDescriptorProto"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_FileOptions_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.FileOptions"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_MessageOptions_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.MessageOptions"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_FieldOptions_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.FieldOptions"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_FieldOptions_EditionDefault_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.FieldOptions.EditionDefault"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_FieldOptions_FeatureSupport_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.FieldOptions.FeatureSupport"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_OneofOptions_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.OneofOptions"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_EnumOptions_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.EnumOptions"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_EnumValueOptions_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.EnumValueOptions"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_ServiceOptions_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.ServiceOptions"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_MethodOptions_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.MethodOptions"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_UninterpretedOption_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.UninterpretedOption"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_UninterpretedOption_NamePart_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.UninterpretedOption.NamePart"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_FeatureSet_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.FeatureSet"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_FeatureSetDefaults_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.FeatureSetDefaults"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_FeatureSetDefaults_FeatureSetEditionDefault_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_SourceCodeInfo_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.SourceCodeInfo"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_SourceCodeInfo_Location_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.SourceCodeInfo.Location"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_GeneratedCodeInfo_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.GeneratedCodeInfo"); +} + +UPB_INLINE const upb_MessageDef *google_protobuf_GeneratedCodeInfo_Annotation_getmsgdef(upb_DefPool *s) { + _upb_DefPool_LoadDefInit(s, &google_protobuf_descriptor_proto_upbdefinit); + return upb_DefPool_FindMessageByName(s, "google.protobuf.GeneratedCodeInfo.Annotation"); +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#include "upb/port/undef.inc" + +#endif /* GOOGLE_PROTOBUF_DESCRIPTOR_PROTO_UPBDEFS_H_ */ diff --git a/google/protobuf/descriptor_database.py b/google/protobuf/descriptor_database.py new file mode 100644 index 0000000..46a893e --- /dev/null +++ b/google/protobuf/descriptor_database.py @@ -0,0 +1,154 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Provides a container for DescriptorProtos.""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +import warnings + + +class Error(Exception): + pass + + +class DescriptorDatabaseConflictingDefinitionError(Error): + """Raised when a proto is added with the same name & different descriptor.""" + + +class DescriptorDatabase(object): + """A container accepting FileDescriptorProtos and maps DescriptorProtos.""" + + def __init__(self): + self._file_desc_protos_by_file = {} + self._file_desc_protos_by_symbol = {} + + def Add(self, file_desc_proto): + """Adds the FileDescriptorProto and its types to this database. + + Args: + file_desc_proto: The FileDescriptorProto to add. + Raises: + DescriptorDatabaseConflictingDefinitionError: if an attempt is made to + add a proto with the same name but different definition than an + existing proto in the database. + """ + proto_name = file_desc_proto.name + if proto_name not in self._file_desc_protos_by_file: + self._file_desc_protos_by_file[proto_name] = file_desc_proto + elif self._file_desc_protos_by_file[proto_name] != file_desc_proto: + raise DescriptorDatabaseConflictingDefinitionError( + '%s already added, but with different descriptor.' % proto_name) + else: + return + + # Add all the top-level descriptors to the index. + package = file_desc_proto.package + for message in file_desc_proto.message_type: + for name in _ExtractSymbols(message, package): + self._AddSymbol(name, file_desc_proto) + for enum in file_desc_proto.enum_type: + self._AddSymbol(('.'.join((package, enum.name))), file_desc_proto) + for enum_value in enum.value: + self._file_desc_protos_by_symbol[ + '.'.join((package, enum_value.name))] = file_desc_proto + for extension in file_desc_proto.extension: + self._AddSymbol(('.'.join((package, extension.name))), file_desc_proto) + for service in file_desc_proto.service: + self._AddSymbol(('.'.join((package, service.name))), file_desc_proto) + + def FindFileByName(self, name): + """Finds the file descriptor proto by file name. + + Typically the file name is a relative path ending to a .proto file. The + proto with the given name will have to have been added to this database + using the Add method or else an error will be raised. + + Args: + name: The file name to find. + + Returns: + The file descriptor proto matching the name. + + Raises: + KeyError if no file by the given name was added. + """ + + return self._file_desc_protos_by_file[name] + + def FindFileContainingSymbol(self, symbol): + """Finds the file descriptor proto containing the specified symbol. + + The symbol should be a fully qualified name including the file descriptor's + package and any containing messages. Some examples: + + 'some.package.name.Message' + 'some.package.name.Message.NestedEnum' + 'some.package.name.Message.some_field' + + The file descriptor proto containing the specified symbol must be added to + this database using the Add method or else an error will be raised. + + Args: + symbol: The fully qualified symbol name. + + Returns: + The file descriptor proto containing the symbol. + + Raises: + KeyError if no file contains the specified symbol. + """ + try: + return self._file_desc_protos_by_symbol[symbol] + except KeyError: + # Fields, enum values, and nested extensions are not in + # _file_desc_protos_by_symbol. Try to find the top level + # descriptor. Non-existent nested symbol under a valid top level + # descriptor can also be found. The behavior is the same with + # protobuf C++. + top_level, _, _ = symbol.rpartition('.') + try: + return self._file_desc_protos_by_symbol[top_level] + except KeyError: + # Raise the original symbol as a KeyError for better diagnostics. + raise KeyError(symbol) + + def FindFileContainingExtension(self, extendee_name, extension_number): + # TODO: implement this API. + return None + + def FindAllExtensionNumbers(self, extendee_name): + # TODO: implement this API. + return [] + + def _AddSymbol(self, name, file_desc_proto): + if name in self._file_desc_protos_by_symbol: + warn_msg = ('Conflict register for file "' + file_desc_proto.name + + '": ' + name + + ' is already defined in file "' + + self._file_desc_protos_by_symbol[name].name + '"') + warnings.warn(warn_msg, RuntimeWarning) + self._file_desc_protos_by_symbol[name] = file_desc_proto + + +def _ExtractSymbols(desc_proto, package): + """Pulls out all the symbols from a descriptor proto. + + Args: + desc_proto: The proto to extract symbols from. + package: The package containing the descriptor type. + + Yields: + The fully qualified name found in the descriptor. + """ + message_name = package + '.' + desc_proto.name if package else desc_proto.name + yield message_name + for nested_type in desc_proto.nested_type: + for symbol in _ExtractSymbols(nested_type, message_name): + yield symbol + for enum_type in desc_proto.enum_type: + yield '.'.join((message_name, enum_type.name)) diff --git a/google/protobuf/descriptor_pb2.py b/google/protobuf/descriptor_pb2.py new file mode 100644 index 0000000..ad5a206 --- /dev/null +++ b/google/protobuf/descriptor_pb2.py @@ -0,0 +1,3169 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/descriptor.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/descriptor.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR = _descriptor.FileDescriptor( + name='google/protobuf/descriptor.proto', + package='google.protobuf', + syntax='proto2', + edition='EDITION_PROTO2', + serialized_options=b'\n\023com.google.protobufB\020DescriptorProtosH\001Z-google.golang.org/protobuf/types/descriptorpb\370\001\001\242\002\003GPB\252\002\032Google.Protobuf.Reflection', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"M\n\x11\x46ileDescriptorSet\x12\x38\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProtoR\x04\x66ile\"\x98\x05\n\x13\x46ileDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n\x07package\x18\x02 \x01(\tR\x07package\x12\x1e\n\ndependency\x18\x03 \x03(\tR\ndependency\x12+\n\x11public_dependency\x18\n \x03(\x05R\x10publicDependency\x12\'\n\x0fweak_dependency\x18\x0b \x03(\x05R\x0eweakDependency\x12\x43\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProtoR\x0bmessageType\x12\x41\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProtoR\x08\x65numType\x12\x41\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProtoR\x07service\x12\x43\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\textension\x12\x36\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptionsR\x07options\x12I\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfoR\x0esourceCodeInfo\x12\x16\n\x06syntax\x18\x0c \x01(\tR\x06syntax\x12\x32\n\x07\x65\x64ition\x18\x0e \x01(\x0e\x32\x18.google.protobuf.EditionR\x07\x65\x64ition\"\xb9\x06\n\x0f\x44\x65scriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12;\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\x05\x66ield\x12\x43\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\textension\x12\x41\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProtoR\nnestedType\x12\x41\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProtoR\x08\x65numType\x12X\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRangeR\x0e\x65xtensionRange\x12\x44\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProtoR\toneofDecl\x12\x39\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptionsR\x07options\x12U\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRangeR\rreservedRange\x12#\n\rreserved_name\x18\n \x03(\tR\x0creservedName\x1az\n\x0e\x45xtensionRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\x12@\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptionsR\x07options\x1a\x37\n\rReservedRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\"\xcc\x04\n\x15\x45xtensionRangeOptions\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\x12Y\n\x0b\x64\x65\x63laration\x18\x02 \x03(\x0b\x32\x32.google.protobuf.ExtensionRangeOptions.DeclarationB\x03\x88\x01\x02R\x0b\x64\x65\x63laration\x12\x37\n\x08\x66\x65\x61tures\x18\x32 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12m\n\x0cverification\x18\x03 \x01(\x0e\x32\x38.google.protobuf.ExtensionRangeOptions.VerificationState:\nUNVERIFIEDB\x03\x88\x01\x02R\x0cverification\x1a\x94\x01\n\x0b\x44\x65\x63laration\x12\x16\n\x06number\x18\x01 \x01(\x05R\x06number\x12\x1b\n\tfull_name\x18\x02 \x01(\tR\x08\x66ullName\x12\x12\n\x04type\x18\x03 \x01(\tR\x04type\x12\x1a\n\x08reserved\x18\x05 \x01(\x08R\x08reserved\x12\x1a\n\x08repeated\x18\x06 \x01(\x08R\x08repeatedJ\x04\x08\x04\x10\x05\"4\n\x11VerificationState\x12\x0f\n\x0b\x44\x45\x43LARATION\x10\x00\x12\x0e\n\nUNVERIFIED\x10\x01*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xc1\x06\n\x14\x46ieldDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n\x06number\x18\x03 \x01(\x05R\x06number\x12\x41\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.LabelR\x05label\x12>\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.TypeR\x04type\x12\x1b\n\ttype_name\x18\x06 \x01(\tR\x08typeName\x12\x1a\n\x08\x65xtendee\x18\x02 \x01(\tR\x08\x65xtendee\x12#\n\rdefault_value\x18\x07 \x01(\tR\x0c\x64\x65\x66\x61ultValue\x12\x1f\n\x0boneof_index\x18\t \x01(\x05R\noneofIndex\x12\x1b\n\tjson_name\x18\n \x01(\tR\x08jsonName\x12\x37\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptionsR\x07options\x12\'\n\x0fproto3_optional\x18\x11 \x01(\x08R\x0eproto3Optional\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REPEATED\x10\x03\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\"c\n\x14OneofDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x37\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptionsR\x07options\"\xe3\x02\n\x13\x45numDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12?\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProtoR\x05value\x12\x36\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptionsR\x07options\x12]\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRangeR\rreservedRange\x12#\n\rreserved_name\x18\x05 \x03(\tR\x0creservedName\x1a;\n\x11\x45numReservedRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\"\x83\x01\n\x18\x45numValueDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n\x06number\x18\x02 \x01(\x05R\x06number\x12;\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptionsR\x07options\"\xa7\x01\n\x16ServiceDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12>\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProtoR\x06method\x12\x39\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptionsR\x07options\"\x89\x02\n\x15MethodDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n\ninput_type\x18\x02 \x01(\tR\tinputType\x12\x1f\n\x0boutput_type\x18\x03 \x01(\tR\noutputType\x12\x38\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptionsR\x07options\x12\x30\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lseR\x0f\x63lientStreaming\x12\x30\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lseR\x0fserverStreaming\"\xad\t\n\x0b\x46ileOptions\x12!\n\x0cjava_package\x18\x01 \x01(\tR\x0bjavaPackage\x12\x30\n\x14java_outer_classname\x18\x08 \x01(\tR\x12javaOuterClassname\x12\x35\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lseR\x11javaMultipleFiles\x12\x44\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01R\x19javaGenerateEqualsAndHash\x12:\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lseR\x13javaStringCheckUtf8\x12S\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEEDR\x0boptimizeFor\x12\x1d\n\ngo_package\x18\x0b \x01(\tR\tgoPackage\x12\x35\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lseR\x11\x63\x63GenericServices\x12\x39\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lseR\x13javaGenericServices\x12\x35\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lseR\x11pyGenericServices\x12%\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12.\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04trueR\x0e\x63\x63\x45nableArenas\x12*\n\x11objc_class_prefix\x18$ \x01(\tR\x0fobjcClassPrefix\x12)\n\x10\x63sharp_namespace\x18% \x01(\tR\x0f\x63sharpNamespace\x12!\n\x0cswift_prefix\x18\' \x01(\tR\x0bswiftPrefix\x12(\n\x10php_class_prefix\x18( \x01(\tR\x0ephpClassPrefix\x12#\n\rphp_namespace\x18) \x01(\tR\x0cphpNamespace\x12\x34\n\x16php_metadata_namespace\x18, \x01(\tR\x14phpMetadataNamespace\x12!\n\x0cruby_package\x18- \x01(\tR\x0brubyPackage\x12\x37\n\x08\x66\x65\x61tures\x18\x32 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08*\x10+J\x04\x08&\x10\'R\x14php_generic_services\"\xf4\x03\n\x0eMessageOptions\x12<\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lseR\x14messageSetWireFormat\x12L\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lseR\x1cnoStandardDescriptorAccessor\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12\x1b\n\tmap_entry\x18\x07 \x01(\x08R\x08mapEntry\x12V\n&deprecated_legacy_json_field_conflicts\x18\x0b \x01(\x08\x42\x02\x18\x01R\"deprecatedLegacyJsonFieldConflicts\x12\x37\n\x08\x66\x65\x61tures\x18\x0c \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\x9d\r\n\x0c\x46ieldOptions\x12\x41\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRINGR\x05\x63type\x12\x16\n\x06packed\x18\x02 \x01(\x08R\x06packed\x12G\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMALR\x06jstype\x12\x19\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lseR\x04lazy\x12.\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lseR\x0eunverifiedLazy\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12\x19\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lseR\x04weak\x12(\n\x0c\x64\x65\x62ug_redact\x18\x10 \x01(\x08:\x05\x66\x61lseR\x0b\x64\x65\x62ugRedact\x12K\n\tretention\x18\x11 \x01(\x0e\x32-.google.protobuf.FieldOptions.OptionRetentionR\tretention\x12H\n\x07targets\x18\x13 \x03(\x0e\x32..google.protobuf.FieldOptions.OptionTargetTypeR\x07targets\x12W\n\x10\x65\x64ition_defaults\x18\x14 \x03(\x0b\x32,.google.protobuf.FieldOptions.EditionDefaultR\x0f\x65\x64itionDefaults\x12\x37\n\x08\x66\x65\x61tures\x18\x15 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12U\n\x0f\x66\x65\x61ture_support\x18\x16 \x01(\x0b\x32,.google.protobuf.FieldOptions.FeatureSupportR\x0e\x66\x65\x61tureSupport\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\x1aZ\n\x0e\x45\x64itionDefault\x12\x32\n\x07\x65\x64ition\x18\x03 \x01(\x0e\x32\x18.google.protobuf.EditionR\x07\x65\x64ition\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value\x1a\x96\x02\n\x0e\x46\x65\x61tureSupport\x12G\n\x12\x65\x64ition_introduced\x18\x01 \x01(\x0e\x32\x18.google.protobuf.EditionR\x11\x65\x64itionIntroduced\x12G\n\x12\x65\x64ition_deprecated\x18\x02 \x01(\x0e\x32\x18.google.protobuf.EditionR\x11\x65\x64itionDeprecated\x12/\n\x13\x64\x65precation_warning\x18\x03 \x01(\tR\x12\x64\x65precationWarning\x12\x41\n\x0f\x65\x64ition_removed\x18\x04 \x01(\x0e\x32\x18.google.protobuf.EditionR\x0e\x65\x64itionRemoved\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02\"U\n\x0fOptionRetention\x12\x15\n\x11RETENTION_UNKNOWN\x10\x00\x12\x15\n\x11RETENTION_RUNTIME\x10\x01\x12\x14\n\x10RETENTION_SOURCE\x10\x02\"\x8c\x02\n\x10OptionTargetType\x12\x17\n\x13TARGET_TYPE_UNKNOWN\x10\x00\x12\x14\n\x10TARGET_TYPE_FILE\x10\x01\x12\x1f\n\x1bTARGET_TYPE_EXTENSION_RANGE\x10\x02\x12\x17\n\x13TARGET_TYPE_MESSAGE\x10\x03\x12\x15\n\x11TARGET_TYPE_FIELD\x10\x04\x12\x15\n\x11TARGET_TYPE_ONEOF\x10\x05\x12\x14\n\x10TARGET_TYPE_ENUM\x10\x06\x12\x1a\n\x16TARGET_TYPE_ENUM_ENTRY\x10\x07\x12\x17\n\x13TARGET_TYPE_SERVICE\x10\x08\x12\x16\n\x12TARGET_TYPE_METHOD\x10\t*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x12\x10\x13\"\xac\x01\n\x0cOneofOptions\x12\x37\n\x08\x66\x65\x61tures\x18\x01 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd1\x02\n\x0b\x45numOptions\x12\x1f\n\x0b\x61llow_alias\x18\x02 \x01(\x08R\nallowAlias\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12V\n&deprecated_legacy_json_field_conflicts\x18\x06 \x01(\x08\x42\x02\x18\x01R\"deprecatedLegacyJsonFieldConflicts\x12\x37\n\x08\x66\x65\x61tures\x18\x07 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"\xd8\x02\n\x10\x45numValueOptions\x12%\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12\x37\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12(\n\x0c\x64\x65\x62ug_redact\x18\x03 \x01(\x08:\x05\x66\x61lseR\x0b\x64\x65\x62ugRedact\x12U\n\x0f\x66\x65\x61ture_support\x18\x04 \x01(\x0b\x32,.google.protobuf.FieldOptions.FeatureSupportR\x0e\x66\x65\x61tureSupport\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd5\x01\n\x0eServiceOptions\x12\x37\n\x08\x66\x65\x61tures\x18\" \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12%\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x99\x03\n\rMethodOptions\x12%\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12q\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWNR\x10idempotencyLevel\x12\x37\n\x08\x66\x65\x61tures\x18# \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9a\x03\n\x13UninterpretedOption\x12\x41\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePartR\x04name\x12)\n\x10identifier_value\x18\x03 \x01(\tR\x0fidentifierValue\x12,\n\x12positive_int_value\x18\x04 \x01(\x04R\x10positiveIntValue\x12,\n\x12negative_int_value\x18\x05 \x01(\x03R\x10negativeIntValue\x12!\n\x0c\x64ouble_value\x18\x06 \x01(\x01R\x0b\x64oubleValue\x12!\n\x0cstring_value\x18\x07 \x01(\x0cR\x0bstringValue\x12\'\n\x0f\x61ggregate_value\x18\x08 \x01(\tR\x0e\x61ggregateValue\x1aJ\n\x08NamePart\x12\x1b\n\tname_part\x18\x01 \x02(\tR\x08namePart\x12!\n\x0cis_extension\x18\x02 \x02(\x08R\x0bisExtension\"\xa7\n\n\nFeatureSet\x12\x91\x01\n\x0e\x66ield_presence\x18\x01 \x01(\x0e\x32).google.protobuf.FeatureSet.FieldPresenceB?\x88\x01\x01\x98\x01\x04\x98\x01\x01\xa2\x01\r\x12\x08\x45XPLICIT\x18\xe6\x07\xa2\x01\r\x12\x08IMPLICIT\x18\xe7\x07\xa2\x01\r\x12\x08\x45XPLICIT\x18\xe8\x07\xb2\x01\x03\x08\xe8\x07R\rfieldPresence\x12l\n\tenum_type\x18\x02 \x01(\x0e\x32$.google.protobuf.FeatureSet.EnumTypeB)\x88\x01\x01\x98\x01\x06\x98\x01\x01\xa2\x01\x0b\x12\x06\x43LOSED\x18\xe6\x07\xa2\x01\t\x12\x04OPEN\x18\xe7\x07\xb2\x01\x03\x08\xe8\x07R\x08\x65numType\x12\x98\x01\n\x17repeated_field_encoding\x18\x03 \x01(\x0e\x32\x31.google.protobuf.FeatureSet.RepeatedFieldEncodingB-\x88\x01\x01\x98\x01\x04\x98\x01\x01\xa2\x01\r\x12\x08\x45XPANDED\x18\xe6\x07\xa2\x01\x0b\x12\x06PACKED\x18\xe7\x07\xb2\x01\x03\x08\xe8\x07R\x15repeatedFieldEncoding\x12~\n\x0futf8_validation\x18\x04 \x01(\x0e\x32*.google.protobuf.FeatureSet.Utf8ValidationB)\x88\x01\x01\x98\x01\x04\x98\x01\x01\xa2\x01\t\x12\x04NONE\x18\xe6\x07\xa2\x01\x0b\x12\x06VERIFY\x18\xe7\x07\xb2\x01\x03\x08\xe8\x07R\x0eutf8Validation\x12~\n\x10message_encoding\x18\x05 \x01(\x0e\x32+.google.protobuf.FeatureSet.MessageEncodingB&\x88\x01\x01\x98\x01\x04\x98\x01\x01\xa2\x01\x14\x12\x0fLENGTH_PREFIXED\x18\xe6\x07\xb2\x01\x03\x08\xe8\x07R\x0fmessageEncoding\x12\x82\x01\n\x0bjson_format\x18\x06 \x01(\x0e\x32&.google.protobuf.FeatureSet.JsonFormatB9\x88\x01\x01\x98\x01\x03\x98\x01\x06\x98\x01\x01\xa2\x01\x17\x12\x12LEGACY_BEST_EFFORT\x18\xe6\x07\xa2\x01\n\x12\x05\x41LLOW\x18\xe7\x07\xb2\x01\x03\x08\xe8\x07R\njsonFormat\"\\\n\rFieldPresence\x12\x1a\n\x16\x46IELD_PRESENCE_UNKNOWN\x10\x00\x12\x0c\n\x08\x45XPLICIT\x10\x01\x12\x0c\n\x08IMPLICIT\x10\x02\x12\x13\n\x0fLEGACY_REQUIRED\x10\x03\"7\n\x08\x45numType\x12\x15\n\x11\x45NUM_TYPE_UNKNOWN\x10\x00\x12\x08\n\x04OPEN\x10\x01\x12\n\n\x06\x43LOSED\x10\x02\"V\n\x15RepeatedFieldEncoding\x12#\n\x1fREPEATED_FIELD_ENCODING_UNKNOWN\x10\x00\x12\n\n\x06PACKED\x10\x01\x12\x0c\n\x08\x45XPANDED\x10\x02\"I\n\x0eUtf8Validation\x12\x1b\n\x17UTF8_VALIDATION_UNKNOWN\x10\x00\x12\n\n\x06VERIFY\x10\x02\x12\x08\n\x04NONE\x10\x03\"\x04\x08\x01\x10\x01\"S\n\x0fMessageEncoding\x12\x1c\n\x18MESSAGE_ENCODING_UNKNOWN\x10\x00\x12\x13\n\x0fLENGTH_PREFIXED\x10\x01\x12\r\n\tDELIMITED\x10\x02\"H\n\nJsonFormat\x12\x17\n\x13JSON_FORMAT_UNKNOWN\x10\x00\x12\t\n\x05\x41LLOW\x10\x01\x12\x16\n\x12LEGACY_BEST_EFFORT\x10\x02*\x06\x08\xe8\x07\x10\x8bN*\x06\x08\x8bN\x10\x90N*\x06\x08\x90N\x10\x91NJ\x06\x08\xe7\x07\x10\xe8\x07\"\xef\x03\n\x12\x46\x65\x61tureSetDefaults\x12X\n\x08\x64\x65\x66\x61ults\x18\x01 \x03(\x0b\x32<.google.protobuf.FeatureSetDefaults.FeatureSetEditionDefaultR\x08\x64\x65\x66\x61ults\x12\x41\n\x0fminimum_edition\x18\x04 \x01(\x0e\x32\x18.google.protobuf.EditionR\x0eminimumEdition\x12\x41\n\x0fmaximum_edition\x18\x05 \x01(\x0e\x32\x18.google.protobuf.EditionR\x0emaximumEdition\x1a\xf8\x01\n\x18\x46\x65\x61tureSetEditionDefault\x12\x32\n\x07\x65\x64ition\x18\x03 \x01(\x0e\x32\x18.google.protobuf.EditionR\x07\x65\x64ition\x12N\n\x14overridable_features\x18\x04 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x13overridableFeatures\x12\x42\n\x0e\x66ixed_features\x18\x05 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\rfixedFeaturesJ\x04\x08\x01\x10\x02J\x04\x08\x02\x10\x03R\x08\x66\x65\x61tures\"\xa7\x02\n\x0eSourceCodeInfo\x12\x44\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.LocationR\x08location\x1a\xce\x01\n\x08Location\x12\x16\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01R\x04path\x12\x16\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01R\x04span\x12)\n\x10leading_comments\x18\x03 \x01(\tR\x0fleadingComments\x12+\n\x11trailing_comments\x18\x04 \x01(\tR\x10trailingComments\x12:\n\x19leading_detached_comments\x18\x06 \x03(\tR\x17leadingDetachedComments\"\xd0\x02\n\x11GeneratedCodeInfo\x12M\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.AnnotationR\nannotation\x1a\xeb\x01\n\nAnnotation\x12\x16\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01R\x04path\x12\x1f\n\x0bsource_file\x18\x02 \x01(\tR\nsourceFile\x12\x14\n\x05\x62\x65gin\x18\x03 \x01(\x05R\x05\x62\x65gin\x12\x10\n\x03\x65nd\x18\x04 \x01(\x05R\x03\x65nd\x12R\n\x08semantic\x18\x05 \x01(\x0e\x32\x36.google.protobuf.GeneratedCodeInfo.Annotation.SemanticR\x08semantic\"(\n\x08Semantic\x12\x08\n\x04NONE\x10\x00\x12\x07\n\x03SET\x10\x01\x12\t\n\x05\x41LIAS\x10\x02*\xa7\x02\n\x07\x45\x64ition\x12\x13\n\x0f\x45\x44ITION_UNKNOWN\x10\x00\x12\x13\n\x0e\x45\x44ITION_LEGACY\x10\x84\x07\x12\x13\n\x0e\x45\x44ITION_PROTO2\x10\xe6\x07\x12\x13\n\x0e\x45\x44ITION_PROTO3\x10\xe7\x07\x12\x11\n\x0c\x45\x44ITION_2023\x10\xe8\x07\x12\x11\n\x0c\x45\x44ITION_2024\x10\xe9\x07\x12\x17\n\x13\x45\x44ITION_1_TEST_ONLY\x10\x01\x12\x17\n\x13\x45\x44ITION_2_TEST_ONLY\x10\x02\x12\x1d\n\x17\x45\x44ITION_99997_TEST_ONLY\x10\x9d\x8d\x06\x12\x1d\n\x17\x45\x44ITION_99998_TEST_ONLY\x10\x9e\x8d\x06\x12\x1d\n\x17\x45\x44ITION_99999_TEST_ONLY\x10\x9f\x8d\x06\x12\x13\n\x0b\x45\x44ITION_MAX\x10\xff\xff\xff\xff\x07\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection' + ) +else: + DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/descriptor.proto\x12\x0fgoogle.protobuf\"M\n\x11\x46ileDescriptorSet\x12\x38\n\x04\x66ile\x18\x01 \x03(\x0b\x32$.google.protobuf.FileDescriptorProtoR\x04\x66ile\"\x98\x05\n\x13\x46ileDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n\x07package\x18\x02 \x01(\tR\x07package\x12\x1e\n\ndependency\x18\x03 \x03(\tR\ndependency\x12+\n\x11public_dependency\x18\n \x03(\x05R\x10publicDependency\x12\'\n\x0fweak_dependency\x18\x0b \x03(\x05R\x0eweakDependency\x12\x43\n\x0cmessage_type\x18\x04 \x03(\x0b\x32 .google.protobuf.DescriptorProtoR\x0bmessageType\x12\x41\n\tenum_type\x18\x05 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProtoR\x08\x65numType\x12\x41\n\x07service\x18\x06 \x03(\x0b\x32\'.google.protobuf.ServiceDescriptorProtoR\x07service\x12\x43\n\textension\x18\x07 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\textension\x12\x36\n\x07options\x18\x08 \x01(\x0b\x32\x1c.google.protobuf.FileOptionsR\x07options\x12I\n\x10source_code_info\x18\t \x01(\x0b\x32\x1f.google.protobuf.SourceCodeInfoR\x0esourceCodeInfo\x12\x16\n\x06syntax\x18\x0c \x01(\tR\x06syntax\x12\x32\n\x07\x65\x64ition\x18\x0e \x01(\x0e\x32\x18.google.protobuf.EditionR\x07\x65\x64ition\"\xb9\x06\n\x0f\x44\x65scriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12;\n\x05\x66ield\x18\x02 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\x05\x66ield\x12\x43\n\textension\x18\x06 \x03(\x0b\x32%.google.protobuf.FieldDescriptorProtoR\textension\x12\x41\n\x0bnested_type\x18\x03 \x03(\x0b\x32 .google.protobuf.DescriptorProtoR\nnestedType\x12\x41\n\tenum_type\x18\x04 \x03(\x0b\x32$.google.protobuf.EnumDescriptorProtoR\x08\x65numType\x12X\n\x0f\x65xtension_range\x18\x05 \x03(\x0b\x32/.google.protobuf.DescriptorProto.ExtensionRangeR\x0e\x65xtensionRange\x12\x44\n\noneof_decl\x18\x08 \x03(\x0b\x32%.google.protobuf.OneofDescriptorProtoR\toneofDecl\x12\x39\n\x07options\x18\x07 \x01(\x0b\x32\x1f.google.protobuf.MessageOptionsR\x07options\x12U\n\x0ereserved_range\x18\t \x03(\x0b\x32..google.protobuf.DescriptorProto.ReservedRangeR\rreservedRange\x12#\n\rreserved_name\x18\n \x03(\tR\x0creservedName\x1az\n\x0e\x45xtensionRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\x12@\n\x07options\x18\x03 \x01(\x0b\x32&.google.protobuf.ExtensionRangeOptionsR\x07options\x1a\x37\n\rReservedRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\"\xcc\x04\n\x15\x45xtensionRangeOptions\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\x12Y\n\x0b\x64\x65\x63laration\x18\x02 \x03(\x0b\x32\x32.google.protobuf.ExtensionRangeOptions.DeclarationB\x03\x88\x01\x02R\x0b\x64\x65\x63laration\x12\x37\n\x08\x66\x65\x61tures\x18\x32 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12m\n\x0cverification\x18\x03 \x01(\x0e\x32\x38.google.protobuf.ExtensionRangeOptions.VerificationState:\nUNVERIFIEDB\x03\x88\x01\x02R\x0cverification\x1a\x94\x01\n\x0b\x44\x65\x63laration\x12\x16\n\x06number\x18\x01 \x01(\x05R\x06number\x12\x1b\n\tfull_name\x18\x02 \x01(\tR\x08\x66ullName\x12\x12\n\x04type\x18\x03 \x01(\tR\x04type\x12\x1a\n\x08reserved\x18\x05 \x01(\x08R\x08reserved\x12\x1a\n\x08repeated\x18\x06 \x01(\x08R\x08repeatedJ\x04\x08\x04\x10\x05\"4\n\x11VerificationState\x12\x0f\n\x0b\x44\x45\x43LARATION\x10\x00\x12\x0e\n\nUNVERIFIED\x10\x01*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xc1\x06\n\x14\x46ieldDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n\x06number\x18\x03 \x01(\x05R\x06number\x12\x41\n\x05label\x18\x04 \x01(\x0e\x32+.google.protobuf.FieldDescriptorProto.LabelR\x05label\x12>\n\x04type\x18\x05 \x01(\x0e\x32*.google.protobuf.FieldDescriptorProto.TypeR\x04type\x12\x1b\n\ttype_name\x18\x06 \x01(\tR\x08typeName\x12\x1a\n\x08\x65xtendee\x18\x02 \x01(\tR\x08\x65xtendee\x12#\n\rdefault_value\x18\x07 \x01(\tR\x0c\x64\x65\x66\x61ultValue\x12\x1f\n\x0boneof_index\x18\t \x01(\x05R\noneofIndex\x12\x1b\n\tjson_name\x18\n \x01(\tR\x08jsonName\x12\x37\n\x07options\x18\x08 \x01(\x0b\x32\x1d.google.protobuf.FieldOptionsR\x07options\x12\'\n\x0fproto3_optional\x18\x11 \x01(\x08R\x0eproto3Optional\"\xb6\x02\n\x04Type\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"C\n\x05Label\x12\x12\n\x0eLABEL_OPTIONAL\x10\x01\x12\x12\n\x0eLABEL_REPEATED\x10\x03\x12\x12\n\x0eLABEL_REQUIRED\x10\x02\"c\n\x14OneofDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x37\n\x07options\x18\x02 \x01(\x0b\x32\x1d.google.protobuf.OneofOptionsR\x07options\"\xe3\x02\n\x13\x45numDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12?\n\x05value\x18\x02 \x03(\x0b\x32).google.protobuf.EnumValueDescriptorProtoR\x05value\x12\x36\n\x07options\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.EnumOptionsR\x07options\x12]\n\x0ereserved_range\x18\x04 \x03(\x0b\x32\x36.google.protobuf.EnumDescriptorProto.EnumReservedRangeR\rreservedRange\x12#\n\rreserved_name\x18\x05 \x03(\tR\x0creservedName\x1a;\n\x11\x45numReservedRange\x12\x14\n\x05start\x18\x01 \x01(\x05R\x05start\x12\x10\n\x03\x65nd\x18\x02 \x01(\x05R\x03\x65nd\"\x83\x01\n\x18\x45numValueDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n\x06number\x18\x02 \x01(\x05R\x06number\x12;\n\x07options\x18\x03 \x01(\x0b\x32!.google.protobuf.EnumValueOptionsR\x07options\"\xa7\x01\n\x16ServiceDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12>\n\x06method\x18\x02 \x03(\x0b\x32&.google.protobuf.MethodDescriptorProtoR\x06method\x12\x39\n\x07options\x18\x03 \x01(\x0b\x32\x1f.google.protobuf.ServiceOptionsR\x07options\"\x89\x02\n\x15MethodDescriptorProto\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n\ninput_type\x18\x02 \x01(\tR\tinputType\x12\x1f\n\x0boutput_type\x18\x03 \x01(\tR\noutputType\x12\x38\n\x07options\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.MethodOptionsR\x07options\x12\x30\n\x10\x63lient_streaming\x18\x05 \x01(\x08:\x05\x66\x61lseR\x0f\x63lientStreaming\x12\x30\n\x10server_streaming\x18\x06 \x01(\x08:\x05\x66\x61lseR\x0fserverStreaming\"\xad\t\n\x0b\x46ileOptions\x12!\n\x0cjava_package\x18\x01 \x01(\tR\x0bjavaPackage\x12\x30\n\x14java_outer_classname\x18\x08 \x01(\tR\x12javaOuterClassname\x12\x35\n\x13java_multiple_files\x18\n \x01(\x08:\x05\x66\x61lseR\x11javaMultipleFiles\x12\x44\n\x1djava_generate_equals_and_hash\x18\x14 \x01(\x08\x42\x02\x18\x01R\x19javaGenerateEqualsAndHash\x12:\n\x16java_string_check_utf8\x18\x1b \x01(\x08:\x05\x66\x61lseR\x13javaStringCheckUtf8\x12S\n\x0coptimize_for\x18\t \x01(\x0e\x32).google.protobuf.FileOptions.OptimizeMode:\x05SPEEDR\x0boptimizeFor\x12\x1d\n\ngo_package\x18\x0b \x01(\tR\tgoPackage\x12\x35\n\x13\x63\x63_generic_services\x18\x10 \x01(\x08:\x05\x66\x61lseR\x11\x63\x63GenericServices\x12\x39\n\x15java_generic_services\x18\x11 \x01(\x08:\x05\x66\x61lseR\x13javaGenericServices\x12\x35\n\x13py_generic_services\x18\x12 \x01(\x08:\x05\x66\x61lseR\x11pyGenericServices\x12%\n\ndeprecated\x18\x17 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12.\n\x10\x63\x63_enable_arenas\x18\x1f \x01(\x08:\x04trueR\x0e\x63\x63\x45nableArenas\x12*\n\x11objc_class_prefix\x18$ \x01(\tR\x0fobjcClassPrefix\x12)\n\x10\x63sharp_namespace\x18% \x01(\tR\x0f\x63sharpNamespace\x12!\n\x0cswift_prefix\x18\' \x01(\tR\x0bswiftPrefix\x12(\n\x10php_class_prefix\x18( \x01(\tR\x0ephpClassPrefix\x12#\n\rphp_namespace\x18) \x01(\tR\x0cphpNamespace\x12\x34\n\x16php_metadata_namespace\x18, \x01(\tR\x14phpMetadataNamespace\x12!\n\x0cruby_package\x18- \x01(\tR\x0brubyPackage\x12\x37\n\x08\x66\x65\x61tures\x18\x32 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\":\n\x0cOptimizeMode\x12\t\n\x05SPEED\x10\x01\x12\r\n\tCODE_SIZE\x10\x02\x12\x10\n\x0cLITE_RUNTIME\x10\x03*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08*\x10+J\x04\x08&\x10\'R\x14php_generic_services\"\xf4\x03\n\x0eMessageOptions\x12<\n\x17message_set_wire_format\x18\x01 \x01(\x08:\x05\x66\x61lseR\x14messageSetWireFormat\x12L\n\x1fno_standard_descriptor_accessor\x18\x02 \x01(\x08:\x05\x66\x61lseR\x1cnoStandardDescriptorAccessor\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12\x1b\n\tmap_entry\x18\x07 \x01(\x08R\x08mapEntry\x12V\n&deprecated_legacy_json_field_conflicts\x18\x0b \x01(\x08\x42\x02\x18\x01R\"deprecatedLegacyJsonFieldConflicts\x12\x37\n\x08\x66\x65\x61tures\x18\x0c \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x05\x10\x06J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\tJ\x04\x08\t\x10\n\"\x9d\r\n\x0c\x46ieldOptions\x12\x41\n\x05\x63type\x18\x01 \x01(\x0e\x32#.google.protobuf.FieldOptions.CType:\x06STRINGR\x05\x63type\x12\x16\n\x06packed\x18\x02 \x01(\x08R\x06packed\x12G\n\x06jstype\x18\x06 \x01(\x0e\x32$.google.protobuf.FieldOptions.JSType:\tJS_NORMALR\x06jstype\x12\x19\n\x04lazy\x18\x05 \x01(\x08:\x05\x66\x61lseR\x04lazy\x12.\n\x0funverified_lazy\x18\x0f \x01(\x08:\x05\x66\x61lseR\x0eunverifiedLazy\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12\x19\n\x04weak\x18\n \x01(\x08:\x05\x66\x61lseR\x04weak\x12(\n\x0c\x64\x65\x62ug_redact\x18\x10 \x01(\x08:\x05\x66\x61lseR\x0b\x64\x65\x62ugRedact\x12K\n\tretention\x18\x11 \x01(\x0e\x32-.google.protobuf.FieldOptions.OptionRetentionR\tretention\x12H\n\x07targets\x18\x13 \x03(\x0e\x32..google.protobuf.FieldOptions.OptionTargetTypeR\x07targets\x12W\n\x10\x65\x64ition_defaults\x18\x14 \x03(\x0b\x32,.google.protobuf.FieldOptions.EditionDefaultR\x0f\x65\x64itionDefaults\x12\x37\n\x08\x66\x65\x61tures\x18\x15 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12U\n\x0f\x66\x65\x61ture_support\x18\x16 \x01(\x0b\x32,.google.protobuf.FieldOptions.FeatureSupportR\x0e\x66\x65\x61tureSupport\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\x1aZ\n\x0e\x45\x64itionDefault\x12\x32\n\x07\x65\x64ition\x18\x03 \x01(\x0e\x32\x18.google.protobuf.EditionR\x07\x65\x64ition\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value\x1a\x96\x02\n\x0e\x46\x65\x61tureSupport\x12G\n\x12\x65\x64ition_introduced\x18\x01 \x01(\x0e\x32\x18.google.protobuf.EditionR\x11\x65\x64itionIntroduced\x12G\n\x12\x65\x64ition_deprecated\x18\x02 \x01(\x0e\x32\x18.google.protobuf.EditionR\x11\x65\x64itionDeprecated\x12/\n\x13\x64\x65precation_warning\x18\x03 \x01(\tR\x12\x64\x65precationWarning\x12\x41\n\x0f\x65\x64ition_removed\x18\x04 \x01(\x0e\x32\x18.google.protobuf.EditionR\x0e\x65\x64itionRemoved\"/\n\x05\x43Type\x12\n\n\x06STRING\x10\x00\x12\x08\n\x04\x43ORD\x10\x01\x12\x10\n\x0cSTRING_PIECE\x10\x02\"5\n\x06JSType\x12\r\n\tJS_NORMAL\x10\x00\x12\r\n\tJS_STRING\x10\x01\x12\r\n\tJS_NUMBER\x10\x02\"U\n\x0fOptionRetention\x12\x15\n\x11RETENTION_UNKNOWN\x10\x00\x12\x15\n\x11RETENTION_RUNTIME\x10\x01\x12\x14\n\x10RETENTION_SOURCE\x10\x02\"\x8c\x02\n\x10OptionTargetType\x12\x17\n\x13TARGET_TYPE_UNKNOWN\x10\x00\x12\x14\n\x10TARGET_TYPE_FILE\x10\x01\x12\x1f\n\x1bTARGET_TYPE_EXTENSION_RANGE\x10\x02\x12\x17\n\x13TARGET_TYPE_MESSAGE\x10\x03\x12\x15\n\x11TARGET_TYPE_FIELD\x10\x04\x12\x15\n\x11TARGET_TYPE_ONEOF\x10\x05\x12\x14\n\x10TARGET_TYPE_ENUM\x10\x06\x12\x1a\n\x16TARGET_TYPE_ENUM_ENTRY\x10\x07\x12\x17\n\x13TARGET_TYPE_SERVICE\x10\x08\x12\x16\n\x12TARGET_TYPE_METHOD\x10\t*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x04\x10\x05J\x04\x08\x12\x10\x13\"\xac\x01\n\x0cOneofOptions\x12\x37\n\x08\x66\x65\x61tures\x18\x01 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd1\x02\n\x0b\x45numOptions\x12\x1f\n\x0b\x61llow_alias\x18\x02 \x01(\x08R\nallowAlias\x12%\n\ndeprecated\x18\x03 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12V\n&deprecated_legacy_json_field_conflicts\x18\x06 \x01(\x08\x42\x02\x18\x01R\"deprecatedLegacyJsonFieldConflicts\x12\x37\n\x08\x66\x65\x61tures\x18\x07 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02J\x04\x08\x05\x10\x06\"\xd8\x02\n\x10\x45numValueOptions\x12%\n\ndeprecated\x18\x01 \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12\x37\n\x08\x66\x65\x61tures\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12(\n\x0c\x64\x65\x62ug_redact\x18\x03 \x01(\x08:\x05\x66\x61lseR\x0b\x64\x65\x62ugRedact\x12U\n\x0f\x66\x65\x61ture_support\x18\x04 \x01(\x0b\x32,.google.protobuf.FieldOptions.FeatureSupportR\x0e\x66\x65\x61tureSupport\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\xd5\x01\n\x0eServiceOptions\x12\x37\n\x08\x66\x65\x61tures\x18\" \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12%\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x99\x03\n\rMethodOptions\x12%\n\ndeprecated\x18! \x01(\x08:\x05\x66\x61lseR\ndeprecated\x12q\n\x11idempotency_level\x18\" \x01(\x0e\x32/.google.protobuf.MethodOptions.IdempotencyLevel:\x13IDEMPOTENCY_UNKNOWNR\x10idempotencyLevel\x12\x37\n\x08\x66\x65\x61tures\x18# \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x08\x66\x65\x61tures\x12X\n\x14uninterpreted_option\x18\xe7\x07 \x03(\x0b\x32$.google.protobuf.UninterpretedOptionR\x13uninterpretedOption\"P\n\x10IdempotencyLevel\x12\x17\n\x13IDEMPOTENCY_UNKNOWN\x10\x00\x12\x13\n\x0fNO_SIDE_EFFECTS\x10\x01\x12\x0e\n\nIDEMPOTENT\x10\x02*\t\x08\xe8\x07\x10\x80\x80\x80\x80\x02\"\x9a\x03\n\x13UninterpretedOption\x12\x41\n\x04name\x18\x02 \x03(\x0b\x32-.google.protobuf.UninterpretedOption.NamePartR\x04name\x12)\n\x10identifier_value\x18\x03 \x01(\tR\x0fidentifierValue\x12,\n\x12positive_int_value\x18\x04 \x01(\x04R\x10positiveIntValue\x12,\n\x12negative_int_value\x18\x05 \x01(\x03R\x10negativeIntValue\x12!\n\x0c\x64ouble_value\x18\x06 \x01(\x01R\x0b\x64oubleValue\x12!\n\x0cstring_value\x18\x07 \x01(\x0cR\x0bstringValue\x12\'\n\x0f\x61ggregate_value\x18\x08 \x01(\tR\x0e\x61ggregateValue\x1aJ\n\x08NamePart\x12\x1b\n\tname_part\x18\x01 \x02(\tR\x08namePart\x12!\n\x0cis_extension\x18\x02 \x02(\x08R\x0bisExtension\"\xa7\n\n\nFeatureSet\x12\x91\x01\n\x0e\x66ield_presence\x18\x01 \x01(\x0e\x32).google.protobuf.FeatureSet.FieldPresenceB?\x88\x01\x01\x98\x01\x04\x98\x01\x01\xa2\x01\r\x12\x08\x45XPLICIT\x18\xe6\x07\xa2\x01\r\x12\x08IMPLICIT\x18\xe7\x07\xa2\x01\r\x12\x08\x45XPLICIT\x18\xe8\x07\xb2\x01\x03\x08\xe8\x07R\rfieldPresence\x12l\n\tenum_type\x18\x02 \x01(\x0e\x32$.google.protobuf.FeatureSet.EnumTypeB)\x88\x01\x01\x98\x01\x06\x98\x01\x01\xa2\x01\x0b\x12\x06\x43LOSED\x18\xe6\x07\xa2\x01\t\x12\x04OPEN\x18\xe7\x07\xb2\x01\x03\x08\xe8\x07R\x08\x65numType\x12\x98\x01\n\x17repeated_field_encoding\x18\x03 \x01(\x0e\x32\x31.google.protobuf.FeatureSet.RepeatedFieldEncodingB-\x88\x01\x01\x98\x01\x04\x98\x01\x01\xa2\x01\r\x12\x08\x45XPANDED\x18\xe6\x07\xa2\x01\x0b\x12\x06PACKED\x18\xe7\x07\xb2\x01\x03\x08\xe8\x07R\x15repeatedFieldEncoding\x12~\n\x0futf8_validation\x18\x04 \x01(\x0e\x32*.google.protobuf.FeatureSet.Utf8ValidationB)\x88\x01\x01\x98\x01\x04\x98\x01\x01\xa2\x01\t\x12\x04NONE\x18\xe6\x07\xa2\x01\x0b\x12\x06VERIFY\x18\xe7\x07\xb2\x01\x03\x08\xe8\x07R\x0eutf8Validation\x12~\n\x10message_encoding\x18\x05 \x01(\x0e\x32+.google.protobuf.FeatureSet.MessageEncodingB&\x88\x01\x01\x98\x01\x04\x98\x01\x01\xa2\x01\x14\x12\x0fLENGTH_PREFIXED\x18\xe6\x07\xb2\x01\x03\x08\xe8\x07R\x0fmessageEncoding\x12\x82\x01\n\x0bjson_format\x18\x06 \x01(\x0e\x32&.google.protobuf.FeatureSet.JsonFormatB9\x88\x01\x01\x98\x01\x03\x98\x01\x06\x98\x01\x01\xa2\x01\x17\x12\x12LEGACY_BEST_EFFORT\x18\xe6\x07\xa2\x01\n\x12\x05\x41LLOW\x18\xe7\x07\xb2\x01\x03\x08\xe8\x07R\njsonFormat\"\\\n\rFieldPresence\x12\x1a\n\x16\x46IELD_PRESENCE_UNKNOWN\x10\x00\x12\x0c\n\x08\x45XPLICIT\x10\x01\x12\x0c\n\x08IMPLICIT\x10\x02\x12\x13\n\x0fLEGACY_REQUIRED\x10\x03\"7\n\x08\x45numType\x12\x15\n\x11\x45NUM_TYPE_UNKNOWN\x10\x00\x12\x08\n\x04OPEN\x10\x01\x12\n\n\x06\x43LOSED\x10\x02\"V\n\x15RepeatedFieldEncoding\x12#\n\x1fREPEATED_FIELD_ENCODING_UNKNOWN\x10\x00\x12\n\n\x06PACKED\x10\x01\x12\x0c\n\x08\x45XPANDED\x10\x02\"I\n\x0eUtf8Validation\x12\x1b\n\x17UTF8_VALIDATION_UNKNOWN\x10\x00\x12\n\n\x06VERIFY\x10\x02\x12\x08\n\x04NONE\x10\x03\"\x04\x08\x01\x10\x01\"S\n\x0fMessageEncoding\x12\x1c\n\x18MESSAGE_ENCODING_UNKNOWN\x10\x00\x12\x13\n\x0fLENGTH_PREFIXED\x10\x01\x12\r\n\tDELIMITED\x10\x02\"H\n\nJsonFormat\x12\x17\n\x13JSON_FORMAT_UNKNOWN\x10\x00\x12\t\n\x05\x41LLOW\x10\x01\x12\x16\n\x12LEGACY_BEST_EFFORT\x10\x02*\x06\x08\xe8\x07\x10\x8bN*\x06\x08\x8bN\x10\x90N*\x06\x08\x90N\x10\x91NJ\x06\x08\xe7\x07\x10\xe8\x07\"\xef\x03\n\x12\x46\x65\x61tureSetDefaults\x12X\n\x08\x64\x65\x66\x61ults\x18\x01 \x03(\x0b\x32<.google.protobuf.FeatureSetDefaults.FeatureSetEditionDefaultR\x08\x64\x65\x66\x61ults\x12\x41\n\x0fminimum_edition\x18\x04 \x01(\x0e\x32\x18.google.protobuf.EditionR\x0eminimumEdition\x12\x41\n\x0fmaximum_edition\x18\x05 \x01(\x0e\x32\x18.google.protobuf.EditionR\x0emaximumEdition\x1a\xf8\x01\n\x18\x46\x65\x61tureSetEditionDefault\x12\x32\n\x07\x65\x64ition\x18\x03 \x01(\x0e\x32\x18.google.protobuf.EditionR\x07\x65\x64ition\x12N\n\x14overridable_features\x18\x04 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\x13overridableFeatures\x12\x42\n\x0e\x66ixed_features\x18\x05 \x01(\x0b\x32\x1b.google.protobuf.FeatureSetR\rfixedFeaturesJ\x04\x08\x01\x10\x02J\x04\x08\x02\x10\x03R\x08\x66\x65\x61tures\"\xa7\x02\n\x0eSourceCodeInfo\x12\x44\n\x08location\x18\x01 \x03(\x0b\x32(.google.protobuf.SourceCodeInfo.LocationR\x08location\x1a\xce\x01\n\x08Location\x12\x16\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01R\x04path\x12\x16\n\x04span\x18\x02 \x03(\x05\x42\x02\x10\x01R\x04span\x12)\n\x10leading_comments\x18\x03 \x01(\tR\x0fleadingComments\x12+\n\x11trailing_comments\x18\x04 \x01(\tR\x10trailingComments\x12:\n\x19leading_detached_comments\x18\x06 \x03(\tR\x17leadingDetachedComments\"\xd0\x02\n\x11GeneratedCodeInfo\x12M\n\nannotation\x18\x01 \x03(\x0b\x32-.google.protobuf.GeneratedCodeInfo.AnnotationR\nannotation\x1a\xeb\x01\n\nAnnotation\x12\x16\n\x04path\x18\x01 \x03(\x05\x42\x02\x10\x01R\x04path\x12\x1f\n\x0bsource_file\x18\x02 \x01(\tR\nsourceFile\x12\x14\n\x05\x62\x65gin\x18\x03 \x01(\x05R\x05\x62\x65gin\x12\x10\n\x03\x65nd\x18\x04 \x01(\x05R\x03\x65nd\x12R\n\x08semantic\x18\x05 \x01(\x0e\x32\x36.google.protobuf.GeneratedCodeInfo.Annotation.SemanticR\x08semantic\"(\n\x08Semantic\x12\x08\n\x04NONE\x10\x00\x12\x07\n\x03SET\x10\x01\x12\t\n\x05\x41LIAS\x10\x02*\xa7\x02\n\x07\x45\x64ition\x12\x13\n\x0f\x45\x44ITION_UNKNOWN\x10\x00\x12\x13\n\x0e\x45\x44ITION_LEGACY\x10\x84\x07\x12\x13\n\x0e\x45\x44ITION_PROTO2\x10\xe6\x07\x12\x13\n\x0e\x45\x44ITION_PROTO3\x10\xe7\x07\x12\x11\n\x0c\x45\x44ITION_2023\x10\xe8\x07\x12\x11\n\x0c\x45\x44ITION_2024\x10\xe9\x07\x12\x17\n\x13\x45\x44ITION_1_TEST_ONLY\x10\x01\x12\x17\n\x13\x45\x44ITION_2_TEST_ONLY\x10\x02\x12\x1d\n\x17\x45\x44ITION_99997_TEST_ONLY\x10\x9d\x8d\x06\x12\x1d\n\x17\x45\x44ITION_99998_TEST_ONLY\x10\x9e\x8d\x06\x12\x1d\n\x17\x45\x44ITION_99999_TEST_ONLY\x10\x9f\x8d\x06\x12\x13\n\x0b\x45\x44ITION_MAX\x10\xff\xff\xff\xff\x07\x42~\n\x13\x63om.google.protobufB\x10\x44\x65scriptorProtosH\x01Z-google.golang.org/protobuf/types/descriptorpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1aGoogle.Protobuf.Reflection') + +_globals = globals() +if not _descriptor._USE_C_DESCRIPTORS: + _EDITION = _descriptor.EnumDescriptor( + name='Edition', + full_name='google.protobuf.Edition', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='EDITION_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EDITION_LEGACY', index=1, number=900, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EDITION_PROTO2', index=2, number=998, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EDITION_PROTO3', index=3, number=999, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EDITION_2023', index=4, number=1000, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EDITION_2024', index=5, number=1001, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EDITION_1_TEST_ONLY', index=6, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EDITION_2_TEST_ONLY', index=7, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EDITION_99997_TEST_ONLY', index=8, number=99997, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EDITION_99998_TEST_ONLY', index=9, number=99998, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EDITION_99999_TEST_ONLY', index=10, number=99999, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EDITION_MAX', index=11, number=2147483647, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_EDITION) + + _EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE = _descriptor.EnumDescriptor( + name='VerificationState', + full_name='google.protobuf.ExtensionRangeOptions.VerificationState', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='DECLARATION', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='UNVERIFIED', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE) + + _FIELDDESCRIPTORPROTO_TYPE = _descriptor.EnumDescriptor( + name='Type', + full_name='google.protobuf.FieldDescriptorProto.Type', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='TYPE_DOUBLE', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FLOAT', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_INT64', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_UINT64', index=3, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_INT32', index=4, number=5, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FIXED64', index=5, number=6, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_FIXED32', index=6, number=7, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_BOOL', index=7, number=8, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_STRING', index=8, number=9, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_GROUP', index=9, number=10, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_MESSAGE', index=10, number=11, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_BYTES', index=11, number=12, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_UINT32', index=12, number=13, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_ENUM', index=13, number=14, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SFIXED32', index=14, number=15, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SFIXED64', index=15, number=16, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SINT32', index=16, number=17, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TYPE_SINT64', index=17, number=18, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_TYPE) + + _FIELDDESCRIPTORPROTO_LABEL = _descriptor.EnumDescriptor( + name='Label', + full_name='google.protobuf.FieldDescriptorProto.Label', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='LABEL_OPTIONAL', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LABEL_REPEATED', index=1, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LABEL_REQUIRED', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDDESCRIPTORPROTO_LABEL) + + _FILEOPTIONS_OPTIMIZEMODE = _descriptor.EnumDescriptor( + name='OptimizeMode', + full_name='google.protobuf.FileOptions.OptimizeMode', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='SPEED', index=0, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CODE_SIZE', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LITE_RUNTIME', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FILEOPTIONS_OPTIMIZEMODE) + + _FIELDOPTIONS_CTYPE = _descriptor.EnumDescriptor( + name='CType', + full_name='google.protobuf.FieldOptions.CType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='STRING', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CORD', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='STRING_PIECE', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_CTYPE) + + _FIELDOPTIONS_JSTYPE = _descriptor.EnumDescriptor( + name='JSType', + full_name='google.protobuf.FieldOptions.JSType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='JS_NORMAL', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='JS_STRING', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='JS_NUMBER', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_JSTYPE) + + _FIELDOPTIONS_OPTIONRETENTION = _descriptor.EnumDescriptor( + name='OptionRetention', + full_name='google.protobuf.FieldOptions.OptionRetention', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='RETENTION_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='RETENTION_RUNTIME', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='RETENTION_SOURCE', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_OPTIONRETENTION) + + _FIELDOPTIONS_OPTIONTARGETTYPE = _descriptor.EnumDescriptor( + name='OptionTargetType', + full_name='google.protobuf.FieldOptions.OptionTargetType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='TARGET_TYPE_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TARGET_TYPE_FILE', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TARGET_TYPE_EXTENSION_RANGE', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TARGET_TYPE_MESSAGE', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TARGET_TYPE_FIELD', index=4, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TARGET_TYPE_ONEOF', index=5, number=5, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TARGET_TYPE_ENUM', index=6, number=6, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TARGET_TYPE_ENUM_ENTRY', index=7, number=7, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TARGET_TYPE_SERVICE', index=8, number=8, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='TARGET_TYPE_METHOD', index=9, number=9, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FIELDOPTIONS_OPTIONTARGETTYPE) + + _METHODOPTIONS_IDEMPOTENCYLEVEL = _descriptor.EnumDescriptor( + name='IdempotencyLevel', + full_name='google.protobuf.MethodOptions.IdempotencyLevel', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='IDEMPOTENCY_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='NO_SIDE_EFFECTS', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='IDEMPOTENT', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_METHODOPTIONS_IDEMPOTENCYLEVEL) + + _FEATURESET_FIELDPRESENCE = _descriptor.EnumDescriptor( + name='FieldPresence', + full_name='google.protobuf.FeatureSet.FieldPresence', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='FIELD_PRESENCE_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EXPLICIT', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='IMPLICIT', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LEGACY_REQUIRED', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FEATURESET_FIELDPRESENCE) + + _FEATURESET_ENUMTYPE = _descriptor.EnumDescriptor( + name='EnumType', + full_name='google.protobuf.FeatureSet.EnumType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='ENUM_TYPE_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='OPEN', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='CLOSED', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FEATURESET_ENUMTYPE) + + _FEATURESET_REPEATEDFIELDENCODING = _descriptor.EnumDescriptor( + name='RepeatedFieldEncoding', + full_name='google.protobuf.FeatureSet.RepeatedFieldEncoding', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='REPEATED_FIELD_ENCODING_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='PACKED', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='EXPANDED', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FEATURESET_REPEATEDFIELDENCODING) + + _FEATURESET_UTF8VALIDATION = _descriptor.EnumDescriptor( + name='Utf8Validation', + full_name='google.protobuf.FeatureSet.Utf8Validation', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='UTF8_VALIDATION_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='VERIFY', index=1, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='NONE', index=2, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FEATURESET_UTF8VALIDATION) + + _FEATURESET_MESSAGEENCODING = _descriptor.EnumDescriptor( + name='MessageEncoding', + full_name='google.protobuf.FeatureSet.MessageEncoding', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='MESSAGE_ENCODING_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LENGTH_PREFIXED', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DELIMITED', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FEATURESET_MESSAGEENCODING) + + _FEATURESET_JSONFORMAT = _descriptor.EnumDescriptor( + name='JsonFormat', + full_name='google.protobuf.FeatureSet.JsonFormat', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='JSON_FORMAT_UNKNOWN', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ALLOW', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='LEGACY_BEST_EFFORT', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_FEATURESET_JSONFORMAT) + + _GENERATEDCODEINFO_ANNOTATION_SEMANTIC = _descriptor.EnumDescriptor( + name='Semantic', + full_name='google.protobuf.GeneratedCodeInfo.Annotation.Semantic', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='NONE', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='SET', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ALIAS', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + ) + _sym_db.RegisterEnumDescriptor(_GENERATEDCODEINFO_ANNOTATION_SEMANTIC) + + + _FILEDESCRIPTORSET = _descriptor.Descriptor( + name='FileDescriptorSet', + full_name='google.protobuf.FileDescriptorSet', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='file', full_name='google.protobuf.FileDescriptorSet.file', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='file', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _FILEDESCRIPTORPROTO = _descriptor.Descriptor( + name='FileDescriptorProto', + full_name='google.protobuf.FileDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.FileDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='package', full_name='google.protobuf.FileDescriptorProto.package', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='package', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='dependency', full_name='google.protobuf.FileDescriptorProto.dependency', index=2, + number=3, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='dependency', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='public_dependency', full_name='google.protobuf.FileDescriptorProto.public_dependency', index=3, + number=10, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='publicDependency', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='weak_dependency', full_name='google.protobuf.FileDescriptorProto.weak_dependency', index=4, + number=11, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='weakDependency', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='message_type', full_name='google.protobuf.FileDescriptorProto.message_type', index=5, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='messageType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='enum_type', full_name='google.protobuf.FileDescriptorProto.enum_type', index=6, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='enumType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='service', full_name='google.protobuf.FileDescriptorProto.service', index=7, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='service', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension', full_name='google.protobuf.FileDescriptorProto.extension', index=8, + number=7, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='extension', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.FileDescriptorProto.options', index=9, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='source_code_info', full_name='google.protobuf.FileDescriptorProto.source_code_info', index=10, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='sourceCodeInfo', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='syntax', full_name='google.protobuf.FileDescriptorProto.syntax', index=11, + number=12, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='syntax', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='edition', full_name='google.protobuf.FileDescriptorProto.edition', index=12, + number=14, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='edition', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _DESCRIPTORPROTO_EXTENSIONRANGE = _descriptor.Descriptor( + name='ExtensionRange', + full_name='google.protobuf.DescriptorProto.ExtensionRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.DescriptorProto.ExtensionRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='start', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.DescriptorProto.ExtensionRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='end', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.DescriptorProto.ExtensionRange.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + _DESCRIPTORPROTO_RESERVEDRANGE = _descriptor.Descriptor( + name='ReservedRange', + full_name='google.protobuf.DescriptorProto.ReservedRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.DescriptorProto.ReservedRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='start', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.DescriptorProto.ReservedRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='end', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + _DESCRIPTORPROTO = _descriptor.Descriptor( + name='DescriptorProto', + full_name='google.protobuf.DescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.DescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='field', full_name='google.protobuf.DescriptorProto.field', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='field', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension', full_name='google.protobuf.DescriptorProto.extension', index=2, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='extension', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='nested_type', full_name='google.protobuf.DescriptorProto.nested_type', index=3, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='nestedType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='enum_type', full_name='google.protobuf.DescriptorProto.enum_type', index=4, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='enumType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extension_range', full_name='google.protobuf.DescriptorProto.extension_range', index=5, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='extensionRange', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='oneof_decl', full_name='google.protobuf.DescriptorProto.oneof_decl', index=6, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='oneofDecl', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.DescriptorProto.options', index=7, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_range', full_name='google.protobuf.DescriptorProto.reserved_range', index=8, + number=9, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='reservedRange', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_name', full_name='google.protobuf.DescriptorProto.reserved_name', index=9, + number=10, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='reservedName', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_DESCRIPTORPROTO_EXTENSIONRANGE, _DESCRIPTORPROTO_RESERVEDRANGE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _EXTENSIONRANGEOPTIONS_DECLARATION = _descriptor.Descriptor( + name='Declaration', + full_name='google.protobuf.ExtensionRangeOptions.Declaration', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='number', full_name='google.protobuf.ExtensionRangeOptions.Declaration.number', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='number', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='full_name', full_name='google.protobuf.ExtensionRangeOptions.Declaration.full_name', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='fullName', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type', full_name='google.protobuf.ExtensionRangeOptions.Declaration.type', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='type', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved', full_name='google.protobuf.ExtensionRangeOptions.Declaration.reserved', index=3, + number=5, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='reserved', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='repeated', full_name='google.protobuf.ExtensionRangeOptions.Declaration.repeated', index=4, + number=6, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='repeated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + _EXTENSIONRANGEOPTIONS = _descriptor.Descriptor( + name='ExtensionRangeOptions', + full_name='google.protobuf.ExtensionRangeOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.ExtensionRangeOptions.uninterpreted_option', index=0, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='declaration', full_name='google.protobuf.ExtensionRangeOptions.declaration', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\210\001\002', json_name='declaration', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='features', full_name='google.protobuf.ExtensionRangeOptions.features', index=2, + number=50, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='features', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='verification', full_name='google.protobuf.ExtensionRangeOptions.verification', index=3, + number=3, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\210\001\002', json_name='verification', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_EXTENSIONRANGEOPTIONS_DECLARATION, ], + enum_types=[ + _EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE, + ], + serialized_options=None, + is_extendable=True, + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _FIELDDESCRIPTORPROTO = _descriptor.Descriptor( + name='FieldDescriptorProto', + full_name='google.protobuf.FieldDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.FieldDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='number', full_name='google.protobuf.FieldDescriptorProto.number', index=1, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='number', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='label', full_name='google.protobuf.FieldDescriptorProto.label', index=2, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='label', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type', full_name='google.protobuf.FieldDescriptorProto.type', index=3, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='type', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type_name', full_name='google.protobuf.FieldDescriptorProto.type_name', index=4, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='typeName', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='extendee', full_name='google.protobuf.FieldDescriptorProto.extendee', index=5, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='extendee', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='default_value', full_name='google.protobuf.FieldDescriptorProto.default_value', index=6, + number=7, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='defaultValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='oneof_index', full_name='google.protobuf.FieldDescriptorProto.oneof_index', index=7, + number=9, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='oneofIndex', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='json_name', full_name='google.protobuf.FieldDescriptorProto.json_name', index=8, + number=10, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='jsonName', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.FieldDescriptorProto.options', index=9, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='proto3_optional', full_name='google.protobuf.FieldDescriptorProto.proto3_optional', index=10, + number=17, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='proto3Optional', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FIELDDESCRIPTORPROTO_TYPE, + _FIELDDESCRIPTORPROTO_LABEL, + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _ONEOFDESCRIPTORPROTO = _descriptor.Descriptor( + name='OneofDescriptorProto', + full_name='google.protobuf.OneofDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.OneofDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.OneofDescriptorProto.options', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE = _descriptor.Descriptor( + name='EnumReservedRange', + full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='start', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.start', index=0, + number=1, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='start', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.EnumDescriptorProto.EnumReservedRange.end', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='end', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + _ENUMDESCRIPTORPROTO = _descriptor.Descriptor( + name='EnumDescriptorProto', + full_name='google.protobuf.EnumDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.EnumDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='google.protobuf.EnumDescriptorProto.value', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='value', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.EnumDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_range', full_name='google.protobuf.EnumDescriptorProto.reserved_range', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='reservedRange', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='reserved_name', full_name='google.protobuf.EnumDescriptorProto.reserved_name', index=4, + number=5, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='reservedName', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _ENUMVALUEDESCRIPTORPROTO = _descriptor.Descriptor( + name='EnumValueDescriptorProto', + full_name='google.protobuf.EnumValueDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.EnumValueDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='number', full_name='google.protobuf.EnumValueDescriptorProto.number', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='number', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.EnumValueDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _SERVICEDESCRIPTORPROTO = _descriptor.Descriptor( + name='ServiceDescriptorProto', + full_name='google.protobuf.ServiceDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.ServiceDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='method', full_name='google.protobuf.ServiceDescriptorProto.method', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='method', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.ServiceDescriptorProto.options', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _METHODDESCRIPTORPROTO = _descriptor.Descriptor( + name='MethodDescriptorProto', + full_name='google.protobuf.MethodDescriptorProto', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.MethodDescriptorProto.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='input_type', full_name='google.protobuf.MethodDescriptorProto.input_type', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='inputType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='output_type', full_name='google.protobuf.MethodDescriptorProto.output_type', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='outputType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='options', full_name='google.protobuf.MethodDescriptorProto.options', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='options', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='client_streaming', full_name='google.protobuf.MethodDescriptorProto.client_streaming', index=4, + number=5, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='clientStreaming', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='server_streaming', full_name='google.protobuf.MethodDescriptorProto.server_streaming', index=5, + number=6, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='serverStreaming', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _FILEOPTIONS = _descriptor.Descriptor( + name='FileOptions', + full_name='google.protobuf.FileOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='java_package', full_name='google.protobuf.FileOptions.java_package', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='javaPackage', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_outer_classname', full_name='google.protobuf.FileOptions.java_outer_classname', index=1, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='javaOuterClassname', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_multiple_files', full_name='google.protobuf.FileOptions.java_multiple_files', index=2, + number=10, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='javaMultipleFiles', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_generate_equals_and_hash', full_name='google.protobuf.FileOptions.java_generate_equals_and_hash', index=3, + number=20, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\030\001', json_name='javaGenerateEqualsAndHash', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_string_check_utf8', full_name='google.protobuf.FileOptions.java_string_check_utf8', index=4, + number=27, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='javaStringCheckUtf8', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='optimize_for', full_name='google.protobuf.FileOptions.optimize_for', index=5, + number=9, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='optimizeFor', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='go_package', full_name='google.protobuf.FileOptions.go_package', index=6, + number=11, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='goPackage', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='cc_generic_services', full_name='google.protobuf.FileOptions.cc_generic_services', index=7, + number=16, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='ccGenericServices', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='java_generic_services', full_name='google.protobuf.FileOptions.java_generic_services', index=8, + number=17, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='javaGenericServices', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='py_generic_services', full_name='google.protobuf.FileOptions.py_generic_services', index=9, + number=18, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='pyGenericServices', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.FileOptions.deprecated', index=10, + number=23, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='cc_enable_arenas', full_name='google.protobuf.FileOptions.cc_enable_arenas', index=11, + number=31, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=True, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='ccEnableArenas', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='objc_class_prefix', full_name='google.protobuf.FileOptions.objc_class_prefix', index=12, + number=36, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='objcClassPrefix', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='csharp_namespace', full_name='google.protobuf.FileOptions.csharp_namespace', index=13, + number=37, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='csharpNamespace', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='swift_prefix', full_name='google.protobuf.FileOptions.swift_prefix', index=14, + number=39, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='swiftPrefix', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_class_prefix', full_name='google.protobuf.FileOptions.php_class_prefix', index=15, + number=40, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='phpClassPrefix', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_namespace', full_name='google.protobuf.FileOptions.php_namespace', index=16, + number=41, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='phpNamespace', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='php_metadata_namespace', full_name='google.protobuf.FileOptions.php_metadata_namespace', index=17, + number=44, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='phpMetadataNamespace', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='ruby_package', full_name='google.protobuf.FileOptions.ruby_package', index=18, + number=45, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='rubyPackage', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='features', full_name='google.protobuf.FileOptions.features', index=19, + number=50, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='features', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.FileOptions.uninterpreted_option', index=20, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FILEOPTIONS_OPTIMIZEMODE, + ], + serialized_options=None, + is_extendable=True, + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _MESSAGEOPTIONS = _descriptor.Descriptor( + name='MessageOptions', + full_name='google.protobuf.MessageOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='message_set_wire_format', full_name='google.protobuf.MessageOptions.message_set_wire_format', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='messageSetWireFormat', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='no_standard_descriptor_accessor', full_name='google.protobuf.MessageOptions.no_standard_descriptor_accessor', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='noStandardDescriptorAccessor', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.MessageOptions.deprecated', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='map_entry', full_name='google.protobuf.MessageOptions.map_entry', index=3, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='mapEntry', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated_legacy_json_field_conflicts', full_name='google.protobuf.MessageOptions.deprecated_legacy_json_field_conflicts', index=4, + number=11, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\030\001', json_name='deprecatedLegacyJsonFieldConflicts', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='features', full_name='google.protobuf.MessageOptions.features', index=5, + number=12, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='features', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.MessageOptions.uninterpreted_option', index=6, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _FIELDOPTIONS_EDITIONDEFAULT = _descriptor.Descriptor( + name='EditionDefault', + full_name='google.protobuf.FieldOptions.EditionDefault', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='edition', full_name='google.protobuf.FieldOptions.EditionDefault.edition', index=0, + number=3, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='edition', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='google.protobuf.FieldOptions.EditionDefault.value', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='value', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + _FIELDOPTIONS_FEATURESUPPORT = _descriptor.Descriptor( + name='FeatureSupport', + full_name='google.protobuf.FieldOptions.FeatureSupport', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='edition_introduced', full_name='google.protobuf.FieldOptions.FeatureSupport.edition_introduced', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='editionIntroduced', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='edition_deprecated', full_name='google.protobuf.FieldOptions.FeatureSupport.edition_deprecated', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='editionDeprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecation_warning', full_name='google.protobuf.FieldOptions.FeatureSupport.deprecation_warning', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='deprecationWarning', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='edition_removed', full_name='google.protobuf.FieldOptions.FeatureSupport.edition_removed', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='editionRemoved', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + _FIELDOPTIONS = _descriptor.Descriptor( + name='FieldOptions', + full_name='google.protobuf.FieldOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='ctype', full_name='google.protobuf.FieldOptions.ctype', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='ctype', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='packed', full_name='google.protobuf.FieldOptions.packed', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='packed', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='jstype', full_name='google.protobuf.FieldOptions.jstype', index=2, + number=6, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='jstype', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='lazy', full_name='google.protobuf.FieldOptions.lazy', index=3, + number=5, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='lazy', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='unverified_lazy', full_name='google.protobuf.FieldOptions.unverified_lazy', index=4, + number=15, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='unverifiedLazy', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.FieldOptions.deprecated', index=5, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='weak', full_name='google.protobuf.FieldOptions.weak', index=6, + number=10, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='weak', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='debug_redact', full_name='google.protobuf.FieldOptions.debug_redact', index=7, + number=16, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='debugRedact', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='retention', full_name='google.protobuf.FieldOptions.retention', index=8, + number=17, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='retention', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='targets', full_name='google.protobuf.FieldOptions.targets', index=9, + number=19, type=14, cpp_type=8, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='targets', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='edition_defaults', full_name='google.protobuf.FieldOptions.edition_defaults', index=10, + number=20, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='editionDefaults', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='features', full_name='google.protobuf.FieldOptions.features', index=11, + number=21, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='features', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='feature_support', full_name='google.protobuf.FieldOptions.feature_support', index=12, + number=22, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='featureSupport', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.FieldOptions.uninterpreted_option', index=13, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_FIELDOPTIONS_EDITIONDEFAULT, _FIELDOPTIONS_FEATURESUPPORT, ], + enum_types=[ + _FIELDOPTIONS_CTYPE, + _FIELDOPTIONS_JSTYPE, + _FIELDOPTIONS_OPTIONRETENTION, + _FIELDOPTIONS_OPTIONTARGETTYPE, + ], + serialized_options=None, + is_extendable=True, + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ONEOFOPTIONS = _descriptor.Descriptor( + name='OneofOptions', + full_name='google.protobuf.OneofOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='features', full_name='google.protobuf.OneofOptions.features', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='features', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.OneofOptions.uninterpreted_option', index=1, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ENUMOPTIONS = _descriptor.Descriptor( + name='EnumOptions', + full_name='google.protobuf.EnumOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='allow_alias', full_name='google.protobuf.EnumOptions.allow_alias', index=0, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='allowAlias', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.EnumOptions.deprecated', index=1, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated_legacy_json_field_conflicts', full_name='google.protobuf.EnumOptions.deprecated_legacy_json_field_conflicts', index=2, + number=6, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\030\001', json_name='deprecatedLegacyJsonFieldConflicts', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='features', full_name='google.protobuf.EnumOptions.features', index=3, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='features', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.EnumOptions.uninterpreted_option', index=4, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _ENUMVALUEOPTIONS = _descriptor.Descriptor( + name='EnumValueOptions', + full_name='google.protobuf.EnumValueOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.EnumValueOptions.deprecated', index=0, + number=1, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='features', full_name='google.protobuf.EnumValueOptions.features', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='features', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='debug_redact', full_name='google.protobuf.EnumValueOptions.debug_redact', index=2, + number=3, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='debugRedact', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='feature_support', full_name='google.protobuf.EnumValueOptions.feature_support', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='featureSupport', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.EnumValueOptions.uninterpreted_option', index=4, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _SERVICEOPTIONS = _descriptor.Descriptor( + name='ServiceOptions', + full_name='google.protobuf.ServiceOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='features', full_name='google.protobuf.ServiceOptions.features', index=0, + number=34, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='features', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.ServiceOptions.deprecated', index=1, + number=33, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.ServiceOptions.uninterpreted_option', index=2, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=True, + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _METHODOPTIONS = _descriptor.Descriptor( + name='MethodOptions', + full_name='google.protobuf.MethodOptions', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='deprecated', full_name='google.protobuf.MethodOptions.deprecated', index=0, + number=33, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='deprecated', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='idempotency_level', full_name='google.protobuf.MethodOptions.idempotency_level', index=1, + number=34, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='idempotencyLevel', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='features', full_name='google.protobuf.MethodOptions.features', index=2, + number=35, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='features', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='uninterpreted_option', full_name='google.protobuf.MethodOptions.uninterpreted_option', index=3, + number=999, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='uninterpretedOption', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _METHODOPTIONS_IDEMPOTENCYLEVEL, + ], + serialized_options=None, + is_extendable=True, + extension_ranges=[(1000, 536870912), ], + oneofs=[ + ], + ) + + + _UNINTERPRETEDOPTION_NAMEPART = _descriptor.Descriptor( + name='NamePart', + full_name='google.protobuf.UninterpretedOption.NamePart', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name_part', full_name='google.protobuf.UninterpretedOption.NamePart.name_part', index=0, + number=1, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='namePart', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='is_extension', full_name='google.protobuf.UninterpretedOption.NamePart.is_extension', index=1, + number=2, type=8, cpp_type=7, label=2, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='isExtension', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + _UNINTERPRETEDOPTION = _descriptor.Descriptor( + name='UninterpretedOption', + full_name='google.protobuf.UninterpretedOption', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='google.protobuf.UninterpretedOption.name', index=0, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='name', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='identifier_value', full_name='google.protobuf.UninterpretedOption.identifier_value', index=1, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='identifierValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='positive_int_value', full_name='google.protobuf.UninterpretedOption.positive_int_value', index=2, + number=4, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='positiveIntValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='negative_int_value', full_name='google.protobuf.UninterpretedOption.negative_int_value', index=3, + number=5, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='negativeIntValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='double_value', full_name='google.protobuf.UninterpretedOption.double_value', index=4, + number=6, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='doubleValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='string_value', full_name='google.protobuf.UninterpretedOption.string_value', index=5, + number=7, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='stringValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='aggregate_value', full_name='google.protobuf.UninterpretedOption.aggregate_value', index=6, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='aggregateValue', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_UNINTERPRETEDOPTION_NAMEPART, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _FEATURESET = _descriptor.Descriptor( + name='FeatureSet', + full_name='google.protobuf.FeatureSet', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='field_presence', full_name='google.protobuf.FeatureSet.field_presence', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\210\001\001\230\001\004\230\001\001\242\001\r\022\010EXPLICIT\030\346\007\242\001\r\022\010IMPLICIT\030\347\007\242\001\r\022\010EXPLICIT\030\350\007\262\001\003\010\350\007', json_name='fieldPresence', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='enum_type', full_name='google.protobuf.FeatureSet.enum_type', index=1, + number=2, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\210\001\001\230\001\006\230\001\001\242\001\013\022\006CLOSED\030\346\007\242\001\t\022\004OPEN\030\347\007\262\001\003\010\350\007', json_name='enumType', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='repeated_field_encoding', full_name='google.protobuf.FeatureSet.repeated_field_encoding', index=2, + number=3, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\210\001\001\230\001\004\230\001\001\242\001\r\022\010EXPANDED\030\346\007\242\001\013\022\006PACKED\030\347\007\262\001\003\010\350\007', json_name='repeatedFieldEncoding', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='utf8_validation', full_name='google.protobuf.FeatureSet.utf8_validation', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\210\001\001\230\001\004\230\001\001\242\001\t\022\004NONE\030\346\007\242\001\013\022\006VERIFY\030\347\007\262\001\003\010\350\007', json_name='utf8Validation', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='message_encoding', full_name='google.protobuf.FeatureSet.message_encoding', index=4, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\210\001\001\230\001\004\230\001\001\242\001\024\022\017LENGTH_PREFIXED\030\346\007\262\001\003\010\350\007', json_name='messageEncoding', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='json_format', full_name='google.protobuf.FeatureSet.json_format', index=5, + number=6, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\210\001\001\230\001\003\230\001\006\230\001\001\242\001\027\022\022LEGACY_BEST_EFFORT\030\346\007\242\001\n\022\005ALLOW\030\347\007\262\001\003\010\350\007', json_name='jsonFormat', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _FEATURESET_FIELDPRESENCE, + _FEATURESET_ENUMTYPE, + _FEATURESET_REPEATEDFIELDENCODING, + _FEATURESET_UTF8VALIDATION, + _FEATURESET_MESSAGEENCODING, + _FEATURESET_JSONFORMAT, + ], + serialized_options=None, + is_extendable=True, + extension_ranges=[(1000, 9995), (9995, 10000), (10000, 10001), ], + oneofs=[ + ], + ) + + + _FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT = _descriptor.Descriptor( + name='FeatureSetEditionDefault', + full_name='google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='edition', full_name='google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault.edition', index=0, + number=3, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='edition', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='overridable_features', full_name='google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault.overridable_features', index=1, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='overridableFeatures', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='fixed_features', full_name='google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault.fixed_features', index=2, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='fixedFeatures', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + _FEATURESETDEFAULTS = _descriptor.Descriptor( + name='FeatureSetDefaults', + full_name='google.protobuf.FeatureSetDefaults', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='defaults', full_name='google.protobuf.FeatureSetDefaults.defaults', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='defaults', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='minimum_edition', full_name='google.protobuf.FeatureSetDefaults.minimum_edition', index=1, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='minimumEdition', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='maximum_edition', full_name='google.protobuf.FeatureSetDefaults.maximum_edition', index=2, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='maximumEdition', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _SOURCECODEINFO_LOCATION = _descriptor.Descriptor( + name='Location', + full_name='google.protobuf.SourceCodeInfo.Location', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='path', full_name='google.protobuf.SourceCodeInfo.Location.path', index=0, + number=1, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\020\001', json_name='path', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='span', full_name='google.protobuf.SourceCodeInfo.Location.span', index=1, + number=2, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\020\001', json_name='span', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='leading_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_comments', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='leadingComments', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='trailing_comments', full_name='google.protobuf.SourceCodeInfo.Location.trailing_comments', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='trailingComments', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='leading_detached_comments', full_name='google.protobuf.SourceCodeInfo.Location.leading_detached_comments', index=4, + number=6, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='leadingDetachedComments', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + _SOURCECODEINFO = _descriptor.Descriptor( + name='SourceCodeInfo', + full_name='google.protobuf.SourceCodeInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='location', full_name='google.protobuf.SourceCodeInfo.location', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='location', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_SOURCECODEINFO_LOCATION, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + + _GENERATEDCODEINFO_ANNOTATION = _descriptor.Descriptor( + name='Annotation', + full_name='google.protobuf.GeneratedCodeInfo.Annotation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='path', full_name='google.protobuf.GeneratedCodeInfo.Annotation.path', index=0, + number=1, type=5, cpp_type=1, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\020\001', json_name='path', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='source_file', full_name='google.protobuf.GeneratedCodeInfo.Annotation.source_file', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='sourceFile', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='begin', full_name='google.protobuf.GeneratedCodeInfo.Annotation.begin', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='begin', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='end', full_name='google.protobuf.GeneratedCodeInfo.Annotation.end', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='end', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='semantic', full_name='google.protobuf.GeneratedCodeInfo.Annotation.semantic', index=4, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='semantic', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _GENERATEDCODEINFO_ANNOTATION_SEMANTIC, + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + _GENERATEDCODEINFO = _descriptor.Descriptor( + name='GeneratedCodeInfo', + full_name='google.protobuf.GeneratedCodeInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='annotation', full_name='google.protobuf.GeneratedCodeInfo.annotation', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, json_name='annotation', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_GENERATEDCODEINFO_ANNOTATION, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + ) + + _FILEDESCRIPTORSET.fields_by_name['file'].message_type = _FILEDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['message_type'].message_type = _DESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['service'].message_type = _SERVICEDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO + _FILEDESCRIPTORPROTO.fields_by_name['options'].message_type = _FILEOPTIONS + _FILEDESCRIPTORPROTO.fields_by_name['source_code_info'].message_type = _SOURCECODEINFO + _FILEDESCRIPTORPROTO.fields_by_name['edition'].enum_type = _EDITION + _DESCRIPTORPROTO_EXTENSIONRANGE.fields_by_name['options'].message_type = _EXTENSIONRANGEOPTIONS + _DESCRIPTORPROTO_EXTENSIONRANGE.containing_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO_RESERVEDRANGE.containing_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['field'].message_type = _FIELDDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['extension'].message_type = _FIELDDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['nested_type'].message_type = _DESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['enum_type'].message_type = _ENUMDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['extension_range'].message_type = _DESCRIPTORPROTO_EXTENSIONRANGE + _DESCRIPTORPROTO.fields_by_name['oneof_decl'].message_type = _ONEOFDESCRIPTORPROTO + _DESCRIPTORPROTO.fields_by_name['options'].message_type = _MESSAGEOPTIONS + _DESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _DESCRIPTORPROTO_RESERVEDRANGE + _EXTENSIONRANGEOPTIONS_DECLARATION.containing_type = _EXTENSIONRANGEOPTIONS + _EXTENSIONRANGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _EXTENSIONRANGEOPTIONS.fields_by_name['declaration'].message_type = _EXTENSIONRANGEOPTIONS_DECLARATION + _EXTENSIONRANGEOPTIONS.fields_by_name['features'].message_type = _FEATURESET + _EXTENSIONRANGEOPTIONS.fields_by_name['verification'].enum_type = _EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE + _EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE.containing_type = _EXTENSIONRANGEOPTIONS + _FIELDDESCRIPTORPROTO.fields_by_name['label'].enum_type = _FIELDDESCRIPTORPROTO_LABEL + _FIELDDESCRIPTORPROTO.fields_by_name['type'].enum_type = _FIELDDESCRIPTORPROTO_TYPE + _FIELDDESCRIPTORPROTO.fields_by_name['options'].message_type = _FIELDOPTIONS + _FIELDDESCRIPTORPROTO_TYPE.containing_type = _FIELDDESCRIPTORPROTO + _FIELDDESCRIPTORPROTO_LABEL.containing_type = _FIELDDESCRIPTORPROTO + _ONEOFDESCRIPTORPROTO.fields_by_name['options'].message_type = _ONEOFOPTIONS + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE.containing_type = _ENUMDESCRIPTORPROTO + _ENUMDESCRIPTORPROTO.fields_by_name['value'].message_type = _ENUMVALUEDESCRIPTORPROTO + _ENUMDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMOPTIONS + _ENUMDESCRIPTORPROTO.fields_by_name['reserved_range'].message_type = _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE + _ENUMVALUEDESCRIPTORPROTO.fields_by_name['options'].message_type = _ENUMVALUEOPTIONS + _SERVICEDESCRIPTORPROTO.fields_by_name['method'].message_type = _METHODDESCRIPTORPROTO + _SERVICEDESCRIPTORPROTO.fields_by_name['options'].message_type = _SERVICEOPTIONS + _METHODDESCRIPTORPROTO.fields_by_name['options'].message_type = _METHODOPTIONS + _FILEOPTIONS.fields_by_name['optimize_for'].enum_type = _FILEOPTIONS_OPTIMIZEMODE + _FILEOPTIONS.fields_by_name['features'].message_type = _FEATURESET + _FILEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FILEOPTIONS_OPTIMIZEMODE.containing_type = _FILEOPTIONS + _MESSAGEOPTIONS.fields_by_name['features'].message_type = _FEATURESET + _MESSAGEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FIELDOPTIONS_EDITIONDEFAULT.fields_by_name['edition'].enum_type = _EDITION + _FIELDOPTIONS_EDITIONDEFAULT.containing_type = _FIELDOPTIONS + _FIELDOPTIONS_FEATURESUPPORT.fields_by_name['edition_introduced'].enum_type = _EDITION + _FIELDOPTIONS_FEATURESUPPORT.fields_by_name['edition_deprecated'].enum_type = _EDITION + _FIELDOPTIONS_FEATURESUPPORT.fields_by_name['edition_removed'].enum_type = _EDITION + _FIELDOPTIONS_FEATURESUPPORT.containing_type = _FIELDOPTIONS + _FIELDOPTIONS.fields_by_name['ctype'].enum_type = _FIELDOPTIONS_CTYPE + _FIELDOPTIONS.fields_by_name['jstype'].enum_type = _FIELDOPTIONS_JSTYPE + _FIELDOPTIONS.fields_by_name['retention'].enum_type = _FIELDOPTIONS_OPTIONRETENTION + _FIELDOPTIONS.fields_by_name['targets'].enum_type = _FIELDOPTIONS_OPTIONTARGETTYPE + _FIELDOPTIONS.fields_by_name['edition_defaults'].message_type = _FIELDOPTIONS_EDITIONDEFAULT + _FIELDOPTIONS.fields_by_name['features'].message_type = _FEATURESET + _FIELDOPTIONS.fields_by_name['feature_support'].message_type = _FIELDOPTIONS_FEATURESUPPORT + _FIELDOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _FIELDOPTIONS_CTYPE.containing_type = _FIELDOPTIONS + _FIELDOPTIONS_JSTYPE.containing_type = _FIELDOPTIONS + _FIELDOPTIONS_OPTIONRETENTION.containing_type = _FIELDOPTIONS + _FIELDOPTIONS_OPTIONTARGETTYPE.containing_type = _FIELDOPTIONS + _ONEOFOPTIONS.fields_by_name['features'].message_type = _FEATURESET + _ONEOFOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _ENUMOPTIONS.fields_by_name['features'].message_type = _FEATURESET + _ENUMOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _ENUMVALUEOPTIONS.fields_by_name['features'].message_type = _FEATURESET + _ENUMVALUEOPTIONS.fields_by_name['feature_support'].message_type = _FIELDOPTIONS_FEATURESUPPORT + _ENUMVALUEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _SERVICEOPTIONS.fields_by_name['features'].message_type = _FEATURESET + _SERVICEOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _METHODOPTIONS.fields_by_name['idempotency_level'].enum_type = _METHODOPTIONS_IDEMPOTENCYLEVEL + _METHODOPTIONS.fields_by_name['features'].message_type = _FEATURESET + _METHODOPTIONS.fields_by_name['uninterpreted_option'].message_type = _UNINTERPRETEDOPTION + _METHODOPTIONS_IDEMPOTENCYLEVEL.containing_type = _METHODOPTIONS + _UNINTERPRETEDOPTION_NAMEPART.containing_type = _UNINTERPRETEDOPTION + _UNINTERPRETEDOPTION.fields_by_name['name'].message_type = _UNINTERPRETEDOPTION_NAMEPART + _FEATURESET.fields_by_name['field_presence'].enum_type = _FEATURESET_FIELDPRESENCE + _FEATURESET.fields_by_name['enum_type'].enum_type = _FEATURESET_ENUMTYPE + _FEATURESET.fields_by_name['repeated_field_encoding'].enum_type = _FEATURESET_REPEATEDFIELDENCODING + _FEATURESET.fields_by_name['utf8_validation'].enum_type = _FEATURESET_UTF8VALIDATION + _FEATURESET.fields_by_name['message_encoding'].enum_type = _FEATURESET_MESSAGEENCODING + _FEATURESET.fields_by_name['json_format'].enum_type = _FEATURESET_JSONFORMAT + _FEATURESET_FIELDPRESENCE.containing_type = _FEATURESET + _FEATURESET_ENUMTYPE.containing_type = _FEATURESET + _FEATURESET_REPEATEDFIELDENCODING.containing_type = _FEATURESET + _FEATURESET_UTF8VALIDATION.containing_type = _FEATURESET + _FEATURESET_MESSAGEENCODING.containing_type = _FEATURESET + _FEATURESET_JSONFORMAT.containing_type = _FEATURESET + _FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT.fields_by_name['edition'].enum_type = _EDITION + _FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT.fields_by_name['overridable_features'].message_type = _FEATURESET + _FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT.fields_by_name['fixed_features'].message_type = _FEATURESET + _FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT.containing_type = _FEATURESETDEFAULTS + _FEATURESETDEFAULTS.fields_by_name['defaults'].message_type = _FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT + _FEATURESETDEFAULTS.fields_by_name['minimum_edition'].enum_type = _EDITION + _FEATURESETDEFAULTS.fields_by_name['maximum_edition'].enum_type = _EDITION + _SOURCECODEINFO_LOCATION.containing_type = _SOURCECODEINFO + _SOURCECODEINFO.fields_by_name['location'].message_type = _SOURCECODEINFO_LOCATION + _GENERATEDCODEINFO_ANNOTATION.fields_by_name['semantic'].enum_type = _GENERATEDCODEINFO_ANNOTATION_SEMANTIC + _GENERATEDCODEINFO_ANNOTATION.containing_type = _GENERATEDCODEINFO + _GENERATEDCODEINFO_ANNOTATION_SEMANTIC.containing_type = _GENERATEDCODEINFO_ANNOTATION + _GENERATEDCODEINFO.fields_by_name['annotation'].message_type = _GENERATEDCODEINFO_ANNOTATION + DESCRIPTOR.message_types_by_name['FileDescriptorSet'] = _FILEDESCRIPTORSET + DESCRIPTOR.message_types_by_name['FileDescriptorProto'] = _FILEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['DescriptorProto'] = _DESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['ExtensionRangeOptions'] = _EXTENSIONRANGEOPTIONS + DESCRIPTOR.message_types_by_name['FieldDescriptorProto'] = _FIELDDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['OneofDescriptorProto'] = _ONEOFDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['EnumDescriptorProto'] = _ENUMDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['EnumValueDescriptorProto'] = _ENUMVALUEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['ServiceDescriptorProto'] = _SERVICEDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['MethodDescriptorProto'] = _METHODDESCRIPTORPROTO + DESCRIPTOR.message_types_by_name['FileOptions'] = _FILEOPTIONS + DESCRIPTOR.message_types_by_name['MessageOptions'] = _MESSAGEOPTIONS + DESCRIPTOR.message_types_by_name['FieldOptions'] = _FIELDOPTIONS + DESCRIPTOR.message_types_by_name['OneofOptions'] = _ONEOFOPTIONS + DESCRIPTOR.message_types_by_name['EnumOptions'] = _ENUMOPTIONS + DESCRIPTOR.message_types_by_name['EnumValueOptions'] = _ENUMVALUEOPTIONS + DESCRIPTOR.message_types_by_name['ServiceOptions'] = _SERVICEOPTIONS + DESCRIPTOR.message_types_by_name['MethodOptions'] = _METHODOPTIONS + DESCRIPTOR.message_types_by_name['UninterpretedOption'] = _UNINTERPRETEDOPTION + DESCRIPTOR.message_types_by_name['FeatureSet'] = _FEATURESET + DESCRIPTOR.message_types_by_name['FeatureSetDefaults'] = _FEATURESETDEFAULTS + DESCRIPTOR.message_types_by_name['SourceCodeInfo'] = _SOURCECODEINFO + DESCRIPTOR.message_types_by_name['GeneratedCodeInfo'] = _GENERATEDCODEINFO + DESCRIPTOR.enum_types_by_name['Edition'] = _EDITION + _sym_db.RegisterFileDescriptor(DESCRIPTOR) + + class _ResolvedFeatures: + def __init__(self, features = None, **kwargs): + if features: + for k, v in features.FIELDS.items(): + setattr(self, k, getattr(features, k)) + else: + for k, v in kwargs.items(): + setattr(self, k, v) + DESCRIPTOR._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORSET._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORSET.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[6]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[7]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[8]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[9]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[10]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[11]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEDESCRIPTORPROTO.fields[12]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO.fields[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO.fields[6]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO.fields[7]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO.fields[8]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO.fields[9]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO_EXTENSIONRANGE._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO_EXTENSIONRANGE.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO_EXTENSIONRANGE.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO_EXTENSIONRANGE.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO_RESERVEDRANGE._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO_RESERVEDRANGE.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _DESCRIPTORPROTO_RESERVEDRANGE.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS_DECLARATION._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS_DECLARATION.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS_DECLARATION.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS_DECLARATION.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS_DECLARATION.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS_DECLARATION.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO.fields[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO.fields[6]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO.fields[7]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO.fields[8]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO.fields[9]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO.fields[10]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ONEOFDESCRIPTORPROTO._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ONEOFDESCRIPTORPROTO.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ONEOFDESCRIPTORPROTO.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMDESCRIPTORPROTO._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMDESCRIPTORPROTO.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMDESCRIPTORPROTO.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMDESCRIPTORPROTO.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMDESCRIPTORPROTO.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMDESCRIPTORPROTO.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMVALUEDESCRIPTORPROTO._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMVALUEDESCRIPTORPROTO.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMVALUEDESCRIPTORPROTO.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMVALUEDESCRIPTORPROTO.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SERVICEDESCRIPTORPROTO._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SERVICEDESCRIPTORPROTO.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SERVICEDESCRIPTORPROTO.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SERVICEDESCRIPTORPROTO.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODDESCRIPTORPROTO._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODDESCRIPTORPROTO.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODDESCRIPTORPROTO.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODDESCRIPTORPROTO.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODDESCRIPTORPROTO.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODDESCRIPTORPROTO.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODDESCRIPTORPROTO.fields[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[6]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[7]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[8]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[9]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[10]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[11]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[12]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[13]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[14]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[15]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[16]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[17]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[18]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[19]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS.fields[20]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _MESSAGEOPTIONS._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _MESSAGEOPTIONS.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _MESSAGEOPTIONS.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _MESSAGEOPTIONS.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _MESSAGEOPTIONS.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _MESSAGEOPTIONS.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _MESSAGEOPTIONS.fields[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _MESSAGEOPTIONS.fields[6]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[6]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[7]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[8]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[9]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[10]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[11]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[12]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS.fields[13]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_EDITIONDEFAULT._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_EDITIONDEFAULT.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_EDITIONDEFAULT.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_FEATURESUPPORT._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_FEATURESUPPORT.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_FEATURESUPPORT.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_FEATURESUPPORT.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_FEATURESUPPORT.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ONEOFOPTIONS._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ONEOFOPTIONS.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ONEOFOPTIONS.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMOPTIONS._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMOPTIONS.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMOPTIONS.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMOPTIONS.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMOPTIONS.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMOPTIONS.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMVALUEOPTIONS._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMVALUEOPTIONS.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMVALUEOPTIONS.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMVALUEOPTIONS.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMVALUEOPTIONS.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _ENUMVALUEOPTIONS.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SERVICEOPTIONS._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SERVICEOPTIONS.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SERVICEOPTIONS.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SERVICEOPTIONS.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODOPTIONS._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODOPTIONS.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODOPTIONS.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODOPTIONS.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODOPTIONS.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _UNINTERPRETEDOPTION._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _UNINTERPRETEDOPTION.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _UNINTERPRETEDOPTION.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _UNINTERPRETEDOPTION.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _UNINTERPRETEDOPTION.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _UNINTERPRETEDOPTION.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _UNINTERPRETEDOPTION.fields[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _UNINTERPRETEDOPTION.fields[6]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _UNINTERPRETEDOPTION_NAMEPART._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _UNINTERPRETEDOPTION_NAMEPART.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["LEGACY_REQUIRED"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _UNINTERPRETEDOPTION_NAMEPART.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["LEGACY_REQUIRED"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET.fields[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESETDEFAULTS._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESETDEFAULTS.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESETDEFAULTS.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESETDEFAULTS.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SOURCECODEINFO._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SOURCECODEINFO.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SOURCECODEINFO_LOCATION._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SOURCECODEINFO_LOCATION.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["PACKED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SOURCECODEINFO_LOCATION.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["PACKED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SOURCECODEINFO_LOCATION.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SOURCECODEINFO_LOCATION.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _SOURCECODEINFO_LOCATION.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO_ANNOTATION._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO_ANNOTATION.fields[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["PACKED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO_ANNOTATION.fields[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO_ANNOTATION.fields[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO_ANNOTATION.fields[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO_ANNOTATION.fields[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[6]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[7]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[8]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[9]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[10]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[11]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[12]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[13]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[14]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[15]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[16]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_TYPE.values[17]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_LABEL._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_LABEL.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_LABEL.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDDESCRIPTORPROTO_LABEL.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS_OPTIMIZEMODE._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS_OPTIMIZEMODE.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS_OPTIMIZEMODE.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FILEOPTIONS_OPTIMIZEMODE.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_CTYPE._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_CTYPE.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_CTYPE.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_CTYPE.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_JSTYPE._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_JSTYPE.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_JSTYPE.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_JSTYPE.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONRETENTION._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONRETENTION.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONRETENTION.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONRETENTION.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONTARGETTYPE._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONTARGETTYPE.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONTARGETTYPE.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONTARGETTYPE.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONTARGETTYPE.values[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONTARGETTYPE.values[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONTARGETTYPE.values[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONTARGETTYPE.values[6]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONTARGETTYPE.values[7]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONTARGETTYPE.values[8]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FIELDOPTIONS_OPTIONTARGETTYPE.values[9]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODOPTIONS_IDEMPOTENCYLEVEL._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODOPTIONS_IDEMPOTENCYLEVEL.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODOPTIONS_IDEMPOTENCYLEVEL.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _METHODOPTIONS_IDEMPOTENCYLEVEL.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_FIELDPRESENCE._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_FIELDPRESENCE.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_FIELDPRESENCE.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_FIELDPRESENCE.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_FIELDPRESENCE.values[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_ENUMTYPE._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_ENUMTYPE.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_ENUMTYPE.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_ENUMTYPE.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_REPEATEDFIELDENCODING._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_REPEATEDFIELDENCODING.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_REPEATEDFIELDENCODING.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_REPEATEDFIELDENCODING.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_UTF8VALIDATION._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_UTF8VALIDATION.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_UTF8VALIDATION.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_UTF8VALIDATION.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_MESSAGEENCODING._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_MESSAGEENCODING.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_MESSAGEENCODING.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_MESSAGEENCODING.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_JSONFORMAT._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_JSONFORMAT.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_JSONFORMAT.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _FEATURESET_JSONFORMAT.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO_ANNOTATION_SEMANTIC._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO_ANNOTATION_SEMANTIC.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO_ANNOTATION_SEMANTIC.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _GENERATEDCODEINFO_ANNOTATION_SEMANTIC.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[0]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[1]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[2]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[3]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[4]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[5]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[6]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[7]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[8]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[9]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[10]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) + _EDITION.values[11]._features = _ResolvedFeatures(field_presence=_FEATURESET_FIELDPRESENCE.values_by_name["EXPLICIT"].number,enum_type=_FEATURESET_ENUMTYPE.values_by_name["CLOSED"].number,repeated_field_encoding=_FEATURESET_REPEATEDFIELDENCODING.values_by_name["EXPANDED"].number,utf8_validation=_FEATURESET_UTF8VALIDATION.values_by_name["NONE"].number,message_encoding=_FEATURESET_MESSAGEENCODING.values_by_name["LENGTH_PREFIXED"].number,json_format=_FEATURESET_JSONFORMAT.values_by_name["LEGACY_BEST_EFFORT"].number) +else: + _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.descriptor_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\020DescriptorProtosH\001Z-google.golang.org/protobuf/types/descriptorpb\370\001\001\242\002\003GPB\252\002\032Google.Protobuf.Reflection' + _globals['_EXTENSIONRANGEOPTIONS'].fields_by_name['declaration']._loaded_options = None + _globals['_EXTENSIONRANGEOPTIONS'].fields_by_name['declaration']._serialized_options = b'\210\001\002' + _globals['_EXTENSIONRANGEOPTIONS'].fields_by_name['verification']._loaded_options = None + _globals['_EXTENSIONRANGEOPTIONS'].fields_by_name['verification']._serialized_options = b'\210\001\002' + _globals['_FILEOPTIONS'].fields_by_name['java_generate_equals_and_hash']._loaded_options = None + _globals['_FILEOPTIONS'].fields_by_name['java_generate_equals_and_hash']._serialized_options = b'\030\001' + _globals['_MESSAGEOPTIONS'].fields_by_name['deprecated_legacy_json_field_conflicts']._loaded_options = None + _globals['_MESSAGEOPTIONS'].fields_by_name['deprecated_legacy_json_field_conflicts']._serialized_options = b'\030\001' + _globals['_ENUMOPTIONS'].fields_by_name['deprecated_legacy_json_field_conflicts']._loaded_options = None + _globals['_ENUMOPTIONS'].fields_by_name['deprecated_legacy_json_field_conflicts']._serialized_options = b'\030\001' + _globals['_FEATURESET'].fields_by_name['field_presence']._loaded_options = None + _globals['_FEATURESET'].fields_by_name['field_presence']._serialized_options = b'\210\001\001\230\001\004\230\001\001\242\001\r\022\010EXPLICIT\030\346\007\242\001\r\022\010IMPLICIT\030\347\007\242\001\r\022\010EXPLICIT\030\350\007\262\001\003\010\350\007' + _globals['_FEATURESET'].fields_by_name['enum_type']._loaded_options = None + _globals['_FEATURESET'].fields_by_name['enum_type']._serialized_options = b'\210\001\001\230\001\006\230\001\001\242\001\013\022\006CLOSED\030\346\007\242\001\t\022\004OPEN\030\347\007\262\001\003\010\350\007' + _globals['_FEATURESET'].fields_by_name['repeated_field_encoding']._loaded_options = None + _globals['_FEATURESET'].fields_by_name['repeated_field_encoding']._serialized_options = b'\210\001\001\230\001\004\230\001\001\242\001\r\022\010EXPANDED\030\346\007\242\001\013\022\006PACKED\030\347\007\262\001\003\010\350\007' + _globals['_FEATURESET'].fields_by_name['utf8_validation']._loaded_options = None + _globals['_FEATURESET'].fields_by_name['utf8_validation']._serialized_options = b'\210\001\001\230\001\004\230\001\001\242\001\t\022\004NONE\030\346\007\242\001\013\022\006VERIFY\030\347\007\262\001\003\010\350\007' + _globals['_FEATURESET'].fields_by_name['message_encoding']._loaded_options = None + _globals['_FEATURESET'].fields_by_name['message_encoding']._serialized_options = b'\210\001\001\230\001\004\230\001\001\242\001\024\022\017LENGTH_PREFIXED\030\346\007\262\001\003\010\350\007' + _globals['_FEATURESET'].fields_by_name['json_format']._loaded_options = None + _globals['_FEATURESET'].fields_by_name['json_format']._serialized_options = b'\210\001\001\230\001\003\230\001\006\230\001\001\242\001\027\022\022LEGACY_BEST_EFFORT\030\346\007\242\001\n\022\005ALLOW\030\347\007\262\001\003\010\350\007' + _globals['_SOURCECODEINFO_LOCATION'].fields_by_name['path']._loaded_options = None + _globals['_SOURCECODEINFO_LOCATION'].fields_by_name['path']._serialized_options = b'\020\001' + _globals['_SOURCECODEINFO_LOCATION'].fields_by_name['span']._loaded_options = None + _globals['_SOURCECODEINFO_LOCATION'].fields_by_name['span']._serialized_options = b'\020\001' + _globals['_GENERATEDCODEINFO_ANNOTATION'].fields_by_name['path']._loaded_options = None + _globals['_GENERATEDCODEINFO_ANNOTATION'].fields_by_name['path']._serialized_options = b'\020\001' + _globals['_EDITION']._serialized_start=11845 + _globals['_EDITION']._serialized_end=12140 + _globals['_FILEDESCRIPTORSET']._serialized_start=53 + _globals['_FILEDESCRIPTORSET']._serialized_end=130 + _globals['_FILEDESCRIPTORPROTO']._serialized_start=133 + _globals['_FILEDESCRIPTORPROTO']._serialized_end=797 + _globals['_DESCRIPTORPROTO']._serialized_start=800 + _globals['_DESCRIPTORPROTO']._serialized_end=1625 + _globals['_DESCRIPTORPROTO_EXTENSIONRANGE']._serialized_start=1446 + _globals['_DESCRIPTORPROTO_EXTENSIONRANGE']._serialized_end=1568 + _globals['_DESCRIPTORPROTO_RESERVEDRANGE']._serialized_start=1570 + _globals['_DESCRIPTORPROTO_RESERVEDRANGE']._serialized_end=1625 + _globals['_EXTENSIONRANGEOPTIONS']._serialized_start=1628 + _globals['_EXTENSIONRANGEOPTIONS']._serialized_end=2216 + _globals['_EXTENSIONRANGEOPTIONS_DECLARATION']._serialized_start=2003 + _globals['_EXTENSIONRANGEOPTIONS_DECLARATION']._serialized_end=2151 + _globals['_EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE']._serialized_start=2153 + _globals['_EXTENSIONRANGEOPTIONS_VERIFICATIONSTATE']._serialized_end=2205 + _globals['_FIELDDESCRIPTORPROTO']._serialized_start=2219 + _globals['_FIELDDESCRIPTORPROTO']._serialized_end=3052 + _globals['_FIELDDESCRIPTORPROTO_TYPE']._serialized_start=2673 + _globals['_FIELDDESCRIPTORPROTO_TYPE']._serialized_end=2983 + _globals['_FIELDDESCRIPTORPROTO_LABEL']._serialized_start=2985 + _globals['_FIELDDESCRIPTORPROTO_LABEL']._serialized_end=3052 + _globals['_ONEOFDESCRIPTORPROTO']._serialized_start=3054 + _globals['_ONEOFDESCRIPTORPROTO']._serialized_end=3153 + _globals['_ENUMDESCRIPTORPROTO']._serialized_start=3156 + _globals['_ENUMDESCRIPTORPROTO']._serialized_end=3511 + _globals['_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE']._serialized_start=3452 + _globals['_ENUMDESCRIPTORPROTO_ENUMRESERVEDRANGE']._serialized_end=3511 + _globals['_ENUMVALUEDESCRIPTORPROTO']._serialized_start=3514 + _globals['_ENUMVALUEDESCRIPTORPROTO']._serialized_end=3645 + _globals['_SERVICEDESCRIPTORPROTO']._serialized_start=3648 + _globals['_SERVICEDESCRIPTORPROTO']._serialized_end=3815 + _globals['_METHODDESCRIPTORPROTO']._serialized_start=3818 + _globals['_METHODDESCRIPTORPROTO']._serialized_end=4083 + _globals['_FILEOPTIONS']._serialized_start=4086 + _globals['_FILEOPTIONS']._serialized_end=5283 + _globals['_FILEOPTIONS_OPTIMIZEMODE']._serialized_start=5180 + _globals['_FILEOPTIONS_OPTIMIZEMODE']._serialized_end=5238 + _globals['_MESSAGEOPTIONS']._serialized_start=5286 + _globals['_MESSAGEOPTIONS']._serialized_end=5786 + _globals['_FIELDOPTIONS']._serialized_start=5789 + _globals['_FIELDOPTIONS']._serialized_end=7482 + _globals['_FIELDOPTIONS_EDITIONDEFAULT']._serialized_start=6626 + _globals['_FIELDOPTIONS_EDITIONDEFAULT']._serialized_end=6716 + _globals['_FIELDOPTIONS_FEATURESUPPORT']._serialized_start=6719 + _globals['_FIELDOPTIONS_FEATURESUPPORT']._serialized_end=6997 + _globals['_FIELDOPTIONS_CTYPE']._serialized_start=6999 + _globals['_FIELDOPTIONS_CTYPE']._serialized_end=7046 + _globals['_FIELDOPTIONS_JSTYPE']._serialized_start=7048 + _globals['_FIELDOPTIONS_JSTYPE']._serialized_end=7101 + _globals['_FIELDOPTIONS_OPTIONRETENTION']._serialized_start=7103 + _globals['_FIELDOPTIONS_OPTIONRETENTION']._serialized_end=7188 + _globals['_FIELDOPTIONS_OPTIONTARGETTYPE']._serialized_start=7191 + _globals['_FIELDOPTIONS_OPTIONTARGETTYPE']._serialized_end=7459 + _globals['_ONEOFOPTIONS']._serialized_start=7485 + _globals['_ONEOFOPTIONS']._serialized_end=7657 + _globals['_ENUMOPTIONS']._serialized_start=7660 + _globals['_ENUMOPTIONS']._serialized_end=7997 + _globals['_ENUMVALUEOPTIONS']._serialized_start=8000 + _globals['_ENUMVALUEOPTIONS']._serialized_end=8344 + _globals['_SERVICEOPTIONS']._serialized_start=8347 + _globals['_SERVICEOPTIONS']._serialized_end=8560 + _globals['_METHODOPTIONS']._serialized_start=8563 + _globals['_METHODOPTIONS']._serialized_end=8972 + _globals['_METHODOPTIONS_IDEMPOTENCYLEVEL']._serialized_start=8881 + _globals['_METHODOPTIONS_IDEMPOTENCYLEVEL']._serialized_end=8961 + _globals['_UNINTERPRETEDOPTION']._serialized_start=8975 + _globals['_UNINTERPRETEDOPTION']._serialized_end=9385 + _globals['_UNINTERPRETEDOPTION_NAMEPART']._serialized_start=9311 + _globals['_UNINTERPRETEDOPTION_NAMEPART']._serialized_end=9385 + _globals['_FEATURESET']._serialized_start=9388 + _globals['_FEATURESET']._serialized_end=10707 + _globals['_FEATURESET_FIELDPRESENCE']._serialized_start=10204 + _globals['_FEATURESET_FIELDPRESENCE']._serialized_end=10296 + _globals['_FEATURESET_ENUMTYPE']._serialized_start=10298 + _globals['_FEATURESET_ENUMTYPE']._serialized_end=10353 + _globals['_FEATURESET_REPEATEDFIELDENCODING']._serialized_start=10355 + _globals['_FEATURESET_REPEATEDFIELDENCODING']._serialized_end=10441 + _globals['_FEATURESET_UTF8VALIDATION']._serialized_start=10443 + _globals['_FEATURESET_UTF8VALIDATION']._serialized_end=10516 + _globals['_FEATURESET_MESSAGEENCODING']._serialized_start=10518 + _globals['_FEATURESET_MESSAGEENCODING']._serialized_end=10601 + _globals['_FEATURESET_JSONFORMAT']._serialized_start=10603 + _globals['_FEATURESET_JSONFORMAT']._serialized_end=10675 + _globals['_FEATURESETDEFAULTS']._serialized_start=10710 + _globals['_FEATURESETDEFAULTS']._serialized_end=11205 + _globals['_FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT']._serialized_start=10957 + _globals['_FEATURESETDEFAULTS_FEATURESETEDITIONDEFAULT']._serialized_end=11205 + _globals['_SOURCECODEINFO']._serialized_start=11208 + _globals['_SOURCECODEINFO']._serialized_end=11503 + _globals['_SOURCECODEINFO_LOCATION']._serialized_start=11297 + _globals['_SOURCECODEINFO_LOCATION']._serialized_end=11503 + _globals['_GENERATEDCODEINFO']._serialized_start=11506 + _globals['_GENERATEDCODEINFO']._serialized_end=11842 + _globals['_GENERATEDCODEINFO_ANNOTATION']._serialized_start=11607 + _globals['_GENERATEDCODEINFO_ANNOTATION']._serialized_end=11842 + _globals['_GENERATEDCODEINFO_ANNOTATION_SEMANTIC']._serialized_start=11802 + _globals['_GENERATEDCODEINFO_ANNOTATION_SEMANTIC']._serialized_end=11842 +# @@protoc_insertion_point(module_scope) diff --git a/google/protobuf/descriptor_pool.py b/google/protobuf/descriptor_pool.py new file mode 100644 index 0000000..5545aed --- /dev/null +++ b/google/protobuf/descriptor_pool.py @@ -0,0 +1,1355 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Provides DescriptorPool to use as a container for proto2 descriptors. + +The DescriptorPool is used in conjection with a DescriptorDatabase to maintain +a collection of protocol buffer descriptors for use when dynamically creating +message types at runtime. + +For most applications protocol buffers should be used via modules generated by +the protocol buffer compiler tool. This should only be used when the type of +protocol buffers used in an application or library cannot be predetermined. + +Below is a straightforward example on how to use this class:: + + pool = DescriptorPool() + file_descriptor_protos = [ ... ] + for file_descriptor_proto in file_descriptor_protos: + pool.Add(file_descriptor_proto) + my_message_descriptor = pool.FindMessageTypeByName('some.package.MessageType') + +The message descriptor can be used in conjunction with the message_factory +module in order to create a protocol buffer class that can be encoded and +decoded. + +If you want to get a Python class for the specified proto, use the +helper functions inside google.protobuf.message_factory +directly instead of this class. +""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +import collections +import threading +import warnings + +from google.protobuf import descriptor +from google.protobuf import descriptor_database +from google.protobuf import text_encoding +from google.protobuf.internal import python_edition_defaults +from google.protobuf.internal import python_message + +_USE_C_DESCRIPTORS = descriptor._USE_C_DESCRIPTORS # pylint: disable=protected-access + + +def _NormalizeFullyQualifiedName(name): + """Remove leading period from fully-qualified type name. + + Due to b/13860351 in descriptor_database.py, types in the root namespace are + generated with a leading period. This function removes that prefix. + + Args: + name (str): The fully-qualified symbol name. + + Returns: + str: The normalized fully-qualified symbol name. + """ + return name.lstrip('.') + + +def _OptionsOrNone(descriptor_proto): + """Returns the value of the field `options`, or None if it is not set.""" + if descriptor_proto.HasField('options'): + return descriptor_proto.options + else: + return None + + +def _IsMessageSetExtension(field): + return (field.is_extension and + field.containing_type.has_options and + field.containing_type.GetOptions().message_set_wire_format and + field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL) + +_edition_defaults_lock = threading.Lock() + + +class DescriptorPool(object): + """A collection of protobufs dynamically constructed by descriptor protos.""" + + if _USE_C_DESCRIPTORS: + + def __new__(cls, descriptor_db=None): + # pylint: disable=protected-access + return descriptor._message.DescriptorPool(descriptor_db) + + def __init__( + self, descriptor_db=None, use_deprecated_legacy_json_field_conflicts=False + ): + """Initializes a Pool of proto buffs. + + The descriptor_db argument to the constructor is provided to allow + specialized file descriptor proto lookup code to be triggered on demand. An + example would be an implementation which will read and compile a file + specified in a call to FindFileByName() and not require the call to Add() + at all. Results from this database will be cached internally here as well. + + Args: + descriptor_db: A secondary source of file descriptors. + use_deprecated_legacy_json_field_conflicts: Unused, for compatibility with + C++. + """ + + self._internal_db = descriptor_database.DescriptorDatabase() + self._descriptor_db = descriptor_db + self._descriptors = {} + self._enum_descriptors = {} + self._service_descriptors = {} + self._file_descriptors = {} + self._toplevel_extensions = {} + self._top_enum_values = {} + # We store extensions in two two-level mappings: The first key is the + # descriptor of the message being extended, the second key is the extension + # full name or its tag number. + self._extensions_by_name = collections.defaultdict(dict) + self._extensions_by_number = collections.defaultdict(dict) + self._serialized_edition_defaults = ( + python_edition_defaults._PROTOBUF_INTERNAL_PYTHON_EDITION_DEFAULTS + ) + self._edition_defaults = None + self._feature_cache = dict() + + def _CheckConflictRegister(self, desc, desc_name, file_name): + """Check if the descriptor name conflicts with another of the same name. + + Args: + desc: Descriptor of a message, enum, service, extension or enum value. + desc_name (str): the full name of desc. + file_name (str): The file name of descriptor. + """ + for register, descriptor_type in [ + (self._descriptors, descriptor.Descriptor), + (self._enum_descriptors, descriptor.EnumDescriptor), + (self._service_descriptors, descriptor.ServiceDescriptor), + (self._toplevel_extensions, descriptor.FieldDescriptor), + (self._top_enum_values, descriptor.EnumValueDescriptor)]: + if desc_name in register: + old_desc = register[desc_name] + if isinstance(old_desc, descriptor.EnumValueDescriptor): + old_file = old_desc.type.file.name + else: + old_file = old_desc.file.name + + if not isinstance(desc, descriptor_type) or ( + old_file != file_name): + error_msg = ('Conflict register for file "' + file_name + + '": ' + desc_name + + ' is already defined in file "' + + old_file + '". Please fix the conflict by adding ' + 'package name on the proto file, or use different ' + 'name for the duplication.') + if isinstance(desc, descriptor.EnumValueDescriptor): + error_msg += ('\nNote: enum values appear as ' + 'siblings of the enum type instead of ' + 'children of it.') + + raise TypeError(error_msg) + + return + + def Add(self, file_desc_proto): + """Adds the FileDescriptorProto and its types to this pool. + + Args: + file_desc_proto (FileDescriptorProto): The file descriptor to add. + """ + + self._internal_db.Add(file_desc_proto) + + def AddSerializedFile(self, serialized_file_desc_proto): + """Adds the FileDescriptorProto and its types to this pool. + + Args: + serialized_file_desc_proto (bytes): A bytes string, serialization of the + :class:`FileDescriptorProto` to add. + + Returns: + FileDescriptor: Descriptor for the added file. + """ + + # pylint: disable=g-import-not-at-top + from google.protobuf import descriptor_pb2 + file_desc_proto = descriptor_pb2.FileDescriptorProto.FromString( + serialized_file_desc_proto) + file_desc = self._ConvertFileProtoToFileDescriptor(file_desc_proto) + file_desc.serialized_pb = serialized_file_desc_proto + return file_desc + + # Never call this method. It is for internal usage only. + def _AddDescriptor(self, desc): + """Adds a Descriptor to the pool, non-recursively. + + If the Descriptor contains nested messages or enums, the caller must + explicitly register them. This method also registers the FileDescriptor + associated with the message. + + Args: + desc: A Descriptor. + """ + if not isinstance(desc, descriptor.Descriptor): + raise TypeError('Expected instance of descriptor.Descriptor.') + + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + + self._descriptors[desc.full_name] = desc + self._AddFileDescriptor(desc.file) + + # Never call this method. It is for internal usage only. + def _AddEnumDescriptor(self, enum_desc): + """Adds an EnumDescriptor to the pool. + + This method also registers the FileDescriptor associated with the enum. + + Args: + enum_desc: An EnumDescriptor. + """ + + if not isinstance(enum_desc, descriptor.EnumDescriptor): + raise TypeError('Expected instance of descriptor.EnumDescriptor.') + + file_name = enum_desc.file.name + self._CheckConflictRegister(enum_desc, enum_desc.full_name, file_name) + self._enum_descriptors[enum_desc.full_name] = enum_desc + + # Top enum values need to be indexed. + # Count the number of dots to see whether the enum is toplevel or nested + # in a message. We cannot use enum_desc.containing_type at this stage. + if enum_desc.file.package: + top_level = (enum_desc.full_name.count('.') + - enum_desc.file.package.count('.') == 1) + else: + top_level = enum_desc.full_name.count('.') == 0 + if top_level: + file_name = enum_desc.file.name + package = enum_desc.file.package + for enum_value in enum_desc.values: + full_name = _NormalizeFullyQualifiedName( + '.'.join((package, enum_value.name))) + self._CheckConflictRegister(enum_value, full_name, file_name) + self._top_enum_values[full_name] = enum_value + self._AddFileDescriptor(enum_desc.file) + + # Never call this method. It is for internal usage only. + def _AddServiceDescriptor(self, service_desc): + """Adds a ServiceDescriptor to the pool. + + Args: + service_desc: A ServiceDescriptor. + """ + + if not isinstance(service_desc, descriptor.ServiceDescriptor): + raise TypeError('Expected instance of descriptor.ServiceDescriptor.') + + self._CheckConflictRegister(service_desc, service_desc.full_name, + service_desc.file.name) + self._service_descriptors[service_desc.full_name] = service_desc + + # Never call this method. It is for internal usage only. + def _AddExtensionDescriptor(self, extension): + """Adds a FieldDescriptor describing an extension to the pool. + + Args: + extension: A FieldDescriptor. + + Raises: + AssertionError: when another extension with the same number extends the + same message. + TypeError: when the specified extension is not a + descriptor.FieldDescriptor. + """ + if not (isinstance(extension, descriptor.FieldDescriptor) and + extension.is_extension): + raise TypeError('Expected an extension descriptor.') + + if extension.extension_scope is None: + self._CheckConflictRegister( + extension, extension.full_name, extension.file.name) + self._toplevel_extensions[extension.full_name] = extension + + try: + existing_desc = self._extensions_by_number[ + extension.containing_type][extension.number] + except KeyError: + pass + else: + if extension is not existing_desc: + raise AssertionError( + 'Extensions "%s" and "%s" both try to extend message type "%s" ' + 'with field number %d.' % + (extension.full_name, existing_desc.full_name, + extension.containing_type.full_name, extension.number)) + + self._extensions_by_number[extension.containing_type][ + extension.number] = extension + self._extensions_by_name[extension.containing_type][ + extension.full_name] = extension + + # Also register MessageSet extensions with the type name. + if _IsMessageSetExtension(extension): + self._extensions_by_name[extension.containing_type][ + extension.message_type.full_name] = extension + + if hasattr(extension.containing_type, '_concrete_class'): + python_message._AttachFieldHelpers( + extension.containing_type._concrete_class, extension) + + # Never call this method. It is for internal usage only. + def _InternalAddFileDescriptor(self, file_desc): + """Adds a FileDescriptor to the pool, non-recursively. + + If the FileDescriptor contains messages or enums, the caller must explicitly + register them. + + Args: + file_desc: A FileDescriptor. + """ + + self._AddFileDescriptor(file_desc) + + def _AddFileDescriptor(self, file_desc): + """Adds a FileDescriptor to the pool, non-recursively. + + If the FileDescriptor contains messages or enums, the caller must explicitly + register them. + + Args: + file_desc: A FileDescriptor. + """ + + if not isinstance(file_desc, descriptor.FileDescriptor): + raise TypeError('Expected instance of descriptor.FileDescriptor.') + self._file_descriptors[file_desc.name] = file_desc + + def FindFileByName(self, file_name): + """Gets a FileDescriptor by file name. + + Args: + file_name (str): The path to the file to get a descriptor for. + + Returns: + FileDescriptor: The descriptor for the named file. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + + try: + return self._file_descriptors[file_name] + except KeyError: + pass + + try: + file_proto = self._internal_db.FindFileByName(file_name) + except KeyError as error: + if self._descriptor_db: + file_proto = self._descriptor_db.FindFileByName(file_name) + else: + raise error + if not file_proto: + raise KeyError('Cannot find a file named %s' % file_name) + return self._ConvertFileProtoToFileDescriptor(file_proto) + + def FindFileContainingSymbol(self, symbol): + """Gets the FileDescriptor for the file containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: Descriptor for the file that contains the specified + symbol. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + + symbol = _NormalizeFullyQualifiedName(symbol) + try: + return self._InternalFindFileContainingSymbol(symbol) + except KeyError: + pass + + try: + # Try fallback database. Build and find again if possible. + self._FindFileContainingSymbolInDb(symbol) + return self._InternalFindFileContainingSymbol(symbol) + except KeyError: + raise KeyError('Cannot find a file containing %s' % symbol) + + def _InternalFindFileContainingSymbol(self, symbol): + """Gets the already built FileDescriptor containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: Descriptor for the file that contains the specified + symbol. + + Raises: + KeyError: if the file cannot be found in the pool. + """ + try: + return self._descriptors[symbol].file + except KeyError: + pass + + try: + return self._enum_descriptors[symbol].file + except KeyError: + pass + + try: + return self._service_descriptors[symbol].file + except KeyError: + pass + + try: + return self._top_enum_values[symbol].type.file + except KeyError: + pass + + try: + return self._toplevel_extensions[symbol].file + except KeyError: + pass + + # Try fields, enum values and nested extensions inside a message. + top_name, _, sub_name = symbol.rpartition('.') + try: + message = self.FindMessageTypeByName(top_name) + assert (sub_name in message.extensions_by_name or + sub_name in message.fields_by_name or + sub_name in message.enum_values_by_name) + return message.file + except (KeyError, AssertionError): + raise KeyError('Cannot find a file containing %s' % symbol) + + def FindMessageTypeByName(self, full_name): + """Loads the named descriptor from the pool. + + Args: + full_name (str): The full name of the descriptor to load. + + Returns: + Descriptor: The descriptor for the named type. + + Raises: + KeyError: if the message cannot be found in the pool. + """ + + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._descriptors[full_name] + + def FindEnumTypeByName(self, full_name): + """Loads the named enum descriptor from the pool. + + Args: + full_name (str): The full name of the enum descriptor to load. + + Returns: + EnumDescriptor: The enum descriptor for the named type. + + Raises: + KeyError: if the enum cannot be found in the pool. + """ + + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._enum_descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._enum_descriptors[full_name] + + def FindFieldByName(self, full_name): + """Loads the named field descriptor from the pool. + + Args: + full_name (str): The full name of the field descriptor to load. + + Returns: + FieldDescriptor: The field descriptor for the named field. + + Raises: + KeyError: if the field cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + message_name, _, field_name = full_name.rpartition('.') + message_descriptor = self.FindMessageTypeByName(message_name) + return message_descriptor.fields_by_name[field_name] + + def FindOneofByName(self, full_name): + """Loads the named oneof descriptor from the pool. + + Args: + full_name (str): The full name of the oneof descriptor to load. + + Returns: + OneofDescriptor: The oneof descriptor for the named oneof. + + Raises: + KeyError: if the oneof cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + message_name, _, oneof_name = full_name.rpartition('.') + message_descriptor = self.FindMessageTypeByName(message_name) + return message_descriptor.oneofs_by_name[oneof_name] + + def FindExtensionByName(self, full_name): + """Loads the named extension descriptor from the pool. + + Args: + full_name (str): The full name of the extension descriptor to load. + + Returns: + FieldDescriptor: The field descriptor for the named extension. + + Raises: + KeyError: if the extension cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + try: + # The proto compiler does not give any link between the FileDescriptor + # and top-level extensions unless the FileDescriptorProto is added to + # the DescriptorDatabase, but this can impact memory usage. + # So we registered these extensions by name explicitly. + return self._toplevel_extensions[full_name] + except KeyError: + pass + message_name, _, extension_name = full_name.rpartition('.') + try: + # Most extensions are nested inside a message. + scope = self.FindMessageTypeByName(message_name) + except KeyError: + # Some extensions are defined at file scope. + scope = self._FindFileContainingSymbolInDb(full_name) + return scope.extensions_by_name[extension_name] + + def FindExtensionByNumber(self, message_descriptor, number): + """Gets the extension of the specified message with the specified number. + + Extensions have to be registered to this pool by calling :func:`Add` or + :func:`AddExtensionDescriptor`. + + Args: + message_descriptor (Descriptor): descriptor of the extended message. + number (int): Number of the extension field. + + Returns: + FieldDescriptor: The descriptor for the extension. + + Raises: + KeyError: when no extension with the given number is known for the + specified message. + """ + try: + return self._extensions_by_number[message_descriptor][number] + except KeyError: + self._TryLoadExtensionFromDB(message_descriptor, number) + return self._extensions_by_number[message_descriptor][number] + + def FindAllExtensions(self, message_descriptor): + """Gets all the known extensions of a given message. + + Extensions have to be registered to this pool by build related + :func:`Add` or :func:`AddExtensionDescriptor`. + + Args: + message_descriptor (Descriptor): Descriptor of the extended message. + + Returns: + list[FieldDescriptor]: Field descriptors describing the extensions. + """ + # Fallback to descriptor db if FindAllExtensionNumbers is provided. + if self._descriptor_db and hasattr( + self._descriptor_db, 'FindAllExtensionNumbers'): + full_name = message_descriptor.full_name + all_numbers = self._descriptor_db.FindAllExtensionNumbers(full_name) + for number in all_numbers: + if number in self._extensions_by_number[message_descriptor]: + continue + self._TryLoadExtensionFromDB(message_descriptor, number) + + return list(self._extensions_by_number[message_descriptor].values()) + + def _TryLoadExtensionFromDB(self, message_descriptor, number): + """Try to Load extensions from descriptor db. + + Args: + message_descriptor: descriptor of the extended message. + number: the extension number that needs to be loaded. + """ + if not self._descriptor_db: + return + # Only supported when FindFileContainingExtension is provided. + if not hasattr( + self._descriptor_db, 'FindFileContainingExtension'): + return + + full_name = message_descriptor.full_name + file_proto = self._descriptor_db.FindFileContainingExtension( + full_name, number) + + if file_proto is None: + return + + try: + self._ConvertFileProtoToFileDescriptor(file_proto) + except: + warn_msg = ('Unable to load proto file %s for extension number %d.' % + (file_proto.name, number)) + warnings.warn(warn_msg, RuntimeWarning) + + def FindServiceByName(self, full_name): + """Loads the named service descriptor from the pool. + + Args: + full_name (str): The full name of the service descriptor to load. + + Returns: + ServiceDescriptor: The service descriptor for the named service. + + Raises: + KeyError: if the service cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + if full_name not in self._service_descriptors: + self._FindFileContainingSymbolInDb(full_name) + return self._service_descriptors[full_name] + + def FindMethodByName(self, full_name): + """Loads the named service method descriptor from the pool. + + Args: + full_name (str): The full name of the method descriptor to load. + + Returns: + MethodDescriptor: The method descriptor for the service method. + + Raises: + KeyError: if the method cannot be found in the pool. + """ + full_name = _NormalizeFullyQualifiedName(full_name) + service_name, _, method_name = full_name.rpartition('.') + service_descriptor = self.FindServiceByName(service_name) + return service_descriptor.methods_by_name[method_name] + + def SetFeatureSetDefaults(self, defaults): + """Sets the default feature mappings used during the build. + + Args: + defaults: a FeatureSetDefaults message containing the new mappings. + """ + if self._edition_defaults is not None: + raise ValueError( + "Feature set defaults can't be changed once the pool has started" + ' building!' + ) + + # pylint: disable=g-import-not-at-top + from google.protobuf import descriptor_pb2 + + if not isinstance(defaults, descriptor_pb2.FeatureSetDefaults): + raise TypeError('SetFeatureSetDefaults called with invalid type') + + + if defaults.minimum_edition > defaults.maximum_edition: + raise ValueError( + 'Invalid edition range %s to %s' + % ( + descriptor_pb2.Edition.Name(defaults.minimum_edition), + descriptor_pb2.Edition.Name(defaults.maximum_edition), + ) + ) + + prev_edition = descriptor_pb2.Edition.EDITION_UNKNOWN + for d in defaults.defaults: + if d.edition == descriptor_pb2.Edition.EDITION_UNKNOWN: + raise ValueError('Invalid edition EDITION_UNKNOWN specified') + if prev_edition >= d.edition: + raise ValueError( + 'Feature set defaults are not strictly increasing. %s is greater' + ' than or equal to %s' + % ( + descriptor_pb2.Edition.Name(prev_edition), + descriptor_pb2.Edition.Name(d.edition), + ) + ) + prev_edition = d.edition + self._edition_defaults = defaults + + def _CreateDefaultFeatures(self, edition): + """Creates a FeatureSet message with defaults for a specific edition. + + Args: + edition: the edition to generate defaults for. + + Returns: + A FeatureSet message with defaults for a specific edition. + """ + # pylint: disable=g-import-not-at-top + from google.protobuf import descriptor_pb2 + + with _edition_defaults_lock: + if not self._edition_defaults: + self._edition_defaults = descriptor_pb2.FeatureSetDefaults() + self._edition_defaults.ParseFromString( + self._serialized_edition_defaults + ) + + if edition < self._edition_defaults.minimum_edition: + raise TypeError( + 'Edition %s is earlier than the minimum supported edition %s!' + % ( + descriptor_pb2.Edition.Name(edition), + descriptor_pb2.Edition.Name( + self._edition_defaults.minimum_edition + ), + ) + ) + if edition > self._edition_defaults.maximum_edition: + raise TypeError( + 'Edition %s is later than the maximum supported edition %s!' + % ( + descriptor_pb2.Edition.Name(edition), + descriptor_pb2.Edition.Name( + self._edition_defaults.maximum_edition + ), + ) + ) + found = None + for d in self._edition_defaults.defaults: + if d.edition > edition: + break + found = d + if found is None: + raise TypeError( + 'No valid default found for edition %s!' + % descriptor_pb2.Edition.Name(edition) + ) + + defaults = descriptor_pb2.FeatureSet() + defaults.CopyFrom(found.fixed_features) + defaults.MergeFrom(found.overridable_features) + return defaults + + def _InternFeatures(self, features): + serialized = features.SerializeToString() + with _edition_defaults_lock: + cached = self._feature_cache.get(serialized) + if cached is None: + self._feature_cache[serialized] = features + cached = features + return cached + + def _FindFileContainingSymbolInDb(self, symbol): + """Finds the file in descriptor DB containing the specified symbol. + + Args: + symbol (str): The name of the symbol to search for. + + Returns: + FileDescriptor: The file that contains the specified symbol. + + Raises: + KeyError: if the file cannot be found in the descriptor database. + """ + try: + file_proto = self._internal_db.FindFileContainingSymbol(symbol) + except KeyError as error: + if self._descriptor_db: + file_proto = self._descriptor_db.FindFileContainingSymbol(symbol) + else: + raise error + if not file_proto: + raise KeyError('Cannot find a file containing %s' % symbol) + return self._ConvertFileProtoToFileDescriptor(file_proto) + + def _ConvertFileProtoToFileDescriptor(self, file_proto): + """Creates a FileDescriptor from a proto or returns a cached copy. + + This method also has the side effect of loading all the symbols found in + the file into the appropriate dictionaries in the pool. + + Args: + file_proto: The proto to convert. + + Returns: + A FileDescriptor matching the passed in proto. + """ + if file_proto.name not in self._file_descriptors: + built_deps = list(self._GetDeps(file_proto.dependency)) + direct_deps = [self.FindFileByName(n) for n in file_proto.dependency] + public_deps = [direct_deps[i] for i in file_proto.public_dependency] + + # pylint: disable=g-import-not-at-top + from google.protobuf import descriptor_pb2 + + file_descriptor = descriptor.FileDescriptor( + pool=self, + name=file_proto.name, + package=file_proto.package, + syntax=file_proto.syntax, + edition=descriptor_pb2.Edition.Name(file_proto.edition), + options=_OptionsOrNone(file_proto), + serialized_pb=file_proto.SerializeToString(), + dependencies=direct_deps, + public_dependencies=public_deps, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key, + ) + scope = {} + + # This loop extracts all the message and enum types from all the + # dependencies of the file_proto. This is necessary to create the + # scope of available message types when defining the passed in + # file proto. + for dependency in built_deps: + scope.update(self._ExtractSymbols( + dependency.message_types_by_name.values())) + scope.update((_PrefixWithDot(enum.full_name), enum) + for enum in dependency.enum_types_by_name.values()) + + for message_type in file_proto.message_type: + message_desc = self._ConvertMessageDescriptor( + message_type, file_proto.package, file_descriptor, scope, + file_proto.syntax) + file_descriptor.message_types_by_name[message_desc.name] = ( + message_desc) + + for enum_type in file_proto.enum_type: + file_descriptor.enum_types_by_name[enum_type.name] = ( + self._ConvertEnumDescriptor(enum_type, file_proto.package, + file_descriptor, None, scope, True)) + + for index, extension_proto in enumerate(file_proto.extension): + extension_desc = self._MakeFieldDescriptor( + extension_proto, file_proto.package, index, file_descriptor, + is_extension=True) + extension_desc.containing_type = self._GetTypeFromScope( + file_descriptor.package, extension_proto.extendee, scope) + self._SetFieldType(extension_proto, extension_desc, + file_descriptor.package, scope) + file_descriptor.extensions_by_name[extension_desc.name] = ( + extension_desc) + + for desc_proto in file_proto.message_type: + self._SetAllFieldTypes(file_proto.package, desc_proto, scope) + + if file_proto.package: + desc_proto_prefix = _PrefixWithDot(file_proto.package) + else: + desc_proto_prefix = '' + + for desc_proto in file_proto.message_type: + desc = self._GetTypeFromScope( + desc_proto_prefix, desc_proto.name, scope) + file_descriptor.message_types_by_name[desc_proto.name] = desc + + for index, service_proto in enumerate(file_proto.service): + file_descriptor.services_by_name[service_proto.name] = ( + self._MakeServiceDescriptor(service_proto, index, scope, + file_proto.package, file_descriptor)) + + self._file_descriptors[file_proto.name] = file_descriptor + + # Add extensions to the pool + def AddExtensionForNested(message_type): + for nested in message_type.nested_types: + AddExtensionForNested(nested) + for extension in message_type.extensions: + self._AddExtensionDescriptor(extension) + + file_desc = self._file_descriptors[file_proto.name] + for extension in file_desc.extensions_by_name.values(): + self._AddExtensionDescriptor(extension) + for message_type in file_desc.message_types_by_name.values(): + AddExtensionForNested(message_type) + + return file_desc + + def _ConvertMessageDescriptor(self, desc_proto, package=None, file_desc=None, + scope=None, syntax=None): + """Adds the proto to the pool in the specified package. + + Args: + desc_proto: The descriptor_pb2.DescriptorProto protobuf message. + package: The package the proto should be located in. + file_desc: The file containing this message. + scope: Dict mapping short and full symbols to message and enum types. + syntax: string indicating syntax of the file ("proto2" or "proto3") + + Returns: + The added descriptor. + """ + + if package: + desc_name = '.'.join((package, desc_proto.name)) + else: + desc_name = desc_proto.name + + if file_desc is None: + file_name = None + else: + file_name = file_desc.name + + if scope is None: + scope = {} + + nested = [ + self._ConvertMessageDescriptor( + nested, desc_name, file_desc, scope, syntax) + for nested in desc_proto.nested_type] + enums = [ + self._ConvertEnumDescriptor(enum, desc_name, file_desc, None, + scope, False) + for enum in desc_proto.enum_type] + fields = [self._MakeFieldDescriptor(field, desc_name, index, file_desc) + for index, field in enumerate(desc_proto.field)] + extensions = [ + self._MakeFieldDescriptor(extension, desc_name, index, file_desc, + is_extension=True) + for index, extension in enumerate(desc_proto.extension)] + oneofs = [ + # pylint: disable=g-complex-comprehension + descriptor.OneofDescriptor( + desc.name, + '.'.join((desc_name, desc.name)), + index, + None, + [], + _OptionsOrNone(desc), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + for index, desc in enumerate(desc_proto.oneof_decl) + ] + extension_ranges = [(r.start, r.end) for r in desc_proto.extension_range] + if extension_ranges: + is_extendable = True + else: + is_extendable = False + desc = descriptor.Descriptor( + name=desc_proto.name, + full_name=desc_name, + filename=file_name, + containing_type=None, + fields=fields, + oneofs=oneofs, + nested_types=nested, + enum_types=enums, + extensions=extensions, + options=_OptionsOrNone(desc_proto), + is_extendable=is_extendable, + extension_ranges=extension_ranges, + file=file_desc, + serialized_start=None, + serialized_end=None, + is_map_entry=desc_proto.options.map_entry, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key, + ) + for nested in desc.nested_types: + nested.containing_type = desc + for enum in desc.enum_types: + enum.containing_type = desc + for field_index, field_desc in enumerate(desc_proto.field): + if field_desc.HasField('oneof_index'): + oneof_index = field_desc.oneof_index + oneofs[oneof_index].fields.append(fields[field_index]) + fields[field_index].containing_oneof = oneofs[oneof_index] + + scope[_PrefixWithDot(desc_name)] = desc + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._descriptors[desc_name] = desc + return desc + + def _ConvertEnumDescriptor(self, enum_proto, package=None, file_desc=None, + containing_type=None, scope=None, top_level=False): + """Make a protobuf EnumDescriptor given an EnumDescriptorProto protobuf. + + Args: + enum_proto: The descriptor_pb2.EnumDescriptorProto protobuf message. + package: Optional package name for the new message EnumDescriptor. + file_desc: The file containing the enum descriptor. + containing_type: The type containing this enum. + scope: Scope containing available types. + top_level: If True, the enum is a top level symbol. If False, the enum + is defined inside a message. + + Returns: + The added descriptor + """ + + if package: + enum_name = '.'.join((package, enum_proto.name)) + else: + enum_name = enum_proto.name + + if file_desc is None: + file_name = None + else: + file_name = file_desc.name + + values = [self._MakeEnumValueDescriptor(value, index) + for index, value in enumerate(enum_proto.value)] + desc = descriptor.EnumDescriptor(name=enum_proto.name, + full_name=enum_name, + filename=file_name, + file=file_desc, + values=values, + containing_type=containing_type, + options=_OptionsOrNone(enum_proto), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + scope['.%s' % enum_name] = desc + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._enum_descriptors[enum_name] = desc + + # Add top level enum values. + if top_level: + for value in values: + full_name = _NormalizeFullyQualifiedName( + '.'.join((package, value.name))) + self._CheckConflictRegister(value, full_name, file_name) + self._top_enum_values[full_name] = value + + return desc + + def _MakeFieldDescriptor(self, field_proto, message_name, index, + file_desc, is_extension=False): + """Creates a field descriptor from a FieldDescriptorProto. + + For message and enum type fields, this method will do a look up + in the pool for the appropriate descriptor for that type. If it + is unavailable, it will fall back to the _source function to + create it. If this type is still unavailable, construction will + fail. + + Args: + field_proto: The proto describing the field. + message_name: The name of the containing message. + index: Index of the field + file_desc: The file containing the field descriptor. + is_extension: Indication that this field is for an extension. + + Returns: + An initialized FieldDescriptor object + """ + + if message_name: + full_name = '.'.join((message_name, field_proto.name)) + else: + full_name = field_proto.name + + if field_proto.json_name: + json_name = field_proto.json_name + else: + json_name = None + + return descriptor.FieldDescriptor( + name=field_proto.name, + full_name=full_name, + index=index, + number=field_proto.number, + type=field_proto.type, + cpp_type=None, + message_type=None, + enum_type=None, + containing_type=None, + label=field_proto.label, + has_default_value=False, + default_value=None, + is_extension=is_extension, + extension_scope=None, + options=_OptionsOrNone(field_proto), + json_name=json_name, + file=file_desc, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _SetAllFieldTypes(self, package, desc_proto, scope): + """Sets all the descriptor's fields's types. + + This method also sets the containing types on any extensions. + + Args: + package: The current package of desc_proto. + desc_proto: The message descriptor to update. + scope: Enclosing scope of available types. + """ + + package = _PrefixWithDot(package) + + main_desc = self._GetTypeFromScope(package, desc_proto.name, scope) + + if package == '.': + nested_package = _PrefixWithDot(desc_proto.name) + else: + nested_package = '.'.join([package, desc_proto.name]) + + for field_proto, field_desc in zip(desc_proto.field, main_desc.fields): + self._SetFieldType(field_proto, field_desc, nested_package, scope) + + for extension_proto, extension_desc in ( + zip(desc_proto.extension, main_desc.extensions)): + extension_desc.containing_type = self._GetTypeFromScope( + nested_package, extension_proto.extendee, scope) + self._SetFieldType(extension_proto, extension_desc, nested_package, scope) + + for nested_type in desc_proto.nested_type: + self._SetAllFieldTypes(nested_package, nested_type, scope) + + def _SetFieldType(self, field_proto, field_desc, package, scope): + """Sets the field's type, cpp_type, message_type and enum_type. + + Args: + field_proto: Data about the field in proto format. + field_desc: The descriptor to modify. + package: The package the field's container is in. + scope: Enclosing scope of available types. + """ + if field_proto.type_name: + desc = self._GetTypeFromScope(package, field_proto.type_name, scope) + else: + desc = None + + if not field_proto.HasField('type'): + if isinstance(desc, descriptor.Descriptor): + field_proto.type = descriptor.FieldDescriptor.TYPE_MESSAGE + else: + field_proto.type = descriptor.FieldDescriptor.TYPE_ENUM + + field_desc.cpp_type = descriptor.FieldDescriptor.ProtoTypeToCppProtoType( + field_proto.type) + + if (field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE + or field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP): + field_desc.message_type = desc + + if field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.enum_type = desc + + if field_proto.label == descriptor.FieldDescriptor.LABEL_REPEATED: + field_desc.has_default_value = False + field_desc.default_value = [] + elif field_proto.HasField('default_value'): + field_desc.has_default_value = True + if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or + field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): + field_desc.default_value = float(field_proto.default_value) + elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: + field_desc.default_value = field_proto.default_value + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: + field_desc.default_value = field_proto.default_value.lower() == 'true' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.default_value = field_desc.enum_type.values_by_name[ + field_proto.default_value].number + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: + field_desc.default_value = text_encoding.CUnescape( + field_proto.default_value) + elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: + field_desc.default_value = None + else: + # All other types are of the "int" type. + field_desc.default_value = int(field_proto.default_value) + else: + field_desc.has_default_value = False + if (field_proto.type == descriptor.FieldDescriptor.TYPE_DOUBLE or + field_proto.type == descriptor.FieldDescriptor.TYPE_FLOAT): + field_desc.default_value = 0.0 + elif field_proto.type == descriptor.FieldDescriptor.TYPE_STRING: + field_desc.default_value = u'' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BOOL: + field_desc.default_value = False + elif field_proto.type == descriptor.FieldDescriptor.TYPE_ENUM: + field_desc.default_value = field_desc.enum_type.values[0].number + elif field_proto.type == descriptor.FieldDescriptor.TYPE_BYTES: + field_desc.default_value = b'' + elif field_proto.type == descriptor.FieldDescriptor.TYPE_MESSAGE: + field_desc.default_value = None + elif field_proto.type == descriptor.FieldDescriptor.TYPE_GROUP: + field_desc.default_value = None + else: + # All other types are of the "int" type. + field_desc.default_value = 0 + + field_desc.type = field_proto.type + + def _MakeEnumValueDescriptor(self, value_proto, index): + """Creates a enum value descriptor object from a enum value proto. + + Args: + value_proto: The proto describing the enum value. + index: The index of the enum value. + + Returns: + An initialized EnumValueDescriptor object. + """ + + return descriptor.EnumValueDescriptor( + name=value_proto.name, + index=index, + number=value_proto.number, + options=_OptionsOrNone(value_proto), + type=None, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _MakeServiceDescriptor(self, service_proto, service_index, scope, + package, file_desc): + """Make a protobuf ServiceDescriptor given a ServiceDescriptorProto. + + Args: + service_proto: The descriptor_pb2.ServiceDescriptorProto protobuf message. + service_index: The index of the service in the File. + scope: Dict mapping short and full symbols to message and enum types. + package: Optional package name for the new message EnumDescriptor. + file_desc: The file containing the service descriptor. + + Returns: + The added descriptor. + """ + + if package: + service_name = '.'.join((package, service_proto.name)) + else: + service_name = service_proto.name + + methods = [self._MakeMethodDescriptor(method_proto, service_name, package, + scope, index) + for index, method_proto in enumerate(service_proto.method)] + desc = descriptor.ServiceDescriptor( + name=service_proto.name, + full_name=service_name, + index=service_index, + methods=methods, + options=_OptionsOrNone(service_proto), + file=file_desc, + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + self._CheckConflictRegister(desc, desc.full_name, desc.file.name) + self._service_descriptors[service_name] = desc + return desc + + def _MakeMethodDescriptor(self, method_proto, service_name, package, scope, + index): + """Creates a method descriptor from a MethodDescriptorProto. + + Args: + method_proto: The proto describing the method. + service_name: The name of the containing service. + package: Optional package name to look up for types. + scope: Scope containing available types. + index: Index of the method in the service. + + Returns: + An initialized MethodDescriptor object. + """ + full_name = '.'.join((service_name, method_proto.name)) + input_type = self._GetTypeFromScope( + package, method_proto.input_type, scope) + output_type = self._GetTypeFromScope( + package, method_proto.output_type, scope) + return descriptor.MethodDescriptor( + name=method_proto.name, + full_name=full_name, + index=index, + containing_service=None, + input_type=input_type, + output_type=output_type, + client_streaming=method_proto.client_streaming, + server_streaming=method_proto.server_streaming, + options=_OptionsOrNone(method_proto), + # pylint: disable=protected-access + create_key=descriptor._internal_create_key) + + def _ExtractSymbols(self, descriptors): + """Pulls out all the symbols from descriptor protos. + + Args: + descriptors: The messages to extract descriptors from. + Yields: + A two element tuple of the type name and descriptor object. + """ + + for desc in descriptors: + yield (_PrefixWithDot(desc.full_name), desc) + for symbol in self._ExtractSymbols(desc.nested_types): + yield symbol + for enum in desc.enum_types: + yield (_PrefixWithDot(enum.full_name), enum) + + def _GetDeps(self, dependencies, visited=None): + """Recursively finds dependencies for file protos. + + Args: + dependencies: The names of the files being depended on. + visited: The names of files already found. + + Yields: + Each direct and indirect dependency. + """ + + visited = visited or set() + for dependency in dependencies: + if dependency not in visited: + visited.add(dependency) + dep_desc = self.FindFileByName(dependency) + yield dep_desc + public_files = [d.name for d in dep_desc.public_dependencies] + yield from self._GetDeps(public_files, visited) + + def _GetTypeFromScope(self, package, type_name, scope): + """Finds a given type name in the current scope. + + Args: + package: The package the proto should be located in. + type_name: The name of the type to be found in the scope. + scope: Dict mapping short and full symbols to message and enum types. + + Returns: + The descriptor for the requested type. + """ + if type_name not in scope: + components = _PrefixWithDot(package).split('.') + while components: + possible_match = '.'.join(components + [type_name]) + if possible_match in scope: + type_name = possible_match + break + else: + components.pop(-1) + return scope[type_name] + + +def _PrefixWithDot(name): + return name if name.startswith('.') else '.%s' % name + + +if _USE_C_DESCRIPTORS: + # TODO: This pool could be constructed from Python code, when we + # support a flag like 'use_cpp_generated_pool=True'. + # pylint: disable=protected-access + _DEFAULT = descriptor._message.default_pool +else: + _DEFAULT = DescriptorPool() + + +def Default(): + return _DEFAULT diff --git a/google/protobuf/duration_pb2.py b/google/protobuf/duration_pb2.py new file mode 100644 index 0000000..f012f1c --- /dev/null +++ b/google/protobuf/duration_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/duration.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/duration.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/duration.proto\x12\x0fgoogle.protobuf\":\n\x08\x44uration\x12\x18\n\x07seconds\x18\x01 \x01(\x03R\x07seconds\x12\x14\n\x05nanos\x18\x02 \x01(\x05R\x05nanosB\x83\x01\n\x13\x63om.google.protobufB\rDurationProtoP\x01Z1google.golang.org/protobuf/types/known/durationpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.duration_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\rDurationProtoP\001Z1google.golang.org/protobuf/types/known/durationpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _globals['_DURATION']._serialized_start=51 + _globals['_DURATION']._serialized_end=109 +# @@protoc_insertion_point(module_scope) diff --git a/google/protobuf/empty_pb2.py b/google/protobuf/empty_pb2.py new file mode 100644 index 0000000..1035fe4 --- /dev/null +++ b/google/protobuf/empty_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/empty.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/empty.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bgoogle/protobuf/empty.proto\x12\x0fgoogle.protobuf\"\x07\n\x05\x45mptyB}\n\x13\x63om.google.protobufB\nEmptyProtoP\x01Z.google.golang.org/protobuf/types/known/emptypb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.empty_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\nEmptyProtoP\001Z.google.golang.org/protobuf/types/known/emptypb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _globals['_EMPTY']._serialized_start=48 + _globals['_EMPTY']._serialized_end=55 +# @@protoc_insertion_point(module_scope) diff --git a/google/protobuf/field_mask_pb2.py b/google/protobuf/field_mask_pb2.py new file mode 100644 index 0000000..d8bc54d --- /dev/null +++ b/google/protobuf/field_mask_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/field_mask.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/field_mask.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n google/protobuf/field_mask.proto\x12\x0fgoogle.protobuf\"!\n\tFieldMask\x12\x14\n\x05paths\x18\x01 \x03(\tR\x05pathsB\x85\x01\n\x13\x63om.google.protobufB\x0e\x46ieldMaskProtoP\x01Z2google.golang.org/protobuf/types/known/fieldmaskpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.field_mask_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\016FieldMaskProtoP\001Z2google.golang.org/protobuf/types/known/fieldmaskpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _globals['_FIELDMASK']._serialized_start=53 + _globals['_FIELDMASK']._serialized_end=86 +# @@protoc_insertion_point(module_scope) diff --git a/google/protobuf/internal/__init__.py b/google/protobuf/internal/__init__.py new file mode 100644 index 0000000..e676e28 --- /dev/null +++ b/google/protobuf/internal/__init__.py @@ -0,0 +1,7 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + diff --git a/google/protobuf/internal/_parameterized.py b/google/protobuf/internal/_parameterized.py new file mode 100644 index 0000000..4cb2cb1 --- /dev/null +++ b/google/protobuf/internal/_parameterized.py @@ -0,0 +1,420 @@ +#! /usr/bin/env python +# +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Adds support for parameterized tests to Python's unittest TestCase class. + +A parameterized test is a method in a test case that is invoked with different +argument tuples. + +A simple example: + + class AdditionExample(_parameterized.TestCase): + @_parameterized.parameters( + (1, 2, 3), + (4, 5, 9), + (1, 1, 3)) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + + +Each invocation is a separate test case and properly isolated just +like a normal test method, with its own setUp/tearDown cycle. In the +example above, there are three separate testcases, one of which will +fail due to an assertion error (1 + 1 != 3). + +Parameters for individual test cases can be tuples (with positional parameters) +or dictionaries (with named parameters): + + class AdditionExample(_parameterized.TestCase): + @_parameterized.parameters( + {'op1': 1, 'op2': 2, 'result': 3}, + {'op1': 4, 'op2': 5, 'result': 9}, + ) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + +If a parameterized test fails, the error message will show the +original test name (which is modified internally) and the arguments +for the specific invocation, which are part of the string returned by +the shortDescription() method on test cases. + +The id method of the test, used internally by the unittest framework, +is also modified to show the arguments. To make sure that test names +stay the same across several invocations, object representations like + + >>> class Foo(object): + ... pass + >>> repr(Foo()) + '<__main__.Foo object at 0x23d8610>' + +are turned into '<__main__.Foo>'. For even more descriptive names, +especially in test logs, you can use the named_parameters decorator. In +this case, only tuples are supported, and the first parameters has to +be a string (or an object that returns an apt name when converted via +str()): + + class NamedExample(_parameterized.TestCase): + @_parameterized.named_parameters( + ('Normal', 'aa', 'aaa', True), + ('EmptyPrefix', '', 'abc', True), + ('BothEmpty', '', '', True)) + def testStartsWith(self, prefix, string, result): + self.assertEqual(result, strings.startswith(prefix)) + +Named tests also have the benefit that they can be run individually +from the command line: + + $ testmodule.py NamedExample.testStartsWithNormal + . + -------------------------------------------------------------------- + Ran 1 test in 0.000s + + OK + +Parameterized Classes +===================== +If invocation arguments are shared across test methods in a single +TestCase class, instead of decorating all test methods +individually, the class itself can be decorated: + + @_parameterized.parameters( + (1, 2, 3) + (4, 5, 9)) + class ArithmeticTest(_parameterized.TestCase): + def testAdd(self, arg1, arg2, result): + self.assertEqual(arg1 + arg2, result) + + def testSubtract(self, arg2, arg2, result): + self.assertEqual(result - arg1, arg2) + +Inputs from Iterables +===================== +If parameters should be shared across several test cases, or are dynamically +created from other sources, a single non-tuple iterable can be passed into +the decorator. This iterable will be used to obtain the test cases: + + class AdditionExample(_parameterized.TestCase): + @_parameterized.parameters( + c.op1, c.op2, c.result for c in testcases + ) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + + +Single-Argument Test Methods +============================ +If a test method takes only one argument, the single argument does not need to +be wrapped into a tuple: + + class NegativeNumberExample(_parameterized.TestCase): + @_parameterized.parameters( + -1, -3, -4, -5 + ) + def testIsNegative(self, arg): + self.assertTrue(IsNegative(arg)) +""" + +__author__ = 'tmarek@google.com (Torsten Marek)' + +import functools +import re +import types +import unittest +import uuid + +try: + # Since python 3 + import collections.abc as collections_abc +except ImportError: + # Won't work after python 3.8 + import collections as collections_abc + +ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') +_SEPARATOR = uuid.uuid1().hex +_FIRST_ARG = object() +_ARGUMENT_REPR = object() + + +def _CleanRepr(obj): + return ADDR_RE.sub(r'<\1>', repr(obj)) + + +# Helper function formerly from the unittest module, removed from it in +# Python 2.7. +def _StrClass(cls): + return '%s.%s' % (cls.__module__, cls.__name__) + + +def _NonStringIterable(obj): + return (isinstance(obj, collections_abc.Iterable) and + not isinstance(obj, str)) + + +def _FormatParameterList(testcase_params): + if isinstance(testcase_params, collections_abc.Mapping): + return ', '.join('%s=%s' % (argname, _CleanRepr(value)) + for argname, value in testcase_params.items()) + elif _NonStringIterable(testcase_params): + return ', '.join(map(_CleanRepr, testcase_params)) + else: + return _FormatParameterList((testcase_params,)) + + +class _ParameterizedTestIter(object): + """Callable and iterable class for producing new test cases.""" + + def __init__(self, test_method, testcases, naming_type): + """Returns concrete test functions for a test and a list of parameters. + + The naming_type is used to determine the name of the concrete + functions as reported by the unittest framework. If naming_type is + _FIRST_ARG, the testcases must be tuples, and the first element must + have a string representation that is a valid Python identifier. + + Args: + test_method: The decorated test method. + testcases: (list of tuple/dict) A list of parameter + tuples/dicts for individual test invocations. + naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. + """ + self._test_method = test_method + self.testcases = testcases + self._naming_type = naming_type + + def __call__(self, *args, **kwargs): + raise RuntimeError('You appear to be running a parameterized test case ' + 'without having inherited from parameterized.' + 'TestCase. This is bad because none of ' + 'your test cases are actually being run.') + + def __iter__(self): + test_method = self._test_method + naming_type = self._naming_type + + def MakeBoundParamTest(testcase_params): + @functools.wraps(test_method) + def BoundParamTest(self): + if isinstance(testcase_params, collections_abc.Mapping): + test_method(self, **testcase_params) + elif _NonStringIterable(testcase_params): + test_method(self, *testcase_params) + else: + test_method(self, testcase_params) + + if naming_type is _FIRST_ARG: + # Signal the metaclass that the name of the test function is unique + # and descriptive. + BoundParamTest.__x_use_name__ = True + BoundParamTest.__name__ += str(testcase_params[0]) + testcase_params = testcase_params[1:] + elif naming_type is _ARGUMENT_REPR: + # __x_extra_id__ is used to pass naming information to the __new__ + # method of TestGeneratorMetaclass. + # The metaclass will make sure to create a unique, but nondescriptive + # name for this test. + BoundParamTest.__x_extra_id__ = '(%s)' % ( + _FormatParameterList(testcase_params),) + else: + raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) + + BoundParamTest.__doc__ = '%s(%s)' % ( + BoundParamTest.__name__, _FormatParameterList(testcase_params)) + if test_method.__doc__: + BoundParamTest.__doc__ += '\n%s' % (test_method.__doc__,) + return BoundParamTest + return (MakeBoundParamTest(c) for c in self.testcases) + + +def _IsSingletonList(testcases): + """True iff testcases contains only a single non-tuple element.""" + return len(testcases) == 1 and not isinstance(testcases[0], tuple) + + +def _ModifyClass(class_object, testcases, naming_type): + assert not getattr(class_object, '_id_suffix', None), ( + 'Cannot add parameters to %s,' + ' which already has parameterized methods.' % (class_object,)) + class_object._id_suffix = id_suffix = {} + # We change the size of __dict__ while we iterate over it, + # which Python 3.x will complain about, so use copy(). + for name, obj in class_object.__dict__.copy().items(): + if (name.startswith(unittest.TestLoader.testMethodPrefix) + and isinstance(obj, types.FunctionType)): + delattr(class_object, name) + methods = {} + _UpdateClassDictForParamTestCase( + methods, id_suffix, name, + _ParameterizedTestIter(obj, testcases, naming_type)) + for name, meth in methods.items(): + setattr(class_object, name, meth) + + +def _ParameterDecorator(naming_type, testcases): + """Implementation of the parameterization decorators. + + Args: + naming_type: The naming type. + testcases: Testcase parameters. + + Returns: + A function for modifying the decorated object. + """ + def _Apply(obj): + if isinstance(obj, type): + _ModifyClass( + obj, + list(testcases) if not isinstance(testcases, collections_abc.Sequence) + else testcases, + naming_type) + return obj + else: + return _ParameterizedTestIter(obj, testcases, naming_type) + + if _IsSingletonList(testcases): + assert _NonStringIterable(testcases[0]), ( + 'Single parameter argument must be a non-string iterable') + testcases = testcases[0] + + return _Apply + + +def parameters(*testcases): # pylint: disable=invalid-name + """A decorator for creating parameterized tests. + + See the module docstring for a usage example. + Args: + *testcases: Parameters for the decorated method, either a single + iterable, or a list of tuples/dicts/objects (for tests + with only one argument). + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + return _ParameterDecorator(_ARGUMENT_REPR, testcases) + + +def named_parameters(*testcases): # pylint: disable=invalid-name + """A decorator for creating parameterized tests. + + See the module docstring for a usage example. The first element of + each parameter tuple should be a string and will be appended to the + name of the test method. + + Args: + *testcases: Parameters for the decorated method, either a single + iterable, or a list of tuples. + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + return _ParameterDecorator(_FIRST_ARG, testcases) + + +class TestGeneratorMetaclass(type): + """Metaclass for test cases with test generators. + + A test generator is an iterable in a testcase that produces callables. These + callables must be single-argument methods. These methods are injected into + the class namespace and the original iterable is removed. If the name of the + iterable conforms to the test pattern, the injected methods will be picked + up as tests by the unittest framework. + + In general, it is supposed to be used in conjunction with the + parameters decorator. + """ + + def __new__(mcs, class_name, bases, dct): + dct['_id_suffix'] = id_suffix = {} + for name, obj in dct.copy().items(): + if (name.startswith(unittest.TestLoader.testMethodPrefix) and + _NonStringIterable(obj)): + iterator = iter(obj) + dct.pop(name) + _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator) + + return type.__new__(mcs, class_name, bases, dct) + + +def _UpdateClassDictForParamTestCase(dct, id_suffix, name, iterator): + """Adds individual test cases to a dictionary. + + Args: + dct: The target dictionary. + id_suffix: The dictionary for mapping names to test IDs. + name: The original name of the test case. + iterator: The iterator generating the individual test cases. + """ + for idx, func in enumerate(iterator): + assert callable(func), 'Test generators must yield callables, got %r' % ( + func,) + if getattr(func, '__x_use_name__', False): + new_name = func.__name__ + else: + new_name = '%s%s%d' % (name, _SEPARATOR, idx) + assert new_name not in dct, ( + 'Name of parameterized test case "%s" not unique' % (new_name,)) + dct[new_name] = func + id_suffix[new_name] = getattr(func, '__x_extra_id__', '') + + +class TestCase(unittest.TestCase, metaclass=TestGeneratorMetaclass): + """Base class for test cases using the parameters decorator.""" + + def _OriginalName(self): + return self._testMethodName.split(_SEPARATOR)[0] + + def __str__(self): + return '%s (%s)' % (self._OriginalName(), _StrClass(self.__class__)) + + def id(self): # pylint: disable=invalid-name + """Returns the descriptive ID of the test. + + This is used internally by the unittesting framework to get a name + for the test to be used in reports. + + Returns: + The test id. + """ + return '%s.%s%s' % (_StrClass(self.__class__), + self._OriginalName(), + self._id_suffix.get(self._testMethodName, '')) + + +def CoopTestCase(other_base_class): + """Returns a new base class with a cooperative metaclass base. + + This enables the TestCase to be used in combination + with other base classes that have custom metaclasses, such as + mox.MoxTestBase. + + Only works with metaclasses that do not override type.__new__. + + Example: + + import google3 + import mox + + from google.protobuf.internal import _parameterized + + class ExampleTest(parameterized.CoopTestCase(mox.MoxTestBase)): + ... + + Args: + other_base_class: (class) A test case base class. + + Returns: + A new class object. + """ + metaclass = type( + 'CoopMetaclass', + (other_base_class.__metaclass__, + TestGeneratorMetaclass), {}) + return metaclass( + 'CoopTestCase', + (other_base_class, TestCase), {}) diff --git a/google/protobuf/internal/api_implementation.py b/google/protobuf/internal/api_implementation.py new file mode 100644 index 0000000..b40446b --- /dev/null +++ b/google/protobuf/internal/api_implementation.py @@ -0,0 +1,142 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Determine which implementation of the protobuf API is used in this process. +""" + +import importlib +import os +import sys +import warnings + +_GOOGLE3_PYTHON_UPB_DEFAULT = True + + +def _ApiVersionToImplementationType(api_version): + if api_version == 2: + return 'cpp' + if api_version == 1: + raise ValueError('api_version=1 is no longer supported.') + if api_version == 0: + return 'python' + return None + + +_implementation_type = None +try: + # pylint: disable=g-import-not-at-top + from google.protobuf.internal import _api_implementation + # The compile-time constants in the _api_implementation module can be used to + # switch to a certain implementation of the Python API at build time. + _implementation_type = _ApiVersionToImplementationType( + _api_implementation.api_version) +except ImportError: + pass # Unspecified by compiler flags. + + +def _CanImport(mod_name): + try: + mod = importlib.import_module(mod_name) + # Work around a known issue in the classic bootstrap .par import hook. + if not mod: + raise ImportError(mod_name + ' import succeeded but was None') + return True + except ImportError: + return False + + +if _implementation_type is None: + if _CanImport('google._upb._message'): + _implementation_type = 'upb' + elif _CanImport('google.protobuf.pyext._message'): + _implementation_type = 'cpp' + else: + _implementation_type = 'python' + + +# This environment variable can be used to switch to a certain implementation +# of the Python API, overriding the compile-time constants in the +# _api_implementation module. Right now only 'python', 'cpp' and 'upb' are +# valid values. Any other value will raise error. +_implementation_type = os.getenv('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION', + _implementation_type) + +if _implementation_type not in ('python', 'cpp', 'upb'): + raise ValueError('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION {0} is not ' + 'supported. Please set to \'python\', \'cpp\' or ' + '\'upb\'.'.format(_implementation_type)) + +if 'PyPy' in sys.version and _implementation_type == 'cpp': + warnings.warn('PyPy does not work yet with cpp protocol buffers. ' + 'Falling back to the python implementation.') + _implementation_type = 'python' + +_c_module = None + +if _implementation_type == 'cpp': + try: + # pylint: disable=g-import-not-at-top + from google.protobuf.pyext import _message + sys.modules['google3.net.proto2.python.internal.cpp._message'] = _message + _c_module = _message + del _message + except ImportError: + # TODO: fail back to python + warnings.warn( + 'Selected implementation cpp is not available.') + pass + +if _implementation_type == 'upb': + try: + # pylint: disable=g-import-not-at-top + from google._upb import _message + _c_module = _message + del _message + except ImportError: + warnings.warn('Selected implementation upb is not available. ' + 'Falling back to the python implementation.') + _implementation_type = 'python' + pass + +# Detect if serialization should be deterministic by default +try: + # The presence of this module in a build allows the proto implementation to + # be upgraded merely via build deps. + # + # NOTE: Merely importing this automatically enables deterministic proto + # serialization for C++ code, but we still need to export it as a boolean so + # that we can do the same for `_implementation_type == 'python'`. + # + # NOTE2: It is possible for C++ code to enable deterministic serialization by + # default _without_ affecting Python code, if the C++ implementation is not in + # use by this module. That is intended behavior, so we don't actually expose + # this boolean outside of this module. + # + # pylint: disable=g-import-not-at-top,unused-import + from google.protobuf import enable_deterministic_proto_serialization + _python_deterministic_proto_serialization = True +except ImportError: + _python_deterministic_proto_serialization = False + + +# Usage of this function is discouraged. Clients shouldn't care which +# implementation of the API is in use. Note that there is no guarantee +# that differences between APIs will be maintained. +# Please don't use this function if possible. +def Type(): + return _implementation_type + + +# See comment on 'Type' above. +# TODO: Remove the API, it returns a constant. b/228102101 +def Version(): + return 2 + + +# For internal use only +def IsPythonDefaultSerializationDeterministic(): + return _python_deterministic_proto_serialization diff --git a/google/protobuf/internal/builder.py b/google/protobuf/internal/builder.py new file mode 100644 index 0000000..c22d994 --- /dev/null +++ b/google/protobuf/internal/builder.py @@ -0,0 +1,118 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Builds descriptors, message classes and services for generated _pb2.py. + +This file is only called in python generated _pb2.py files. It builds +descriptors, message classes and services that users can directly use +in generated code. +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + +from google.protobuf.internal import enum_type_wrapper +from google.protobuf.internal import python_message +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +_sym_db = _symbol_database.Default() + + +def BuildMessageAndEnumDescriptors(file_des, module): + """Builds message and enum descriptors. + + Args: + file_des: FileDescriptor of the .proto file + module: Generated _pb2 module + """ + + def BuildNestedDescriptors(msg_des, prefix): + for (name, nested_msg) in msg_des.nested_types_by_name.items(): + module_name = prefix + name.upper() + module[module_name] = nested_msg + BuildNestedDescriptors(nested_msg, module_name + '_') + for enum_des in msg_des.enum_types: + module[prefix + enum_des.name.upper()] = enum_des + + for (name, msg_des) in file_des.message_types_by_name.items(): + module_name = '_' + name.upper() + module[module_name] = msg_des + BuildNestedDescriptors(msg_des, module_name + '_') + + +def BuildTopDescriptorsAndMessages(file_des, module_name, module): + """Builds top level descriptors and message classes. + + Args: + file_des: FileDescriptor of the .proto file + module_name: str, the name of generated _pb2 module + module: Generated _pb2 module + """ + + def BuildMessage(msg_des): + create_dict = {} + for (name, nested_msg) in msg_des.nested_types_by_name.items(): + create_dict[name] = BuildMessage(nested_msg) + create_dict['DESCRIPTOR'] = msg_des + create_dict['__module__'] = module_name + message_class = _reflection.GeneratedProtocolMessageType( + msg_des.name, (_message.Message,), create_dict) + _sym_db.RegisterMessage(message_class) + return message_class + + # top level enums + for (name, enum_des) in file_des.enum_types_by_name.items(): + module['_' + name.upper()] = enum_des + module[name] = enum_type_wrapper.EnumTypeWrapper(enum_des) + for enum_value in enum_des.values: + module[enum_value.name] = enum_value.number + + # top level extensions + for (name, extension_des) in file_des.extensions_by_name.items(): + module[name.upper() + '_FIELD_NUMBER'] = extension_des.number + module[name] = extension_des + + # services + for (name, service) in file_des.services_by_name.items(): + module['_' + name.upper()] = service + + # Build messages. + for (name, msg_des) in file_des.message_types_by_name.items(): + module[name] = BuildMessage(msg_des) + + +def AddHelpersToExtensions(file_des): + """no-op to keep old generated code work with new runtime. + + Args: + file_des: FileDescriptor of the .proto file + """ + # TODO: Remove this on-op + return + + +def BuildServices(file_des, module_name, module): + """Builds services classes and services stub class. + + Args: + file_des: FileDescriptor of the .proto file + module_name: str, the name of generated _pb2 module + module: Generated _pb2 module + """ + # pylint: disable=g-import-not-at-top + from google.protobuf import service as _service + from google.protobuf import service_reflection + # pylint: enable=g-import-not-at-top + for (name, service) in file_des.services_by_name.items(): + module[name] = service_reflection.GeneratedServiceType( + name, (_service.Service,), + dict(DESCRIPTOR=service, __module__=module_name)) + stub_name = name + '_Stub' + module[stub_name] = service_reflection.GeneratedServiceStubType( + stub_name, (module[name],), + dict(DESCRIPTOR=service, __module__=module_name)) diff --git a/google/protobuf/internal/containers.py b/google/protobuf/internal/containers.py new file mode 100644 index 0000000..2335781 --- /dev/null +++ b/google/protobuf/internal/containers.py @@ -0,0 +1,677 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Contains container classes to represent different protocol buffer types. + +This file defines container classes which represent categories of protocol +buffer field types which need extra maintenance. Currently these categories +are: + +- Repeated scalar fields - These are all repeated fields which aren't + composite (e.g. they are of simple types like int32, string, etc). +- Repeated composite fields - Repeated fields which are composite. This + includes groups and nested messages. +""" + +import collections.abc +import copy +import pickle +from typing import ( + Any, + Iterable, + Iterator, + List, + MutableMapping, + MutableSequence, + NoReturn, + Optional, + Sequence, + TypeVar, + Union, + overload, +) + + +_T = TypeVar('_T') +_K = TypeVar('_K') +_V = TypeVar('_V') + + +class BaseContainer(Sequence[_T]): + """Base container class.""" + + # Minimizes memory usage and disallows assignment to other attributes. + __slots__ = ['_message_listener', '_values'] + + def __init__(self, message_listener: Any) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The RepeatedScalarFieldContainer will call this object's + Modified() method when it is modified. + """ + self._message_listener = message_listener + self._values = [] + + @overload + def __getitem__(self, key: int) -> _T: + ... + + @overload + def __getitem__(self, key: slice) -> List[_T]: + ... + + def __getitem__(self, key): + """Retrieves item by the specified key.""" + return self._values[key] + + def __len__(self) -> int: + """Returns the number of elements in the container.""" + return len(self._values) + + def __ne__(self, other: Any) -> bool: + """Checks if another instance isn't equal to this one.""" + # The concrete classes should define __eq__. + return not self == other + + __hash__ = None + + def __repr__(self) -> str: + return repr(self._values) + + def sort(self, *args, **kwargs) -> None: + # Continue to support the old sort_function keyword argument. + # This is expected to be a rare occurrence, so use LBYL to avoid + # the overhead of actually catching KeyError. + if 'sort_function' in kwargs: + kwargs['cmp'] = kwargs.pop('sort_function') + self._values.sort(*args, **kwargs) + + def reverse(self) -> None: + self._values.reverse() + + +# TODO: Remove this. BaseContainer does *not* conform to +# MutableSequence, only its subclasses do. +collections.abc.MutableSequence.register(BaseContainer) + + +class RepeatedScalarFieldContainer(BaseContainer[_T], MutableSequence[_T]): + """Simple, type-checked, list-like container for holding repeated scalars.""" + + # Disallows assignment to other attributes. + __slots__ = ['_type_checker'] + + def __init__( + self, + message_listener: Any, + type_checker: Any, + ) -> None: + """Args: + + message_listener: A MessageListener implementation. The + RepeatedScalarFieldContainer will call this object's Modified() method + when it is modified. + type_checker: A type_checkers.ValueChecker instance to run on elements + inserted into this container. + """ + super().__init__(message_listener) + self._type_checker = type_checker + + def append(self, value: _T) -> None: + """Appends an item to the list. Similar to list.append().""" + self._values.append(self._type_checker.CheckValue(value)) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def insert(self, key: int, value: _T) -> None: + """Inserts the item at the specified position. Similar to list.insert().""" + self._values.insert(key, self._type_checker.CheckValue(value)) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def extend(self, elem_seq: Iterable[_T]) -> None: + """Extends by appending the given iterable. Similar to list.extend().""" + elem_seq_iter = iter(elem_seq) + new_values = [self._type_checker.CheckValue(elem) for elem in elem_seq_iter] + if new_values: + self._values.extend(new_values) + self._message_listener.Modified() + + def MergeFrom( + self, + other: Union['RepeatedScalarFieldContainer[_T]', Iterable[_T]], + ) -> None: + """Appends the contents of another repeated field of the same type to this + one. We do not check the types of the individual fields. + """ + self._values.extend(other) + self._message_listener.Modified() + + def remove(self, elem: _T): + """Removes an item from the list. Similar to list.remove().""" + self._values.remove(elem) + self._message_listener.Modified() + + def pop(self, key: Optional[int] = -1) -> _T: + """Removes and returns an item at a given index. Similar to list.pop().""" + value = self._values[key] + self.__delitem__(key) + return value + + @overload + def __setitem__(self, key: int, value: _T) -> None: + ... + + @overload + def __setitem__(self, key: slice, value: Iterable[_T]) -> None: + ... + + def __setitem__(self, key, value) -> None: + """Sets the item on the specified position.""" + if isinstance(key, slice): + if key.step is not None: + raise ValueError('Extended slices not supported') + self._values[key] = map(self._type_checker.CheckValue, value) + self._message_listener.Modified() + else: + self._values[key] = self._type_checker.CheckValue(value) + self._message_listener.Modified() + + def __delitem__(self, key: Union[int, slice]) -> None: + """Deletes the item at the specified position.""" + del self._values[key] + self._message_listener.Modified() + + def __eq__(self, other: Any) -> bool: + """Compares the current instance with another one.""" + if self is other: + return True + # Special case for the same type which should be common and fast. + if isinstance(other, self.__class__): + return other._values == self._values + # We are presumably comparing against some other sequence type. + return other == self._values + + def __deepcopy__( + self, + unused_memo: Any = None, + ) -> 'RepeatedScalarFieldContainer[_T]': + clone = RepeatedScalarFieldContainer( + copy.deepcopy(self._message_listener), self._type_checker) + clone.MergeFrom(self) + return clone + + def __reduce__(self, **kwargs) -> NoReturn: + raise pickle.PickleError( + "Can't pickle repeated scalar fields, convert to list first") + + +# TODO: Constrain T to be a subtype of Message. +class RepeatedCompositeFieldContainer(BaseContainer[_T], MutableSequence[_T]): + """Simple, list-like container for holding repeated composite fields.""" + + # Disallows assignment to other attributes. + __slots__ = ['_message_descriptor'] + + def __init__(self, message_listener: Any, message_descriptor: Any) -> None: + """ + Note that we pass in a descriptor instead of the generated directly, + since at the time we construct a _RepeatedCompositeFieldContainer we + haven't yet necessarily initialized the type that will be contained in the + container. + + Args: + message_listener: A MessageListener implementation. + The RepeatedCompositeFieldContainer will call this object's + Modified() method when it is modified. + message_descriptor: A Descriptor instance describing the protocol type + that should be present in this container. We'll use the + _concrete_class field of this descriptor when the client calls add(). + """ + super().__init__(message_listener) + self._message_descriptor = message_descriptor + + def add(self, **kwargs: Any) -> _T: + """Adds a new element at the end of the list and returns it. Keyword + arguments may be used to initialize the element. + """ + new_element = self._message_descriptor._concrete_class(**kwargs) + new_element._SetListener(self._message_listener) + self._values.append(new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + return new_element + + def append(self, value: _T) -> None: + """Appends one element by copying the message.""" + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + new_element.CopyFrom(value) + self._values.append(new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def insert(self, key: int, value: _T) -> None: + """Inserts the item at the specified position by copying.""" + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + new_element.CopyFrom(value) + self._values.insert(key, new_element) + if not self._message_listener.dirty: + self._message_listener.Modified() + + def extend(self, elem_seq: Iterable[_T]) -> None: + """Extends by appending the given sequence of elements of the same type + + as this one, copying each individual message. + """ + message_class = self._message_descriptor._concrete_class + listener = self._message_listener + values = self._values + for message in elem_seq: + new_element = message_class() + new_element._SetListener(listener) + new_element.MergeFrom(message) + values.append(new_element) + listener.Modified() + + def MergeFrom( + self, + other: Union['RepeatedCompositeFieldContainer[_T]', Iterable[_T]], + ) -> None: + """Appends the contents of another repeated field of the same type to this + one, copying each individual message. + """ + self.extend(other) + + def remove(self, elem: _T) -> None: + """Removes an item from the list. Similar to list.remove().""" + self._values.remove(elem) + self._message_listener.Modified() + + def pop(self, key: Optional[int] = -1) -> _T: + """Removes and returns an item at a given index. Similar to list.pop().""" + value = self._values[key] + self.__delitem__(key) + return value + + @overload + def __setitem__(self, key: int, value: _T) -> None: + ... + + @overload + def __setitem__(self, key: slice, value: Iterable[_T]) -> None: + ... + + def __setitem__(self, key, value): + # This method is implemented to make RepeatedCompositeFieldContainer + # structurally compatible with typing.MutableSequence. It is + # otherwise unsupported and will always raise an error. + raise TypeError( + f'{self.__class__.__name__} object does not support item assignment') + + def __delitem__(self, key: Union[int, slice]) -> None: + """Deletes the item at the specified position.""" + del self._values[key] + self._message_listener.Modified() + + def __eq__(self, other: Any) -> bool: + """Compares the current instance with another one.""" + if self is other: + return True + if not isinstance(other, self.__class__): + raise TypeError('Can only compare repeated composite fields against ' + 'other repeated composite fields.') + return self._values == other._values + + +class ScalarMap(MutableMapping[_K, _V]): + """Simple, type-checked, dict-like container for holding repeated scalars.""" + + # Disallows assignment to other attributes. + __slots__ = ['_key_checker', '_value_checker', '_values', '_message_listener', + '_entry_descriptor'] + + def __init__( + self, + message_listener: Any, + key_checker: Any, + value_checker: Any, + entry_descriptor: Any, + ) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The ScalarMap will call this object's Modified() method when it + is modified. + key_checker: A type_checkers.ValueChecker instance to run on keys + inserted into this container. + value_checker: A type_checkers.ValueChecker instance to run on values + inserted into this container. + entry_descriptor: The MessageDescriptor of a map entry: key and value. + """ + self._message_listener = message_listener + self._key_checker = key_checker + self._value_checker = value_checker + self._entry_descriptor = entry_descriptor + self._values = {} + + def __getitem__(self, key: _K) -> _V: + try: + return self._values[key] + except KeyError: + key = self._key_checker.CheckValue(key) + val = self._value_checker.DefaultValue() + self._values[key] = val + return val + + def __contains__(self, item: _K) -> bool: + # We check the key's type to match the strong-typing flavor of the API. + # Also this makes it easier to match the behavior of the C++ implementation. + self._key_checker.CheckValue(item) + return item in self._values + + @overload + def get(self, key: _K) -> Optional[_V]: + ... + + @overload + def get(self, key: _K, default: _T) -> Union[_V, _T]: + ... + + # We need to override this explicitly, because our defaultdict-like behavior + # will make the default implementation (from our base class) always insert + # the key. + def get(self, key, default=None): + if key in self: + return self[key] + else: + return default + + def __setitem__(self, key: _K, value: _V) -> _T: + checked_key = self._key_checker.CheckValue(key) + checked_value = self._value_checker.CheckValue(value) + self._values[checked_key] = checked_value + self._message_listener.Modified() + + def __delitem__(self, key: _K) -> None: + del self._values[key] + self._message_listener.Modified() + + def __len__(self) -> int: + return len(self._values) + + def __iter__(self) -> Iterator[_K]: + return iter(self._values) + + def __repr__(self) -> str: + return repr(self._values) + + def MergeFrom(self, other: 'ScalarMap[_K, _V]') -> None: + self._values.update(other._values) + self._message_listener.Modified() + + def InvalidateIterators(self) -> None: + # It appears that the only way to reliably invalidate iterators to + # self._values is to ensure that its size changes. + original = self._values + self._values = original.copy() + original[None] = None + + # This is defined in the abstract base, but we can do it much more cheaply. + def clear(self) -> None: + self._values.clear() + self._message_listener.Modified() + + def GetEntryClass(self) -> Any: + return self._entry_descriptor._concrete_class + + +class MessageMap(MutableMapping[_K, _V]): + """Simple, type-checked, dict-like container for with submessage values.""" + + # Disallows assignment to other attributes. + __slots__ = ['_key_checker', '_values', '_message_listener', + '_message_descriptor', '_entry_descriptor'] + + def __init__( + self, + message_listener: Any, + message_descriptor: Any, + key_checker: Any, + entry_descriptor: Any, + ) -> None: + """ + Args: + message_listener: A MessageListener implementation. + The ScalarMap will call this object's Modified() method when it + is modified. + key_checker: A type_checkers.ValueChecker instance to run on keys + inserted into this container. + value_checker: A type_checkers.ValueChecker instance to run on values + inserted into this container. + entry_descriptor: The MessageDescriptor of a map entry: key and value. + """ + self._message_listener = message_listener + self._message_descriptor = message_descriptor + self._key_checker = key_checker + self._entry_descriptor = entry_descriptor + self._values = {} + + def __getitem__(self, key: _K) -> _V: + key = self._key_checker.CheckValue(key) + try: + return self._values[key] + except KeyError: + new_element = self._message_descriptor._concrete_class() + new_element._SetListener(self._message_listener) + self._values[key] = new_element + self._message_listener.Modified() + return new_element + + def get_or_create(self, key: _K) -> _V: + """get_or_create() is an alias for getitem (ie. map[key]). + + Args: + key: The key to get or create in the map. + + This is useful in cases where you want to be explicit that the call is + mutating the map. This can avoid lint errors for statements like this + that otherwise would appear to be pointless statements: + + msg.my_map[key] + """ + return self[key] + + @overload + def get(self, key: _K) -> Optional[_V]: + ... + + @overload + def get(self, key: _K, default: _T) -> Union[_V, _T]: + ... + + # We need to override this explicitly, because our defaultdict-like behavior + # will make the default implementation (from our base class) always insert + # the key. + def get(self, key, default=None): + if key in self: + return self[key] + else: + return default + + def __contains__(self, item: _K) -> bool: + item = self._key_checker.CheckValue(item) + return item in self._values + + def __setitem__(self, key: _K, value: _V) -> NoReturn: + raise ValueError('May not set values directly, call my_map[key].foo = 5') + + def __delitem__(self, key: _K) -> None: + key = self._key_checker.CheckValue(key) + del self._values[key] + self._message_listener.Modified() + + def __len__(self) -> int: + return len(self._values) + + def __iter__(self) -> Iterator[_K]: + return iter(self._values) + + def __repr__(self) -> str: + return repr(self._values) + + def MergeFrom(self, other: 'MessageMap[_K, _V]') -> None: + # pylint: disable=protected-access + for key in other._values: + # According to documentation: "When parsing from the wire or when merging, + # if there are duplicate map keys the last key seen is used". + if key in self: + del self[key] + self[key].CopyFrom(other[key]) + # self._message_listener.Modified() not required here, because + # mutations to submessages already propagate. + + def InvalidateIterators(self) -> None: + # It appears that the only way to reliably invalidate iterators to + # self._values is to ensure that its size changes. + original = self._values + self._values = original.copy() + original[None] = None + + # This is defined in the abstract base, but we can do it much more cheaply. + def clear(self) -> None: + self._values.clear() + self._message_listener.Modified() + + def GetEntryClass(self) -> Any: + return self._entry_descriptor._concrete_class + + +class _UnknownField: + """A parsed unknown field.""" + + # Disallows assignment to other attributes. + __slots__ = ['_field_number', '_wire_type', '_data'] + + def __init__(self, field_number, wire_type, data): + self._field_number = field_number + self._wire_type = wire_type + self._data = data + return + + def __lt__(self, other): + # pylint: disable=protected-access + return self._field_number < other._field_number + + def __eq__(self, other): + if self is other: + return True + # pylint: disable=protected-access + return (self._field_number == other._field_number and + self._wire_type == other._wire_type and + self._data == other._data) + + +class UnknownFieldRef: # pylint: disable=missing-class-docstring + + def __init__(self, parent, index): + self._parent = parent + self._index = index + + def _check_valid(self): + if not self._parent: + raise ValueError('UnknownField does not exist. ' + 'The parent message might be cleared.') + if self._index >= len(self._parent): + raise ValueError('UnknownField does not exist. ' + 'The parent message might be cleared.') + + @property + def field_number(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._field_number + + @property + def wire_type(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._wire_type + + @property + def data(self): + self._check_valid() + # pylint: disable=protected-access + return self._parent._internal_get(self._index)._data + + +class UnknownFieldSet: + """UnknownField container""" + + # Disallows assignment to other attributes. + __slots__ = ['_values'] + + def __init__(self): + self._values = [] + + def __getitem__(self, index): + if self._values is None: + raise ValueError('UnknownFields does not exist. ' + 'The parent message might be cleared.') + size = len(self._values) + if index < 0: + index += size + if index < 0 or index >= size: + raise IndexError('index %d out of range'.index) + + return UnknownFieldRef(self, index) + + def _internal_get(self, index): + return self._values[index] + + def __len__(self): + if self._values is None: + raise ValueError('UnknownFields does not exist. ' + 'The parent message might be cleared.') + return len(self._values) + + def _add(self, field_number, wire_type, data): + unknown_field = _UnknownField(field_number, wire_type, data) + self._values.append(unknown_field) + return unknown_field + + def __iter__(self): + for i in range(len(self)): + yield UnknownFieldRef(self, i) + + def _extend(self, other): + if other is None: + return + # pylint: disable=protected-access + self._values.extend(other._values) + + def __eq__(self, other): + if self is other: + return True + # Sort unknown fields because their order shouldn't + # affect equality test. + values = list(self._values) + if other is None: + return not values + values.sort() + # pylint: disable=protected-access + other_values = sorted(other._values) + return values == other_values + + def _clear(self): + for value in self._values: + # pylint: disable=protected-access + if isinstance(value._data, UnknownFieldSet): + value._data._clear() # pylint: disable=protected-access + self._values = None diff --git a/google/protobuf/internal/decoder.py b/google/protobuf/internal/decoder.py new file mode 100644 index 0000000..ddaced2 --- /dev/null +++ b/google/protobuf/internal/decoder.py @@ -0,0 +1,1024 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Code for decoding protocol buffer primitives. + +This code is very similar to encoder.py -- read the docs for that module first. + +A "decoder" is a function with the signature: + Decode(buffer, pos, end, message, field_dict) +The arguments are: + buffer: The string containing the encoded message. + pos: The current position in the string. + end: The position in the string where the current message ends. May be + less than len(buffer) if we're reading a sub-message. + message: The message object into which we're parsing. + field_dict: message._fields (avoids a hashtable lookup). +The decoder reads the field and stores it into field_dict, returning the new +buffer position. A decoder for a repeated field may proactively decode all of +the elements of that field, if they appear consecutively. + +Note that decoders may throw any of the following: + IndexError: Indicates a truncated message. + struct.error: Unpacking of a fixed-width field failed. + message.DecodeError: Other errors. + +Decoders are expected to raise an exception if they are called with pos > end. +This allows callers to be lax about bounds checking: it's fineto read past +"end" as long as you are sure that someone else will notice and throw an +exception later on. + +Something up the call stack is expected to catch IndexError and struct.error +and convert them to message.DecodeError. + +Decoders are constructed using decoder constructors with the signature: + MakeDecoder(field_number, is_repeated, is_packed, key, new_default) +The arguments are: + field_number: The field number of the field we want to decode. + is_repeated: Is the field a repeated field? (bool) + is_packed: Is the field a packed field? (bool) + key: The key to use when looking up the field within field_dict. + (This is actually the FieldDescriptor but nothing in this + file should depend on that.) + new_default: A function which takes a message object as a parameter and + returns a new instance of the default value for this field. + (This is called for repeated fields and sub-messages, when an + instance does not already exist.) + +As with encoders, we define a decoder constructor for every type of field. +Then, for every field of every message class we construct an actual decoder. +That decoder goes into a dict indexed by tag, so when we decode a message +we repeatedly read a tag, look up the corresponding decoder, and invoke it. +""" + +__author__ = 'kenton@google.com (Kenton Varda)' + +import math +import struct + +from google.protobuf.internal import containers +from google.protobuf.internal import encoder +from google.protobuf.internal import wire_format +from google.protobuf import message + + +# This is not for optimization, but rather to avoid conflicts with local +# variables named "message". +_DecodeError = message.DecodeError + + +def _VarintDecoder(mask, result_type): + """Return an encoder for a basic varint value (does not include tag). + + Decoded values will be bitwise-anded with the given mask before being + returned, e.g. to limit them to 32 bits. The returned decoder does not + take the usual "end" parameter -- the caller is expected to do bounds checking + after the fact (often the caller can defer such checking until later). The + decoder returns a (value, new_pos) pair. + """ + + def DecodeVarint(buffer, pos): + result = 0 + shift = 0 + while 1: + b = buffer[pos] + result |= ((b & 0x7f) << shift) + pos += 1 + if not (b & 0x80): + result &= mask + result = result_type(result) + return (result, pos) + shift += 7 + if shift >= 64: + raise _DecodeError('Too many bytes when decoding varint.') + return DecodeVarint + + +def _SignedVarintDecoder(bits, result_type): + """Like _VarintDecoder() but decodes signed values.""" + + signbit = 1 << (bits - 1) + mask = (1 << bits) - 1 + + def DecodeVarint(buffer, pos): + result = 0 + shift = 0 + while 1: + b = buffer[pos] + result |= ((b & 0x7f) << shift) + pos += 1 + if not (b & 0x80): + result &= mask + result = (result ^ signbit) - signbit + result = result_type(result) + return (result, pos) + shift += 7 + if shift >= 64: + raise _DecodeError('Too many bytes when decoding varint.') + return DecodeVarint + +# All 32-bit and 64-bit values are represented as int. +_DecodeVarint = _VarintDecoder((1 << 64) - 1, int) +_DecodeSignedVarint = _SignedVarintDecoder(64, int) + +# Use these versions for values which must be limited to 32 bits. +_DecodeVarint32 = _VarintDecoder((1 << 32) - 1, int) +_DecodeSignedVarint32 = _SignedVarintDecoder(32, int) + + +def ReadTag(buffer, pos): + """Read a tag from the memoryview, and return a (tag_bytes, new_pos) tuple. + + We return the raw bytes of the tag rather than decoding them. The raw + bytes can then be used to look up the proper decoder. This effectively allows + us to trade some work that would be done in pure-python (decoding a varint) + for work that is done in C (searching for a byte string in a hash table). + In a low-level language it would be much cheaper to decode the varint and + use that, but not in Python. + + Args: + buffer: memoryview object of the encoded bytes + pos: int of the current position to start from + + Returns: + Tuple[bytes, int] of the tag data and new position. + """ + start = pos + while buffer[pos] & 0x80: + pos += 1 + pos += 1 + + tag_bytes = buffer[start:pos].tobytes() + return tag_bytes, pos + + +# -------------------------------------------------------------------- + + +def _SimpleDecoder(wire_type, decode_value): + """Return a constructor for a decoder for fields of a particular type. + + Args: + wire_type: The field's wire type. + decode_value: A function which decodes an individual value, e.g. + _DecodeVarint() + """ + + def SpecificDecoder(field_number, is_repeated, is_packed, key, new_default, + clear_if_default=False): + if is_packed: + local_DecodeVarint = _DecodeVarint + def DecodePackedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + (endpoint, pos) = local_DecodeVarint(buffer, pos) + endpoint += pos + if endpoint > end: + raise _DecodeError('Truncated message.') + while pos < endpoint: + (element, pos) = decode_value(buffer, pos) + value.append(element) + if pos > endpoint: + del value[-1] # Discard corrupt value. + raise _DecodeError('Packed element was truncated.') + return pos + return DecodePackedField + elif is_repeated: + tag_bytes = encoder.TagBytes(field_number, wire_type) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (element, new_pos) = decode_value(buffer, pos) + value.append(element) + # Predict that the next tag is another copy of the same repeated + # field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos >= end: + # Prediction failed. Return. + if new_pos > end: + raise _DecodeError('Truncated message.') + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (new_value, pos) = decode_value(buffer, pos) + if pos > end: + raise _DecodeError('Truncated message.') + if clear_if_default and not new_value: + field_dict.pop(key, None) + else: + field_dict[key] = new_value + return pos + return DecodeField + + return SpecificDecoder + + +def _ModifiedDecoder(wire_type, decode_value, modify_value): + """Like SimpleDecoder but additionally invokes modify_value on every value + before storing it. Usually modify_value is ZigZagDecode. + """ + + # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but + # not enough to make a significant difference. + + def InnerDecode(buffer, pos): + (result, new_pos) = decode_value(buffer, pos) + return (modify_value(result), new_pos) + return _SimpleDecoder(wire_type, InnerDecode) + + +def _StructPackDecoder(wire_type, format): + """Return a constructor for a decoder for a fixed-width field. + + Args: + wire_type: The field's wire type. + format: The format string to pass to struct.unpack(). + """ + + value_size = struct.calcsize(format) + local_unpack = struct.unpack + + # Reusing _SimpleDecoder is slightly slower than copying a bunch of code, but + # not enough to make a significant difference. + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + + def InnerDecode(buffer, pos): + new_pos = pos + value_size + result = local_unpack(format, buffer[pos:new_pos])[0] + return (result, new_pos) + return _SimpleDecoder(wire_type, InnerDecode) + + +def _FloatDecoder(): + """Returns a decoder for a float field. + + This code works around a bug in struct.unpack for non-finite 32-bit + floating-point values. + """ + + local_unpack = struct.unpack + + def InnerDecode(buffer, pos): + """Decode serialized float to a float and new position. + + Args: + buffer: memoryview of the serialized bytes + pos: int, position in the memory view to start at. + + Returns: + Tuple[float, int] of the deserialized float value and new position + in the serialized data. + """ + # We expect a 32-bit value in little-endian byte order. Bit 1 is the sign + # bit, bits 2-9 represent the exponent, and bits 10-32 are the significand. + new_pos = pos + 4 + float_bytes = buffer[pos:new_pos].tobytes() + + # If this value has all its exponent bits set, then it's non-finite. + # In Python 2.4, struct.unpack will convert it to a finite 64-bit value. + # To avoid that, we parse it specially. + if (float_bytes[3:4] in b'\x7F\xFF' and float_bytes[2:3] >= b'\x80'): + # If at least one significand bit is set... + if float_bytes[0:3] != b'\x00\x00\x80': + return (math.nan, new_pos) + # If sign bit is set... + if float_bytes[3:4] == b'\xFF': + return (-math.inf, new_pos) + return (math.inf, new_pos) + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + result = local_unpack('= b'\xF0') + and (double_bytes[0:7] != b'\x00\x00\x00\x00\x00\x00\xF0')): + return (math.nan, new_pos) + + # Note that we expect someone up-stack to catch struct.error and convert + # it to _DecodeError -- this way we don't have to set up exception- + # handling blocks every time we parse one value. + result = local_unpack(' end: + raise _DecodeError('Truncated message.') + while pos < endpoint: + value_start_pos = pos + (element, pos) = _DecodeSignedVarint32(buffer, pos) + # pylint: disable=protected-access + if element in enum_type.values_by_number: + value.append(element) + else: + if not message._unknown_fields: + message._unknown_fields = [] + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_VARINT) + + message._unknown_fields.append( + (tag_bytes, buffer[value_start_pos:pos].tobytes())) + # pylint: enable=protected-access + if pos > endpoint: + if element in enum_type.values_by_number: + del value[-1] # Discard corrupt value. + else: + del message._unknown_fields[-1] + # pylint: enable=protected-access + raise _DecodeError('Packed element was truncated.') + return pos + return DecodePackedField + elif is_repeated: + tag_bytes = encoder.TagBytes(field_number, wire_format.WIRETYPE_VARINT) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + """Decode serialized repeated enum to its value and a new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (element, new_pos) = _DecodeSignedVarint32(buffer, pos) + # pylint: disable=protected-access + if element in enum_type.values_by_number: + value.append(element) + else: + if not message._unknown_fields: + message._unknown_fields = [] + message._unknown_fields.append( + (tag_bytes, buffer[pos:new_pos].tobytes())) + # pylint: enable=protected-access + # Predict that the next tag is another copy of the same repeated + # field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos >= end: + # Prediction failed. Return. + if new_pos > end: + raise _DecodeError('Truncated message.') + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + """Decode serialized repeated enum to its value and a new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + value_start_pos = pos + (enum_value, pos) = _DecodeSignedVarint32(buffer, pos) + if pos > end: + raise _DecodeError('Truncated message.') + if clear_if_default and not enum_value: + field_dict.pop(key, None) + return pos + # pylint: disable=protected-access + if enum_value in enum_type.values_by_number: + field_dict[key] = enum_value + else: + if not message._unknown_fields: + message._unknown_fields = [] + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_VARINT) + message._unknown_fields.append( + (tag_bytes, buffer[value_start_pos:pos].tobytes())) + # pylint: enable=protected-access + return pos + return DecodeField + + +# -------------------------------------------------------------------- + + +Int32Decoder = _SimpleDecoder( + wire_format.WIRETYPE_VARINT, _DecodeSignedVarint32) + +Int64Decoder = _SimpleDecoder( + wire_format.WIRETYPE_VARINT, _DecodeSignedVarint) + +UInt32Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint32) +UInt64Decoder = _SimpleDecoder(wire_format.WIRETYPE_VARINT, _DecodeVarint) + +SInt32Decoder = _ModifiedDecoder( + wire_format.WIRETYPE_VARINT, _DecodeVarint32, wire_format.ZigZagDecode) +SInt64Decoder = _ModifiedDecoder( + wire_format.WIRETYPE_VARINT, _DecodeVarint, wire_format.ZigZagDecode) + +# Note that Python conveniently guarantees that when using the '<' prefix on +# formats, they will also have the same size across all platforms (as opposed +# to without the prefix, where their sizes depend on the C compiler's basic +# type sizes). +Fixed32Decoder = _StructPackDecoder(wire_format.WIRETYPE_FIXED32, ' end: + raise _DecodeError('Truncated string.') + value.append(_ConvertToUnicode(buffer[pos:new_pos])) + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + if clear_if_default and not size: + field_dict.pop(key, None) + else: + field_dict[key] = _ConvertToUnicode(buffer[pos:new_pos]) + return new_pos + return DecodeField + + +def BytesDecoder(field_number, is_repeated, is_packed, key, new_default, + clear_if_default=False): + """Returns a decoder for a bytes field.""" + + local_DecodeVarint = _DecodeVarint + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + value.append(buffer[pos:new_pos].tobytes()) + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated string.') + if clear_if_default and not size: + field_dict.pop(key, None) + else: + field_dict[key] = buffer[pos:new_pos].tobytes() + return new_pos + return DecodeField + + +def GroupDecoder(field_number, is_repeated, is_packed, key, new_default): + """Returns a decoder for a group field.""" + + end_tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_END_GROUP) + end_tag_len = len(end_tag_bytes) + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_START_GROUP) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read sub-message. + pos = value.add()._InternalParse(buffer, pos, end) + # Read end tag. + new_pos = pos+end_tag_len + if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: + raise _DecodeError('Missing group end tag.') + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read sub-message. + pos = value._InternalParse(buffer, pos, end) + # Read end tag. + new_pos = pos+end_tag_len + if buffer[pos:new_pos] != end_tag_bytes or new_pos > end: + raise _DecodeError('Missing group end tag.') + return new_pos + return DecodeField + + +def MessageDecoder(field_number, is_repeated, is_packed, key, new_default): + """Returns a decoder for a message field.""" + + local_DecodeVarint = _DecodeVarint + + assert not is_packed + if is_repeated: + tag_bytes = encoder.TagBytes(field_number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + def DecodeRepeatedField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + if value.add()._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + return DecodeRepeatedField + else: + def DecodeField(buffer, pos, end, message, field_dict): + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + if value._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it encountered + # an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + return new_pos + return DecodeField + + +# -------------------------------------------------------------------- + +MESSAGE_SET_ITEM_TAG = encoder.TagBytes(1, wire_format.WIRETYPE_START_GROUP) + +def MessageSetItemDecoder(descriptor): + """Returns a decoder for a MessageSet item. + + The parameter is the message Descriptor. + + The message set message looks like this: + message MessageSet { + repeated group Item = 1 { + required int32 type_id = 2; + required string message = 3; + } + } + """ + + type_id_tag_bytes = encoder.TagBytes(2, wire_format.WIRETYPE_VARINT) + message_tag_bytes = encoder.TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED) + item_end_tag_bytes = encoder.TagBytes(1, wire_format.WIRETYPE_END_GROUP) + + local_ReadTag = ReadTag + local_DecodeVarint = _DecodeVarint + local_SkipField = SkipField + + def DecodeItem(buffer, pos, end, message, field_dict): + """Decode serialized message set to its value and new position. + + Args: + buffer: memoryview of the serialized bytes. + pos: int, position in the memory view to start at. + end: int, end position of serialized data + message: Message object to store unknown fields in + field_dict: Map[Descriptor, Any] to store decoded values in. + + Returns: + int, new position in serialized data. + """ + message_set_item_start = pos + type_id = -1 + message_start = -1 + message_end = -1 + + # Technically, type_id and message can appear in any order, so we need + # a little loop here. + while 1: + (tag_bytes, pos) = local_ReadTag(buffer, pos) + if tag_bytes == type_id_tag_bytes: + (type_id, pos) = local_DecodeVarint(buffer, pos) + elif tag_bytes == message_tag_bytes: + (size, message_start) = local_DecodeVarint(buffer, pos) + pos = message_end = message_start + size + elif tag_bytes == item_end_tag_bytes: + break + else: + pos = SkipField(buffer, pos, end, tag_bytes) + if pos == -1: + raise _DecodeError('Missing group end tag.') + + if pos > end: + raise _DecodeError('Truncated message.') + + if type_id == -1: + raise _DecodeError('MessageSet item missing type_id.') + if message_start == -1: + raise _DecodeError('MessageSet item missing message.') + + extension = message.Extensions._FindExtensionByNumber(type_id) + # pylint: disable=protected-access + if extension is not None: + value = field_dict.get(extension) + if value is None: + message_type = extension.message_type + if not hasattr(message_type, '_concrete_class'): + message_factory.GetMessageClass(message_type) + value = field_dict.setdefault( + extension, message_type._concrete_class()) + if value._InternalParse(buffer, message_start,message_end) != message_end: + # The only reason _InternalParse would return early is if it encountered + # an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + else: + if not message._unknown_fields: + message._unknown_fields = [] + message._unknown_fields.append( + (MESSAGE_SET_ITEM_TAG, buffer[message_set_item_start:pos].tobytes())) + # pylint: enable=protected-access + + return pos + + return DecodeItem + + +def UnknownMessageSetItemDecoder(): + """Returns a decoder for a Unknown MessageSet item.""" + + type_id_tag_bytes = encoder.TagBytes(2, wire_format.WIRETYPE_VARINT) + message_tag_bytes = encoder.TagBytes(3, wire_format.WIRETYPE_LENGTH_DELIMITED) + item_end_tag_bytes = encoder.TagBytes(1, wire_format.WIRETYPE_END_GROUP) + + def DecodeUnknownItem(buffer): + pos = 0 + end = len(buffer) + message_start = -1 + message_end = -1 + while 1: + (tag_bytes, pos) = ReadTag(buffer, pos) + if tag_bytes == type_id_tag_bytes: + (type_id, pos) = _DecodeVarint(buffer, pos) + elif tag_bytes == message_tag_bytes: + (size, message_start) = _DecodeVarint(buffer, pos) + pos = message_end = message_start + size + elif tag_bytes == item_end_tag_bytes: + break + else: + pos = SkipField(buffer, pos, end, tag_bytes) + if pos == -1: + raise _DecodeError('Missing group end tag.') + + if pos > end: + raise _DecodeError('Truncated message.') + + if type_id == -1: + raise _DecodeError('MessageSet item missing type_id.') + if message_start == -1: + raise _DecodeError('MessageSet item missing message.') + + return (type_id, buffer[message_start:message_end].tobytes()) + + return DecodeUnknownItem + +# -------------------------------------------------------------------- + +def MapDecoder(field_descriptor, new_default, is_message_map): + """Returns a decoder for a map field.""" + + key = field_descriptor + tag_bytes = encoder.TagBytes(field_descriptor.number, + wire_format.WIRETYPE_LENGTH_DELIMITED) + tag_len = len(tag_bytes) + local_DecodeVarint = _DecodeVarint + # Can't read _concrete_class yet; might not be initialized. + message_type = field_descriptor.message_type + + def DecodeMap(buffer, pos, end, message, field_dict): + submsg = message_type._concrete_class() + value = field_dict.get(key) + if value is None: + value = field_dict.setdefault(key, new_default(message)) + while 1: + # Read length. + (size, pos) = local_DecodeVarint(buffer, pos) + new_pos = pos + size + if new_pos > end: + raise _DecodeError('Truncated message.') + # Read sub-message. + submsg.Clear() + if submsg._InternalParse(buffer, pos, new_pos) != new_pos: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise _DecodeError('Unexpected end-group tag.') + + if is_message_map: + value[submsg.key].CopyFrom(submsg.value) + else: + value[submsg.key] = submsg.value + + # Predict that the next tag is another copy of the same repeated field. + pos = new_pos + tag_len + if buffer[new_pos:pos] != tag_bytes or new_pos == end: + # Prediction failed. Return. + return new_pos + + return DecodeMap + +# -------------------------------------------------------------------- +# Optimization is not as heavy here because calls to SkipField() are rare, +# except for handling end-group tags. + +def _SkipVarint(buffer, pos, end): + """Skip a varint value. Returns the new position.""" + # Previously ord(buffer[pos]) raised IndexError when pos is out of range. + # With this code, ord(b'') raises TypeError. Both are handled in + # python_message.py to generate a 'Truncated message' error. + while ord(buffer[pos:pos+1].tobytes()) & 0x80: + pos += 1 + pos += 1 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + +def _SkipFixed64(buffer, pos, end): + """Skip a fixed64 value. Returns the new position.""" + + pos += 8 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + + +def _DecodeFixed64(buffer, pos): + """Decode a fixed64.""" + new_pos = pos + 8 + return (struct.unpack(' end: + raise _DecodeError('Truncated message.') + return pos + + +def _SkipGroup(buffer, pos, end): + """Skip sub-group. Returns the new position.""" + + while 1: + (tag_bytes, pos) = ReadTag(buffer, pos) + new_pos = SkipField(buffer, pos, end, tag_bytes) + if new_pos == -1: + return pos + pos = new_pos + + +def _DecodeUnknownFieldSet(buffer, pos, end_pos=None): + """Decode UnknownFieldSet. Returns the UnknownFieldSet and new position.""" + + unknown_field_set = containers.UnknownFieldSet() + while end_pos is None or pos < end_pos: + (tag_bytes, pos) = ReadTag(buffer, pos) + (tag, _) = _DecodeVarint(tag_bytes, 0) + field_number, wire_type = wire_format.UnpackTag(tag) + if wire_type == wire_format.WIRETYPE_END_GROUP: + break + (data, pos) = _DecodeUnknownField(buffer, pos, wire_type) + # pylint: disable=protected-access + unknown_field_set._add(field_number, wire_type, data) + + return (unknown_field_set, pos) + + +def _DecodeUnknownField(buffer, pos, wire_type): + """Decode a unknown field. Returns the UnknownField and new position.""" + + if wire_type == wire_format.WIRETYPE_VARINT: + (data, pos) = _DecodeVarint(buffer, pos) + elif wire_type == wire_format.WIRETYPE_FIXED64: + (data, pos) = _DecodeFixed64(buffer, pos) + elif wire_type == wire_format.WIRETYPE_FIXED32: + (data, pos) = _DecodeFixed32(buffer, pos) + elif wire_type == wire_format.WIRETYPE_LENGTH_DELIMITED: + (size, pos) = _DecodeVarint(buffer, pos) + data = buffer[pos:pos+size].tobytes() + pos += size + elif wire_type == wire_format.WIRETYPE_START_GROUP: + (data, pos) = _DecodeUnknownFieldSet(buffer, pos) + elif wire_type == wire_format.WIRETYPE_END_GROUP: + return (0, -1) + else: + raise _DecodeError('Wrong wire type in tag.') + + return (data, pos) + + +def _EndGroup(buffer, pos, end): + """Skipping an END_GROUP tag returns -1 to tell the parent loop to break.""" + + return -1 + + +def _SkipFixed32(buffer, pos, end): + """Skip a fixed32 value. Returns the new position.""" + + pos += 4 + if pos > end: + raise _DecodeError('Truncated message.') + return pos + + +def _DecodeFixed32(buffer, pos): + """Decode a fixed32.""" + + new_pos = pos + 4 + return (struct.unpack('B').pack + + def EncodeVarint(write, value, unused_deterministic=None): + bits = value & 0x7f + value >>= 7 + while value: + write(local_int2byte(0x80|bits)) + bits = value & 0x7f + value >>= 7 + return write(local_int2byte(bits)) + + return EncodeVarint + + +def _SignedVarintEncoder(): + """Return an encoder for a basic signed varint value (does not include + tag).""" + + local_int2byte = struct.Struct('>B').pack + + def EncodeSignedVarint(write, value, unused_deterministic=None): + if value < 0: + value += (1 << 64) + bits = value & 0x7f + value >>= 7 + while value: + write(local_int2byte(0x80|bits)) + bits = value & 0x7f + value >>= 7 + return write(local_int2byte(bits)) + + return EncodeSignedVarint + + +_EncodeVarint = _VarintEncoder() +_EncodeSignedVarint = _SignedVarintEncoder() + + +def _VarintBytes(value): + """Encode the given integer as a varint and return the bytes. This is only + called at startup time so it doesn't need to be fast.""" + + pieces = [] + _EncodeVarint(pieces.append, value, True) + return b"".join(pieces) + + +def TagBytes(field_number, wire_type): + """Encode the given tag and return the bytes. Only called at startup.""" + + return bytes(_VarintBytes(wire_format.PackTag(field_number, wire_type))) + +# -------------------------------------------------------------------- +# As with sizers (see above), we have a number of common encoder +# implementations. + + +def _SimpleEncoder(wire_type, encode_value, compute_value_size): + """Return a constructor for an encoder for fields of a particular type. + + Args: + wire_type: The field's wire type, for encoding tags. + encode_value: A function which encodes an individual value, e.g. + _EncodeVarint(). + compute_value_size: A function which computes the size of an individual + value, e.g. _VarintSize(). + """ + + def SpecificEncoder(field_number, is_repeated, is_packed): + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + size = 0 + for element in value: + size += compute_value_size(element) + local_EncodeVarint(write, size, deterministic) + for element in value: + encode_value(write, element, deterministic) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, deterministic): + for element in value: + write(tag_bytes) + encode_value(write, element, deterministic) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, deterministic): + write(tag_bytes) + return encode_value(write, value, deterministic) + return EncodeField + + return SpecificEncoder + + +def _ModifiedEncoder(wire_type, encode_value, compute_value_size, modify_value): + """Like SimpleEncoder but additionally invokes modify_value on every value + before passing it to encode_value. Usually modify_value is ZigZagEncode.""" + + def SpecificEncoder(field_number, is_repeated, is_packed): + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + size = 0 + for element in value: + size += compute_value_size(modify_value(element)) + local_EncodeVarint(write, size, deterministic) + for element in value: + encode_value(write, modify_value(element), deterministic) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, deterministic): + for element in value: + write(tag_bytes) + encode_value(write, modify_value(element), deterministic) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, deterministic): + write(tag_bytes) + return encode_value(write, modify_value(value), deterministic) + return EncodeField + + return SpecificEncoder + + +def _StructPackEncoder(wire_type, format): + """Return a constructor for an encoder for a fixed-width field. + + Args: + wire_type: The field's wire type, for encoding tags. + format: The format string to pass to struct.pack(). + """ + + value_size = struct.calcsize(format) + + def SpecificEncoder(field_number, is_repeated, is_packed): + local_struct_pack = struct.pack + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + local_EncodeVarint(write, len(value) * value_size, deterministic) + for element in value: + write(local_struct_pack(format, element)) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, unused_deterministic=None): + for element in value: + write(tag_bytes) + write(local_struct_pack(format, element)) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, unused_deterministic=None): + write(tag_bytes) + return write(local_struct_pack(format, value)) + return EncodeField + + return SpecificEncoder + + +def _FloatingPointEncoder(wire_type, format): + """Return a constructor for an encoder for float fields. + + This is like StructPackEncoder, but catches errors that may be due to + passing non-finite floating-point values to struct.pack, and makes a + second attempt to encode those values. + + Args: + wire_type: The field's wire type, for encoding tags. + format: The format string to pass to struct.pack(). + """ + + value_size = struct.calcsize(format) + if value_size == 4: + def EncodeNonFiniteOrRaise(write, value): + # Remember that the serialized form uses little-endian byte order. + if value == _POS_INF: + write(b'\x00\x00\x80\x7F') + elif value == _NEG_INF: + write(b'\x00\x00\x80\xFF') + elif value != value: # NaN + write(b'\x00\x00\xC0\x7F') + else: + raise + elif value_size == 8: + def EncodeNonFiniteOrRaise(write, value): + if value == _POS_INF: + write(b'\x00\x00\x00\x00\x00\x00\xF0\x7F') + elif value == _NEG_INF: + write(b'\x00\x00\x00\x00\x00\x00\xF0\xFF') + elif value != value: # NaN + write(b'\x00\x00\x00\x00\x00\x00\xF8\x7F') + else: + raise + else: + raise ValueError('Can\'t encode floating-point values that are ' + '%d bytes long (only 4 or 8)' % value_size) + + def SpecificEncoder(field_number, is_repeated, is_packed): + local_struct_pack = struct.pack + if is_packed: + tag_bytes = TagBytes(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED) + local_EncodeVarint = _EncodeVarint + def EncodePackedField(write, value, deterministic): + write(tag_bytes) + local_EncodeVarint(write, len(value) * value_size, deterministic) + for element in value: + # This try/except block is going to be faster than any code that + # we could write to check whether element is finite. + try: + write(local_struct_pack(format, element)) + except SystemError: + EncodeNonFiniteOrRaise(write, element) + return EncodePackedField + elif is_repeated: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeRepeatedField(write, value, unused_deterministic=None): + for element in value: + write(tag_bytes) + try: + write(local_struct_pack(format, element)) + except SystemError: + EncodeNonFiniteOrRaise(write, element) + return EncodeRepeatedField + else: + tag_bytes = TagBytes(field_number, wire_type) + def EncodeField(write, value, unused_deterministic=None): + write(tag_bytes) + try: + write(local_struct_pack(format, value)) + except SystemError: + EncodeNonFiniteOrRaise(write, value) + return EncodeField + + return SpecificEncoder + + +# ==================================================================== +# Here we declare an encoder constructor for each field type. These work +# very similarly to sizer constructors, described earlier. + + +Int32Encoder = Int64Encoder = EnumEncoder = _SimpleEncoder( + wire_format.WIRETYPE_VARINT, _EncodeSignedVarint, _SignedVarintSize) + +UInt32Encoder = UInt64Encoder = _SimpleEncoder( + wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize) + +SInt32Encoder = SInt64Encoder = _ModifiedEncoder( + wire_format.WIRETYPE_VARINT, _EncodeVarint, _VarintSize, + wire_format.ZigZagEncode) + +# Note that Python conveniently guarantees that when using the '<' prefix on +# formats, they will also have the same size across all platforms (as opposed +# to without the prefix, where their sizes depend on the C compiler's basic +# type sizes). +Fixed32Encoder = _StructPackEncoder(wire_format.WIRETYPE_FIXED32, ' str + ValueType = int + + def __init__(self, enum_type): + """Inits EnumTypeWrapper with an EnumDescriptor.""" + self._enum_type = enum_type + self.DESCRIPTOR = enum_type # pylint: disable=invalid-name + + def Name(self, number): # pylint: disable=invalid-name + """Returns a string containing the name of an enum value.""" + try: + return self._enum_type.values_by_number[number].name + except KeyError: + pass # fall out to break exception chaining + + if not isinstance(number, int): + raise TypeError( + 'Enum value for {} must be an int, but got {} {!r}.'.format( + self._enum_type.name, type(number), number)) + else: + # repr here to handle the odd case when you pass in a boolean. + raise ValueError('Enum {} has no name defined for value {!r}'.format( + self._enum_type.name, number)) + + def Value(self, name): # pylint: disable=invalid-name + """Returns the value corresponding to the given enum name.""" + try: + return self._enum_type.values_by_name[name].number + except KeyError: + pass # fall out to break exception chaining + raise ValueError('Enum {} has no value defined for name {!r}'.format( + self._enum_type.name, name)) + + def keys(self): + """Return a list of the string names in the enum. + + Returns: + A list of strs, in the order they were defined in the .proto file. + """ + + return [value_descriptor.name + for value_descriptor in self._enum_type.values] + + def values(self): + """Return a list of the integer values in the enum. + + Returns: + A list of ints, in the order they were defined in the .proto file. + """ + + return [value_descriptor.number + for value_descriptor in self._enum_type.values] + + def items(self): + """Return a list of the (name, value) pairs of the enum. + + Returns: + A list of (str, int) pairs, in the order they were defined + in the .proto file. + """ + return [(value_descriptor.name, value_descriptor.number) + for value_descriptor in self._enum_type.values] + + def __getattr__(self, name): + """Returns the value corresponding to the given enum name.""" + try: + return super( + EnumTypeWrapper, + self).__getattribute__('_enum_type').values_by_name[name].number + except KeyError: + pass # fall out to break exception chaining + raise AttributeError('Enum {} has no value defined for name {!r}'.format( + self._enum_type.name, name)) diff --git a/google/protobuf/internal/extension_dict.py b/google/protobuf/internal/extension_dict.py new file mode 100644 index 0000000..89e64d3 --- /dev/null +++ b/google/protobuf/internal/extension_dict.py @@ -0,0 +1,194 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Contains _ExtensionDict class to represent extensions. +""" + +from google.protobuf.internal import type_checkers +from google.protobuf.descriptor import FieldDescriptor + + +def _VerifyExtensionHandle(message, extension_handle): + """Verify that the given extension handle is valid.""" + + if not isinstance(extension_handle, FieldDescriptor): + raise KeyError('HasExtension() expects an extension handle, got: %s' % + extension_handle) + + if not extension_handle.is_extension: + raise KeyError('"%s" is not an extension.' % extension_handle.full_name) + + if not extension_handle.containing_type: + raise KeyError('"%s" is missing a containing_type.' + % extension_handle.full_name) + + if extension_handle.containing_type is not message.DESCRIPTOR: + raise KeyError('Extension "%s" extends message type "%s", but this ' + 'message is of type "%s".' % + (extension_handle.full_name, + extension_handle.containing_type.full_name, + message.DESCRIPTOR.full_name)) + + +# TODO: Unify error handling of "unknown extension" crap. +# TODO: Support iteritems()-style iteration over all +# extensions with the "has" bits turned on? +class _ExtensionDict(object): + + """Dict-like container for Extension fields on proto instances. + + Note that in all cases we expect extension handles to be + FieldDescriptors. + """ + + def __init__(self, extended_message): + """ + Args: + extended_message: Message instance for which we are the Extensions dict. + """ + self._extended_message = extended_message + + def __getitem__(self, extension_handle): + """Returns the current value of the given extension handle.""" + + _VerifyExtensionHandle(self._extended_message, extension_handle) + + result = self._extended_message._fields.get(extension_handle) + if result is not None: + return result + + if extension_handle.label == FieldDescriptor.LABEL_REPEATED: + result = extension_handle._default_constructor(self._extended_message) + elif extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + message_type = extension_handle.message_type + if not hasattr(message_type, '_concrete_class'): + # pylint: disable=g-import-not-at-top + from google.protobuf import message_factory + message_factory.GetMessageClass(message_type) + if not hasattr(extension_handle.message_type, '_concrete_class'): + from google.protobuf import message_factory + message_factory.GetMessageClass(extension_handle.message_type) + result = extension_handle.message_type._concrete_class() + try: + result._SetListener(self._extended_message._listener_for_children) + except ReferenceError: + pass + else: + # Singular scalar -- just return the default without inserting into the + # dict. + return extension_handle.default_value + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + result = self._extended_message._fields.setdefault( + extension_handle, result) + + return result + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + + my_fields = self._extended_message.ListFields() + other_fields = other._extended_message.ListFields() + + # Get rid of non-extension fields. + my_fields = [field for field in my_fields if field.is_extension] + other_fields = [field for field in other_fields if field.is_extension] + + return my_fields == other_fields + + def __ne__(self, other): + return not self == other + + def __len__(self): + fields = self._extended_message.ListFields() + # Get rid of non-extension fields. + extension_fields = [field for field in fields if field[0].is_extension] + return len(extension_fields) + + def __hash__(self): + raise TypeError('unhashable object') + + # Note that this is only meaningful for non-repeated, scalar extension + # fields. Note also that we may have to call _Modified() when we do + # successfully set a field this way, to set any necessary "has" bits in the + # ancestors of the extended message. + def __setitem__(self, extension_handle, value): + """If extension_handle specifies a non-repeated, scalar extension + field, sets the value of that field. + """ + + _VerifyExtensionHandle(self._extended_message, extension_handle) + + if (extension_handle.label == FieldDescriptor.LABEL_REPEATED or + extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE): + raise TypeError( + 'Cannot assign to extension "%s" because it is a repeated or ' + 'composite type.' % extension_handle.full_name) + + # It's slightly wasteful to lookup the type checker each time, + # but we expect this to be a vanishingly uncommon case anyway. + type_checker = type_checkers.GetTypeChecker(extension_handle) + # pylint: disable=protected-access + self._extended_message._fields[extension_handle] = ( + type_checker.CheckValue(value)) + self._extended_message._Modified() + + def __delitem__(self, extension_handle): + self._extended_message.ClearExtension(extension_handle) + + def _FindExtensionByName(self, name): + """Tries to find a known extension with the specified name. + + Args: + name: Extension full name. + + Returns: + Extension field descriptor. + """ + descriptor = self._extended_message.DESCRIPTOR + extensions = descriptor.file.pool._extensions_by_name[descriptor] + return extensions.get(name, None) + + def _FindExtensionByNumber(self, number): + """Tries to find a known extension with the field number. + + Args: + number: Extension field number. + + Returns: + Extension field descriptor. + """ + descriptor = self._extended_message.DESCRIPTOR + extensions = descriptor.file.pool._extensions_by_number[descriptor] + return extensions.get(number, None) + + def __iter__(self): + # Return a generator over the populated extension fields + return (f[0] for f in self._extended_message.ListFields() + if f[0].is_extension) + + def __contains__(self, extension_handle): + _VerifyExtensionHandle(self._extended_message, extension_handle) + + if extension_handle not in self._extended_message._fields: + return False + + if extension_handle.label == FieldDescriptor.LABEL_REPEATED: + return bool(self._extended_message._fields.get(extension_handle)) + + if extension_handle.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + value = self._extended_message._fields.get(extension_handle) + # pylint: disable=protected-access + return value is not None and value._is_present_in_parent + + return True diff --git a/google/protobuf/internal/field_mask.py b/google/protobuf/internal/field_mask.py new file mode 100644 index 0000000..ae34f08 --- /dev/null +++ b/google/protobuf/internal/field_mask.py @@ -0,0 +1,310 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Contains FieldMask class.""" + +from google.protobuf.descriptor import FieldDescriptor + + +class FieldMask(object): + """Class for FieldMask message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts FieldMask to string according to proto3 JSON spec.""" + camelcase_paths = [] + for path in self.paths: + camelcase_paths.append(_SnakeCaseToCamelCase(path)) + return ','.join(camelcase_paths) + + def FromJsonString(self, value): + """Converts string to FieldMask according to proto3 JSON spec.""" + if not isinstance(value, str): + raise ValueError('FieldMask JSON value not a string: {!r}'.format(value)) + self.Clear() + if value: + for path in value.split(','): + self.paths.append(_CamelCaseToSnakeCase(path)) + + def IsValidForDescriptor(self, message_descriptor): + """Checks whether the FieldMask is valid for Message Descriptor.""" + for path in self.paths: + if not _IsValidPath(message_descriptor, path): + return False + return True + + def AllFieldsFromDescriptor(self, message_descriptor): + """Gets all direct fields of Message Descriptor to FieldMask.""" + self.Clear() + for field in message_descriptor.fields: + self.paths.append(field.name) + + def CanonicalFormFromMask(self, mask): + """Converts a FieldMask to the canonical form. + + Removes paths that are covered by another path. For example, + "foo.bar" is covered by "foo" and will be removed if "foo" + is also in the FieldMask. Then sorts all paths in alphabetical order. + + Args: + mask: The original FieldMask to be converted. + """ + tree = _FieldMaskTree(mask) + tree.ToFieldMask(self) + + def Union(self, mask1, mask2): + """Merges mask1 and mask2 into this FieldMask.""" + _CheckFieldMaskMessage(mask1) + _CheckFieldMaskMessage(mask2) + tree = _FieldMaskTree(mask1) + tree.MergeFromFieldMask(mask2) + tree.ToFieldMask(self) + + def Intersect(self, mask1, mask2): + """Intersects mask1 and mask2 into this FieldMask.""" + _CheckFieldMaskMessage(mask1) + _CheckFieldMaskMessage(mask2) + tree = _FieldMaskTree(mask1) + intersection = _FieldMaskTree() + for path in mask2.paths: + tree.IntersectPath(path, intersection) + intersection.ToFieldMask(self) + + def MergeMessage( + self, source, destination, + replace_message_field=False, replace_repeated_field=False): + """Merges fields specified in FieldMask from source to destination. + + Args: + source: Source message. + destination: The destination message to be merged into. + replace_message_field: Replace message field if True. Merge message + field if False. + replace_repeated_field: Replace repeated field if True. Append + elements of repeated field if False. + """ + tree = _FieldMaskTree(self) + tree.MergeMessage( + source, destination, replace_message_field, replace_repeated_field) + + +def _IsValidPath(message_descriptor, path): + """Checks whether the path is valid for Message Descriptor.""" + parts = path.split('.') + last = parts.pop() + for name in parts: + field = message_descriptor.fields_by_name.get(name) + if (field is None or + field.label == FieldDescriptor.LABEL_REPEATED or + field.type != FieldDescriptor.TYPE_MESSAGE): + return False + message_descriptor = field.message_type + return last in message_descriptor.fields_by_name + + +def _CheckFieldMaskMessage(message): + """Raises ValueError if message is not a FieldMask.""" + message_descriptor = message.DESCRIPTOR + if (message_descriptor.name != 'FieldMask' or + message_descriptor.file.name != 'google/protobuf/field_mask.proto'): + raise ValueError('Message {0} is not a FieldMask.'.format( + message_descriptor.full_name)) + + +def _SnakeCaseToCamelCase(path_name): + """Converts a path name from snake_case to camelCase.""" + result = [] + after_underscore = False + for c in path_name: + if c.isupper(): + raise ValueError( + 'Fail to print FieldMask to Json string: Path name ' + '{0} must not contain uppercase letters.'.format(path_name)) + if after_underscore: + if c.islower(): + result.append(c.upper()) + after_underscore = False + else: + raise ValueError( + 'Fail to print FieldMask to Json string: The ' + 'character after a "_" must be a lowercase letter ' + 'in path name {0}.'.format(path_name)) + elif c == '_': + after_underscore = True + else: + result += c + + if after_underscore: + raise ValueError('Fail to print FieldMask to Json string: Trailing "_" ' + 'in path name {0}.'.format(path_name)) + return ''.join(result) + + +def _CamelCaseToSnakeCase(path_name): + """Converts a field name from camelCase to snake_case.""" + result = [] + for c in path_name: + if c == '_': + raise ValueError('Fail to parse FieldMask: Path name ' + '{0} must not contain "_"s.'.format(path_name)) + if c.isupper(): + result += '_' + result += c.lower() + else: + result += c + return ''.join(result) + + +class _FieldMaskTree(object): + """Represents a FieldMask in a tree structure. + + For example, given a FieldMask "foo.bar,foo.baz,bar.baz", + the FieldMaskTree will be: + [_root] -+- foo -+- bar + | | + | +- baz + | + +- bar --- baz + In the tree, each leaf node represents a field path. + """ + + __slots__ = ('_root',) + + def __init__(self, field_mask=None): + """Initializes the tree by FieldMask.""" + self._root = {} + if field_mask: + self.MergeFromFieldMask(field_mask) + + def MergeFromFieldMask(self, field_mask): + """Merges a FieldMask to the tree.""" + for path in field_mask.paths: + self.AddPath(path) + + def AddPath(self, path): + """Adds a field path into the tree. + + If the field path to add is a sub-path of an existing field path + in the tree (i.e., a leaf node), it means the tree already matches + the given path so nothing will be added to the tree. If the path + matches an existing non-leaf node in the tree, that non-leaf node + will be turned into a leaf node with all its children removed because + the path matches all the node's children. Otherwise, a new path will + be added. + + Args: + path: The field path to add. + """ + node = self._root + for name in path.split('.'): + if name not in node: + node[name] = {} + elif not node[name]: + # Pre-existing empty node implies we already have this entire tree. + return + node = node[name] + # Remove any sub-trees we might have had. + node.clear() + + def ToFieldMask(self, field_mask): + """Converts the tree to a FieldMask.""" + field_mask.Clear() + _AddFieldPaths(self._root, '', field_mask) + + def IntersectPath(self, path, intersection): + """Calculates the intersection part of a field path with this tree. + + Args: + path: The field path to calculates. + intersection: The out tree to record the intersection part. + """ + node = self._root + for name in path.split('.'): + if name not in node: + return + elif not node[name]: + intersection.AddPath(path) + return + node = node[name] + intersection.AddLeafNodes(path, node) + + def AddLeafNodes(self, prefix, node): + """Adds leaf nodes begin with prefix to this tree.""" + if not node: + self.AddPath(prefix) + for name in node: + child_path = prefix + '.' + name + self.AddLeafNodes(child_path, node[name]) + + def MergeMessage( + self, source, destination, + replace_message, replace_repeated): + """Merge all fields specified by this tree from source to destination.""" + _MergeMessage( + self._root, source, destination, replace_message, replace_repeated) + + +def _StrConvert(value): + """Converts value to str if it is not.""" + # This file is imported by c extension and some methods like ClearField + # requires string for the field name. py2/py3 has different text + # type and may use unicode. + if not isinstance(value, str): + return value.encode('utf-8') + return value + + +def _MergeMessage( + node, source, destination, replace_message, replace_repeated): + """Merge all fields specified by a sub-tree from source to destination.""" + source_descriptor = source.DESCRIPTOR + for name in node: + child = node[name] + field = source_descriptor.fields_by_name[name] + if field is None: + raise ValueError('Error: Can\'t find field {0} in message {1}.'.format( + name, source_descriptor.full_name)) + if child: + # Sub-paths are only allowed for singular message fields. + if (field.label == FieldDescriptor.LABEL_REPEATED or + field.cpp_type != FieldDescriptor.CPPTYPE_MESSAGE): + raise ValueError('Error: Field {0} in message {1} is not a singular ' + 'message field and cannot have sub-fields.'.format( + name, source_descriptor.full_name)) + if source.HasField(name): + _MergeMessage( + child, getattr(source, name), getattr(destination, name), + replace_message, replace_repeated) + continue + if field.label == FieldDescriptor.LABEL_REPEATED: + if replace_repeated: + destination.ClearField(_StrConvert(name)) + repeated_source = getattr(source, name) + repeated_destination = getattr(destination, name) + repeated_destination.MergeFrom(repeated_source) + else: + if field.cpp_type == FieldDescriptor.CPPTYPE_MESSAGE: + if replace_message: + destination.ClearField(_StrConvert(name)) + if source.HasField(name): + getattr(destination, name).MergeFrom(getattr(source, name)) + else: + setattr(destination, name, getattr(source, name)) + + +def _AddFieldPaths(node, prefix, field_mask): + """Adds the field paths descended from node to field_mask.""" + if not node and prefix: + field_mask.paths.append(prefix) + return + for name in sorted(node): + if prefix: + child_path = prefix + '.' + name + else: + child_path = name + _AddFieldPaths(node[name], child_path, field_mask) diff --git a/google/protobuf/internal/message_listener.py b/google/protobuf/internal/message_listener.py new file mode 100644 index 0000000..ff1c127 --- /dev/null +++ b/google/protobuf/internal/message_listener.py @@ -0,0 +1,55 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Defines a listener interface for observing certain +state transitions on Message objects. + +Also defines a null implementation of this interface. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + + +class MessageListener(object): + + """Listens for modifications made to a message. Meant to be registered via + Message._SetListener(). + + Attributes: + dirty: If True, then calling Modified() would be a no-op. This can be + used to avoid these calls entirely in the common case. + """ + + def Modified(self): + """Called every time the message is modified in such a way that the parent + message may need to be updated. This currently means either: + (a) The message was modified for the first time, so the parent message + should henceforth mark the message as present. + (b) The message's cached byte size became dirty -- i.e. the message was + modified for the first time after a previous call to ByteSize(). + Therefore the parent should also mark its byte size as dirty. + Note that (a) implies (b), since new objects start out with a client cached + size (zero). However, we document (a) explicitly because it is important. + + Modified() will *only* be called in response to one of these two events -- + not every time the sub-message is modified. + + Note that if the listener's |dirty| attribute is true, then calling + Modified at the moment would be a no-op, so it can be skipped. Performance- + sensitive callers should check this attribute directly before calling since + it will be true most of the time. + """ + + raise NotImplementedError + + +class NullMessageListener(object): + + """No-op MessageListener implementation.""" + + def Modified(self): + pass diff --git a/google/protobuf/internal/python_edition_defaults.py b/google/protobuf/internal/python_edition_defaults.py new file mode 100644 index 0000000..57cb98a --- /dev/null +++ b/google/protobuf/internal/python_edition_defaults.py @@ -0,0 +1,5 @@ +""" +This file contains the serialized FeatureSetDefaults object corresponding to +the Pure Python runtime. This is used for feature resolution under Editions. +""" +_PROTOBUF_INTERNAL_PYTHON_EDITION_DEFAULTS = b"\n\023\030\346\007\"\000*\014\010\001\020\002\030\002 \003(\0010\002\n\023\030\347\007\"\000*\014\010\002\020\001\030\001 \002(\0010\001\n\023\030\350\007\"\014\010\001\020\001\030\001 \002(\0010\001*\000 \346\007(\350\007" diff --git a/google/protobuf/internal/python_message.py b/google/protobuf/internal/python_message.py new file mode 100644 index 0000000..34efbfe --- /dev/null +++ b/google/protobuf/internal/python_message.py @@ -0,0 +1,1508 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +# This code is meant to work on Python 2.4 and above only. +# +# TODO: Helpers for verbose, common checks like seeing if a +# descriptor's cpp_type is CPPTYPE_MESSAGE. + +"""Contains a metaclass and helper functions used to create +protocol message classes from Descriptor objects at runtime. + +Recall that a metaclass is the "type" of a class. +(A class is to a metaclass what an instance is to a class.) + +In this case, we use the GeneratedProtocolMessageType metaclass +to inject all the useful functionality into the classes +output by the protocol compiler at compile-time. + +The upshot of all this is that the real implementation +details for ALL pure-Python protocol buffers are *here in +this file*. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +from io import BytesIO +import struct +import sys +import warnings +import weakref + +from google.protobuf import descriptor as descriptor_mod +from google.protobuf import message as message_mod +from google.protobuf import text_format +# We use "as" to avoid name collisions with variables. +from google.protobuf.internal import api_implementation +from google.protobuf.internal import containers +from google.protobuf.internal import decoder +from google.protobuf.internal import encoder +from google.protobuf.internal import enum_type_wrapper +from google.protobuf.internal import extension_dict +from google.protobuf.internal import message_listener as message_listener_mod +from google.protobuf.internal import type_checkers +from google.protobuf.internal import well_known_types +from google.protobuf.internal import wire_format + +_FieldDescriptor = descriptor_mod.FieldDescriptor +_AnyFullTypeName = 'google.protobuf.Any' +_ExtensionDict = extension_dict._ExtensionDict + +class GeneratedProtocolMessageType(type): + + """Metaclass for protocol message classes created at runtime from Descriptors. + + We add implementations for all methods described in the Message class. We + also create properties to allow getting/setting all fields in the protocol + message. Finally, we create slots to prevent users from accidentally + "setting" nonexistent fields in the protocol message, which then wouldn't get + serialized / deserialized properly. + + The protocol compiler currently uses this metaclass to create protocol + message classes at runtime. Clients can also manually create their own + classes at runtime, as in this example: + + mydescriptor = Descriptor(.....) + factory = symbol_database.Default() + factory.pool.AddDescriptor(mydescriptor) + MyProtoClass = factory.GetPrototype(mydescriptor) + myproto_instance = MyProtoClass() + myproto.foo_field = 23 + ... + """ + + # Must be consistent with the protocol-compiler code in + # proto2/compiler/internal/generator.*. + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __new__(cls, name, bases, dictionary): + """Custom allocation for runtime-generated class types. + + We override __new__ because this is apparently the only place + where we can meaningfully set __slots__ on the class we're creating(?). + (The interplay between metaclasses and slots is not very well-documented). + + Args: + name: Name of the class (ignored, but required by the + metaclass protocol). + bases: Base classes of the class we're constructing. + (Should be message.Message). We ignore this field, but + it's required by the metaclass protocol + dictionary: The class dictionary of the class we're + constructing. dictionary[_DESCRIPTOR_KEY] must contain + a Descriptor object describing this protocol message + type. + + Returns: + Newly-allocated class. + + Raises: + RuntimeError: Generated code only work with python cpp extension. + """ + descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] + + if isinstance(descriptor, str): + raise RuntimeError('The generated code only work with python cpp ' + 'extension, but it is using pure python runtime.') + + # If a concrete class already exists for this descriptor, don't try to + # create another. Doing so will break any messages that already exist with + # the existing class. + # + # The C++ implementation appears to have its own internal `PyMessageFactory` + # to achieve similar results. + # + # This most commonly happens in `text_format.py` when using descriptors from + # a custom pool; it calls symbol_database.Global().getPrototype() on a + # descriptor which already has an existing concrete class. + new_class = getattr(descriptor, '_concrete_class', None) + if new_class: + return new_class + + if descriptor.full_name in well_known_types.WKTBASES: + bases += (well_known_types.WKTBASES[descriptor.full_name],) + _AddClassAttributesForNestedExtensions(descriptor, dictionary) + _AddSlots(descriptor, dictionary) + + superclass = super(GeneratedProtocolMessageType, cls) + new_class = superclass.__new__(cls, name, bases, dictionary) + return new_class + + def __init__(cls, name, bases, dictionary): + """Here we perform the majority of our work on the class. + We add enum getters, an __init__ method, implementations + of all Message methods, and properties for all fields + in the protocol type. + + Args: + name: Name of the class (ignored, but required by the + metaclass protocol). + bases: Base classes of the class we're constructing. + (Should be message.Message). We ignore this field, but + it's required by the metaclass protocol + dictionary: The class dictionary of the class we're + constructing. dictionary[_DESCRIPTOR_KEY] must contain + a Descriptor object describing this protocol message + type. + """ + descriptor = dictionary[GeneratedProtocolMessageType._DESCRIPTOR_KEY] + + # If this is an _existing_ class looked up via `_concrete_class` in the + # __new__ method above, then we don't need to re-initialize anything. + existing_class = getattr(descriptor, '_concrete_class', None) + if existing_class: + assert existing_class is cls, ( + 'Duplicate `GeneratedProtocolMessageType` created for descriptor %r' + % (descriptor.full_name)) + return + + cls._message_set_decoders_by_tag = {} + cls._fields_by_tag = {} + if (descriptor.has_options and + descriptor.GetOptions().message_set_wire_format): + cls._message_set_decoders_by_tag[decoder.MESSAGE_SET_ITEM_TAG] = ( + decoder.MessageSetItemDecoder(descriptor), + None, + ) + + # Attach stuff to each FieldDescriptor for quick lookup later on. + for field in descriptor.fields: + _AttachFieldHelpers(cls, field) + + if descriptor.is_extendable and hasattr(descriptor.file, 'pool'): + extensions = descriptor.file.pool.FindAllExtensions(descriptor) + for ext in extensions: + _AttachFieldHelpers(cls, ext) + + descriptor._concrete_class = cls # pylint: disable=protected-access + _AddEnumValues(descriptor, cls) + _AddInitMethod(descriptor, cls) + _AddPropertiesForFields(descriptor, cls) + _AddPropertiesForExtensions(descriptor, cls) + _AddStaticMethods(cls) + _AddMessageMethods(descriptor, cls) + _AddPrivateHelperMethods(descriptor, cls) + + superclass = super(GeneratedProtocolMessageType, cls) + superclass.__init__(name, bases, dictionary) + + +# Stateless helpers for GeneratedProtocolMessageType below. +# Outside clients should not access these directly. +# +# I opted not to make any of these methods on the metaclass, to make it more +# clear that I'm not really using any state there and to keep clients from +# thinking that they have direct access to these construction helpers. + + +def _PropertyName(proto_field_name): + """Returns the name of the public property attribute which + clients can use to get and (in some cases) set the value + of a protocol message field. + + Args: + proto_field_name: The protocol message field name, exactly + as it appears (or would appear) in a .proto file. + """ + # TODO: Escape Python keywords (e.g., yield), and test this support. + # nnorwitz makes my day by writing: + # """ + # FYI. See the keyword module in the stdlib. This could be as simple as: + # + # if keyword.iskeyword(proto_field_name): + # return proto_field_name + "_" + # return proto_field_name + # """ + # Kenton says: The above is a BAD IDEA. People rely on being able to use + # getattr() and setattr() to reflectively manipulate field values. If we + # rename the properties, then every such user has to also make sure to apply + # the same transformation. Note that currently if you name a field "yield", + # you can still access it just fine using getattr/setattr -- it's not even + # that cumbersome to do so. + # TODO: Remove this method entirely if/when everyone agrees with my + # position. + return proto_field_name + + +def _AddSlots(message_descriptor, dictionary): + """Adds a __slots__ entry to dictionary, containing the names of all valid + attributes for this message type. + + Args: + message_descriptor: A Descriptor instance describing this message type. + dictionary: Class dictionary to which we'll add a '__slots__' entry. + """ + dictionary['__slots__'] = ['_cached_byte_size', + '_cached_byte_size_dirty', + '_fields', + '_unknown_fields', + '_is_present_in_parent', + '_listener', + '_listener_for_children', + '__weakref__', + '_oneofs'] + + +def _IsMessageSetExtension(field): + return (field.is_extension and + field.containing_type.has_options and + field.containing_type.GetOptions().message_set_wire_format and + field.type == _FieldDescriptor.TYPE_MESSAGE and + field.label == _FieldDescriptor.LABEL_OPTIONAL) + + +def _IsMapField(field): + return (field.type == _FieldDescriptor.TYPE_MESSAGE and + field.message_type._is_map_entry) + + +def _IsMessageMapField(field): + value_type = field.message_type.fields_by_name['value'] + return value_type.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE + +def _AttachFieldHelpers(cls, field_descriptor): + is_repeated = field_descriptor.label == _FieldDescriptor.LABEL_REPEATED + field_descriptor._default_constructor = _DefaultValueConstructorForField( + field_descriptor + ) + + def AddFieldByTag(wiretype, is_packed): + tag_bytes = encoder.TagBytes(field_descriptor.number, wiretype) + cls._fields_by_tag[tag_bytes] = (field_descriptor, is_packed) + + AddFieldByTag( + type_checkers.FIELD_TYPE_TO_WIRE_TYPE[field_descriptor.type], False + ) + + if is_repeated and wire_format.IsTypePackable(field_descriptor.type): + # To support wire compatibility of adding packed = true, add a decoder for + # packed values regardless of the field's options. + AddFieldByTag(wire_format.WIRETYPE_LENGTH_DELIMITED, True) + + +def _MaybeAddEncoder(cls, field_descriptor): + if hasattr(field_descriptor, '_encoder'): + return + is_repeated = (field_descriptor.label == _FieldDescriptor.LABEL_REPEATED) + is_map_entry = _IsMapField(field_descriptor) + is_packed = field_descriptor.is_packed + + if is_map_entry: + field_encoder = encoder.MapEncoder(field_descriptor) + sizer = encoder.MapSizer(field_descriptor, + _IsMessageMapField(field_descriptor)) + elif _IsMessageSetExtension(field_descriptor): + field_encoder = encoder.MessageSetItemEncoder(field_descriptor.number) + sizer = encoder.MessageSetItemSizer(field_descriptor.number) + else: + field_encoder = type_checkers.TYPE_TO_ENCODER[field_descriptor.type]( + field_descriptor.number, is_repeated, is_packed) + sizer = type_checkers.TYPE_TO_SIZER[field_descriptor.type]( + field_descriptor.number, is_repeated, is_packed) + + field_descriptor._sizer = sizer + field_descriptor._encoder = field_encoder + + +def _MaybeAddDecoder(cls, field_descriptor): + if hasattr(field_descriptor, '_decoders'): + return + + is_repeated = field_descriptor.label == _FieldDescriptor.LABEL_REPEATED + is_map_entry = _IsMapField(field_descriptor) + helper_decoders = {} + + def AddDecoder(is_packed): + decode_type = field_descriptor.type + if (decode_type == _FieldDescriptor.TYPE_ENUM and + not field_descriptor.enum_type.is_closed): + decode_type = _FieldDescriptor.TYPE_INT32 + + oneof_descriptor = None + if field_descriptor.containing_oneof is not None: + oneof_descriptor = field_descriptor + + if is_map_entry: + is_message_map = _IsMessageMapField(field_descriptor) + + field_decoder = decoder.MapDecoder( + field_descriptor, _GetInitializeDefaultForMap(field_descriptor), + is_message_map) + elif decode_type == _FieldDescriptor.TYPE_STRING: + field_decoder = decoder.StringDecoder( + field_descriptor.number, is_repeated, is_packed, + field_descriptor, field_descriptor._default_constructor, + not field_descriptor.has_presence) + elif field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( + field_descriptor.number, is_repeated, is_packed, + field_descriptor, field_descriptor._default_constructor) + else: + field_decoder = type_checkers.TYPE_TO_DECODER[decode_type]( + field_descriptor.number, is_repeated, is_packed, + # pylint: disable=protected-access + field_descriptor, field_descriptor._default_constructor, + not field_descriptor.has_presence) + + helper_decoders[is_packed] = field_decoder + + AddDecoder(False) + + if is_repeated and wire_format.IsTypePackable(field_descriptor.type): + # To support wire compatibility of adding packed = true, add a decoder for + # packed values regardless of the field's options. + AddDecoder(True) + + field_descriptor._decoders = helper_decoders + + +def _AddClassAttributesForNestedExtensions(descriptor, dictionary): + extensions = descriptor.extensions_by_name + for extension_name, extension_field in extensions.items(): + assert extension_name not in dictionary + dictionary[extension_name] = extension_field + + +def _AddEnumValues(descriptor, cls): + """Sets class-level attributes for all enum fields defined in this message. + + Also exporting a class-level object that can name enum values. + + Args: + descriptor: Descriptor object for this message type. + cls: Class we're constructing for this message type. + """ + for enum_type in descriptor.enum_types: + setattr(cls, enum_type.name, enum_type_wrapper.EnumTypeWrapper(enum_type)) + for enum_value in enum_type.values: + setattr(cls, enum_value.name, enum_value.number) + + +def _GetInitializeDefaultForMap(field): + if field.label != _FieldDescriptor.LABEL_REPEATED: + raise ValueError('map_entry set on non-repeated field %s' % ( + field.name)) + fields_by_name = field.message_type.fields_by_name + key_checker = type_checkers.GetTypeChecker(fields_by_name['key']) + + value_field = fields_by_name['value'] + if _IsMessageMapField(field): + def MakeMessageMapDefault(message): + return containers.MessageMap( + message._listener_for_children, value_field.message_type, key_checker, + field.message_type) + return MakeMessageMapDefault + else: + value_checker = type_checkers.GetTypeChecker(value_field) + def MakePrimitiveMapDefault(message): + return containers.ScalarMap( + message._listener_for_children, key_checker, value_checker, + field.message_type) + return MakePrimitiveMapDefault + +def _DefaultValueConstructorForField(field): + """Returns a function which returns a default value for a field. + + Args: + field: FieldDescriptor object for this field. + + The returned function has one argument: + message: Message instance containing this field, or a weakref proxy + of same. + + That function in turn returns a default value for this field. The default + value may refer back to |message| via a weak reference. + """ + + if _IsMapField(field): + return _GetInitializeDefaultForMap(field) + + if field.label == _FieldDescriptor.LABEL_REPEATED: + if field.has_default_value and field.default_value != []: + raise ValueError('Repeated field default value not empty list: %s' % ( + field.default_value)) + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + # We can't look at _concrete_class yet since it might not have + # been set. (Depends on order in which we initialize the classes). + message_type = field.message_type + def MakeRepeatedMessageDefault(message): + return containers.RepeatedCompositeFieldContainer( + message._listener_for_children, field.message_type) + return MakeRepeatedMessageDefault + else: + type_checker = type_checkers.GetTypeChecker(field) + def MakeRepeatedScalarDefault(message): + return containers.RepeatedScalarFieldContainer( + message._listener_for_children, type_checker) + return MakeRepeatedScalarDefault + + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + message_type = field.message_type + def MakeSubMessageDefault(message): + # _concrete_class may not yet be initialized. + if not hasattr(message_type, '_concrete_class'): + from google.protobuf import message_factory + message_factory.GetMessageClass(message_type) + result = message_type._concrete_class() + result._SetListener( + _OneofListener(message, field) + if field.containing_oneof is not None + else message._listener_for_children) + return result + return MakeSubMessageDefault + + def MakeScalarDefault(message): + # TODO: This may be broken since there may not be + # default_value. Combine with has_default_value somehow. + return field.default_value + return MakeScalarDefault + + +def _ReraiseTypeErrorWithFieldName(message_name, field_name): + """Re-raise the currently-handled TypeError with the field name added.""" + exc = sys.exc_info()[1] + if len(exc.args) == 1 and type(exc) is TypeError: + # simple TypeError; add field name to exception message + exc = TypeError('%s for field %s.%s' % (str(exc), message_name, field_name)) + + # re-raise possibly-amended exception with original traceback: + raise exc.with_traceback(sys.exc_info()[2]) + + +def _AddInitMethod(message_descriptor, cls): + """Adds an __init__ method to cls.""" + + def _GetIntegerEnumValue(enum_type, value): + """Convert a string or integer enum value to an integer. + + If the value is a string, it is converted to the enum value in + enum_type with the same name. If the value is not a string, it's + returned as-is. (No conversion or bounds-checking is done.) + """ + if isinstance(value, str): + try: + return enum_type.values_by_name[value].number + except KeyError: + raise ValueError('Enum type %s: unknown label "%s"' % ( + enum_type.full_name, value)) + return value + + def init(self, **kwargs): + self._cached_byte_size = 0 + self._cached_byte_size_dirty = len(kwargs) > 0 + self._fields = {} + # Contains a mapping from oneof field descriptors to the descriptor + # of the currently set field in that oneof field. + self._oneofs = {} + + # _unknown_fields is () when empty for efficiency, and will be turned into + # a list if fields are added. + self._unknown_fields = () + self._is_present_in_parent = False + self._listener = message_listener_mod.NullMessageListener() + self._listener_for_children = _Listener(self) + for field_name, field_value in kwargs.items(): + field = _GetFieldByName(message_descriptor, field_name) + if field is None: + raise TypeError('%s() got an unexpected keyword argument "%s"' % + (message_descriptor.name, field_name)) + if field_value is None: + # field=None is the same as no field at all. + continue + if field.label == _FieldDescriptor.LABEL_REPEATED: + copy = field._default_constructor(self) + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: # Composite + if _IsMapField(field): + if _IsMessageMapField(field): + for key in field_value: + copy[key].MergeFrom(field_value[key]) + else: + copy.update(field_value) + else: + for val in field_value: + if isinstance(val, dict): + copy.add(**val) + else: + copy.add().MergeFrom(val) + else: # Scalar + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + field_value = [_GetIntegerEnumValue(field.enum_type, val) + for val in field_value] + copy.extend(field_value) + self._fields[field] = copy + elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + copy = field._default_constructor(self) + new_val = field_value + if isinstance(field_value, dict): + new_val = field.message_type._concrete_class(**field_value) + try: + copy.MergeFrom(new_val) + except TypeError: + _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) + self._fields[field] = copy + else: + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + field_value = _GetIntegerEnumValue(field.enum_type, field_value) + try: + setattr(self, field_name, field_value) + except TypeError: + _ReraiseTypeErrorWithFieldName(message_descriptor.name, field_name) + + init.__module__ = None + init.__doc__ = None + cls.__init__ = init + + +def _GetFieldByName(message_descriptor, field_name): + """Returns a field descriptor by field name. + + Args: + message_descriptor: A Descriptor describing all fields in message. + field_name: The name of the field to retrieve. + Returns: + The field descriptor associated with the field name. + """ + try: + return message_descriptor.fields_by_name[field_name] + except KeyError: + raise ValueError('Protocol message %s has no "%s" field.' % + (message_descriptor.name, field_name)) + + +def _AddPropertiesForFields(descriptor, cls): + """Adds properties for all fields in this protocol message type.""" + for field in descriptor.fields: + _AddPropertiesForField(field, cls) + + if descriptor.is_extendable: + # _ExtensionDict is just an adaptor with no state so we allocate a new one + # every time it is accessed. + cls.Extensions = property(lambda self: _ExtensionDict(self)) + + +def _AddPropertiesForField(field, cls): + """Adds a public property for a protocol message field. + Clients can use this property to get and (in the case + of non-repeated scalar fields) directly set the value + of a protocol message field. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + # Catch it if we add other types that we should + # handle specially here. + assert _FieldDescriptor.MAX_CPPTYPE == 10 + + constant_name = field.name.upper() + '_FIELD_NUMBER' + setattr(cls, constant_name, field.number) + + if field.label == _FieldDescriptor.LABEL_REPEATED: + _AddPropertiesForRepeatedField(field, cls) + elif field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + _AddPropertiesForNonRepeatedCompositeField(field, cls) + else: + _AddPropertiesForNonRepeatedScalarField(field, cls) + + +class _FieldProperty(property): + __slots__ = ('DESCRIPTOR',) + + def __init__(self, descriptor, getter, setter, doc): + property.__init__(self, getter, setter, doc=doc) + self.DESCRIPTOR = descriptor + + +def _AddPropertiesForRepeatedField(field, cls): + """Adds a public property for a "repeated" protocol message field. Clients + can use this property to get the value of the field, which will be either a + RepeatedScalarFieldContainer or RepeatedCompositeFieldContainer (see + below). + + Note that when clients add values to these containers, we perform + type-checking in the case of repeated scalar fields, and we also set any + necessary "has" bits as a side-effect. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + + def getter(self): + field_value = self._fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + field_value = self._fields.setdefault(field, field_value) + return field_value + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + # We define a setter just so we can throw an exception with a more + # helpful error message. + def setter(self, new_value): + raise AttributeError('Assignment not allowed to repeated field ' + '"%s" in protocol message object.' % proto_field_name) + + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForNonRepeatedScalarField(field, cls): + """Adds a public property for a nonrepeated, scalar protocol message field. + Clients can use this property to get and directly set the value of the field. + Note that when the client sets the value of a field by using this property, + all necessary "has" bits are set as a side-effect, and we also perform + type-checking. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + type_checker = type_checkers.GetTypeChecker(field) + default_value = field.default_value + + def getter(self): + # TODO: This may be broken since there may not be + # default_value. Combine with has_default_value somehow. + return self._fields.get(field, default_value) + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + def field_setter(self, new_value): + # pylint: disable=protected-access + # Testing the value for truthiness captures all of the proto3 defaults + # (0, 0.0, enum 0, and False). + try: + new_value = type_checker.CheckValue(new_value) + except TypeError as e: + raise TypeError( + 'Cannot set %s to %.1024r: %s' % (field.full_name, new_value, e)) + if not field.has_presence and not new_value: + self._fields.pop(field, None) + else: + self._fields[field] = new_value + # Check _cached_byte_size_dirty inline to improve performance, since scalar + # setters are called frequently. + if not self._cached_byte_size_dirty: + self._Modified() + + if field.containing_oneof: + def setter(self, new_value): + field_setter(self, new_value) + self._UpdateOneofState(field) + else: + setter = field_setter + + setter.__module__ = None + setter.__doc__ = 'Setter for %s.' % proto_field_name + + # Add a property to encapsulate the getter/setter. + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForNonRepeatedCompositeField(field, cls): + """Adds a public property for a nonrepeated, composite protocol message field. + A composite field is a "group" or "message" field. + + Clients can use this property to get the value of the field, but cannot + assign to the property directly. + + Args: + field: A FieldDescriptor for this field. + cls: The class we're constructing. + """ + # TODO: Remove duplication with similar method + # for non-repeated scalars. + proto_field_name = field.name + property_name = _PropertyName(proto_field_name) + + def getter(self): + field_value = self._fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + + # Atomically check if another thread has preempted us and, if not, swap + # in the new object we just created. If someone has preempted us, we + # take that object and discard ours. + # WARNING: We are relying on setdefault() being atomic. This is true + # in CPython but we haven't investigated others. This warning appears + # in several other locations in this file. + field_value = self._fields.setdefault(field, field_value) + return field_value + getter.__module__ = None + getter.__doc__ = 'Getter for %s.' % proto_field_name + + # We define a setter just so we can throw an exception with a more + # helpful error message. + def setter(self, new_value): + raise AttributeError('Assignment not allowed to composite field ' + '"%s" in protocol message object.' % proto_field_name) + + # Add a property to encapsulate the getter. + doc = 'Magic attribute generated for "%s" proto field.' % proto_field_name + setattr(cls, property_name, _FieldProperty(field, getter, setter, doc=doc)) + + +def _AddPropertiesForExtensions(descriptor, cls): + """Adds properties for all fields in this protocol message type.""" + extensions = descriptor.extensions_by_name + for extension_name, extension_field in extensions.items(): + constant_name = extension_name.upper() + '_FIELD_NUMBER' + setattr(cls, constant_name, extension_field.number) + + # TODO: Migrate all users of these attributes to functions like + # pool.FindExtensionByNumber(descriptor). + if descriptor.file is not None: + # TODO: Use cls.MESSAGE_FACTORY.pool when available. + pool = descriptor.file.pool + +def _AddStaticMethods(cls): + def FromString(s): + message = cls() + message.MergeFromString(s) + return message + cls.FromString = staticmethod(FromString) + + +def _IsPresent(item): + """Given a (FieldDescriptor, value) tuple from _fields, return true if the + value should be included in the list returned by ListFields().""" + + if item[0].label == _FieldDescriptor.LABEL_REPEATED: + return bool(item[1]) + elif item[0].cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + return item[1]._is_present_in_parent + else: + return True + + +def _AddListFieldsMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def ListFields(self): + all_fields = [item for item in self._fields.items() if _IsPresent(item)] + all_fields.sort(key = lambda item: item[0].number) + return all_fields + + cls.ListFields = ListFields + + +def _AddHasFieldMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + hassable_fields = {} + for field in message_descriptor.fields: + if field.label == _FieldDescriptor.LABEL_REPEATED: + continue + # For proto3, only submessages and fields inside a oneof have presence. + if not field.has_presence: + continue + hassable_fields[field.name] = field + + # Has methods are supported for oneof descriptors. + for oneof in message_descriptor.oneofs: + hassable_fields[oneof.name] = oneof + + def HasField(self, field_name): + try: + field = hassable_fields[field_name] + except KeyError as exc: + raise ValueError('Protocol message %s has no non-repeated field "%s" ' + 'nor has presence is not available for this field.' % ( + message_descriptor.full_name, field_name)) from exc + + if isinstance(field, descriptor_mod.OneofDescriptor): + try: + return HasField(self, self._oneofs[field].name) + except KeyError: + return False + else: + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + value = self._fields.get(field) + return value is not None and value._is_present_in_parent + else: + return field in self._fields + + cls.HasField = HasField + + +def _AddClearFieldMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def ClearField(self, field_name): + try: + field = message_descriptor.fields_by_name[field_name] + except KeyError: + try: + field = message_descriptor.oneofs_by_name[field_name] + if field in self._oneofs: + field = self._oneofs[field] + else: + return + except KeyError: + raise ValueError('Protocol message %s has no "%s" field.' % + (message_descriptor.name, field_name)) + + if field in self._fields: + # To match the C++ implementation, we need to invalidate iterators + # for map fields when ClearField() happens. + if hasattr(self._fields[field], 'InvalidateIterators'): + self._fields[field].InvalidateIterators() + + # Note: If the field is a sub-message, its listener will still point + # at us. That's fine, because the worst than can happen is that it + # will call _Modified() and invalidate our byte size. Big deal. + del self._fields[field] + + if self._oneofs.get(field.containing_oneof, None) is field: + del self._oneofs[field.containing_oneof] + + # Always call _Modified() -- even if nothing was changed, this is + # a mutating method, and thus calling it should cause the field to become + # present in the parent message. + self._Modified() + + cls.ClearField = ClearField + + +def _AddClearExtensionMethod(cls): + """Helper for _AddMessageMethods().""" + def ClearExtension(self, field_descriptor): + extension_dict._VerifyExtensionHandle(self, field_descriptor) + + # Similar to ClearField(), above. + if field_descriptor in self._fields: + del self._fields[field_descriptor] + self._Modified() + cls.ClearExtension = ClearExtension + + +def _AddHasExtensionMethod(cls): + """Helper for _AddMessageMethods().""" + def HasExtension(self, field_descriptor): + extension_dict._VerifyExtensionHandle(self, field_descriptor) + if field_descriptor.label == _FieldDescriptor.LABEL_REPEATED: + raise KeyError('"%s" is repeated.' % field_descriptor.full_name) + + if field_descriptor.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + value = self._fields.get(field_descriptor) + return value is not None and value._is_present_in_parent + else: + return field_descriptor in self._fields + cls.HasExtension = HasExtension + +def _InternalUnpackAny(msg): + """Unpacks Any message and returns the unpacked message. + + This internal method is different from public Any Unpack method which takes + the target message as argument. _InternalUnpackAny method does not have + target message type and need to find the message type in descriptor pool. + + Args: + msg: An Any message to be unpacked. + + Returns: + The unpacked message. + """ + # TODO: Don't use the factory of generated messages. + # To make Any work with custom factories, use the message factory of the + # parent message. + # pylint: disable=g-import-not-at-top + from google.protobuf import symbol_database + factory = symbol_database.Default() + + type_url = msg.type_url + + if not type_url: + return None + + # TODO: For now we just strip the hostname. Better logic will be + # required. + type_name = type_url.split('/')[-1] + descriptor = factory.pool.FindMessageTypeByName(type_name) + + if descriptor is None: + return None + + message_class = factory.GetPrototype(descriptor) + message = message_class() + + message.ParseFromString(msg.value) + return message + + +def _AddEqualsMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __eq__(self, other): + if (not isinstance(other, message_mod.Message) or + other.DESCRIPTOR != self.DESCRIPTOR): + return NotImplemented + + if self is other: + return True + + if self.DESCRIPTOR.full_name == _AnyFullTypeName: + any_a = _InternalUnpackAny(self) + any_b = _InternalUnpackAny(other) + if any_a and any_b: + return any_a == any_b + + if not self.ListFields() == other.ListFields(): + return False + + # TODO: Fix UnknownFieldSet to consider MessageSet extensions, + # then use it for the comparison. + unknown_fields = list(self._unknown_fields) + unknown_fields.sort() + other_unknown_fields = list(other._unknown_fields) + other_unknown_fields.sort() + return unknown_fields == other_unknown_fields + + cls.__eq__ = __eq__ + + +def _AddStrMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __str__(self): + return text_format.MessageToString(self) + cls.__str__ = __str__ + + +def _AddReprMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def __repr__(self): + return text_format.MessageToString(self) + cls.__repr__ = __repr__ + + +def _AddUnicodeMethod(unused_message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def __unicode__(self): + return text_format.MessageToString(self, as_utf8=True).decode('utf-8') + cls.__unicode__ = __unicode__ + + +def _BytesForNonRepeatedElement(value, field_number, field_type): + """Returns the number of bytes needed to serialize a non-repeated element. + The returned byte count includes space for tag information and any + other additional space associated with serializing value. + + Args: + value: Value we're serializing. + field_number: Field number of this value. (Since the field number + is stored as part of a varint-encoded tag, this has an impact + on the total bytes required to serialize the value). + field_type: The type of the field. One of the TYPE_* constants + within FieldDescriptor. + """ + try: + fn = type_checkers.TYPE_TO_BYTE_SIZE_FN[field_type] + return fn(field_number, value) + except KeyError: + raise message_mod.EncodeError('Unrecognized field type: %d' % field_type) + + +def _AddByteSizeMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def ByteSize(self): + if not self._cached_byte_size_dirty: + return self._cached_byte_size + + size = 0 + descriptor = self.DESCRIPTOR + if descriptor._is_map_entry: + # Fields of map entry should always be serialized. + key_field = descriptor.fields_by_name['key'] + _MaybeAddEncoder(cls, key_field) + size = key_field._sizer(self.key) + value_field = descriptor.fields_by_name['value'] + _MaybeAddEncoder(cls, value_field) + size += value_field._sizer(self.value) + else: + for field_descriptor, field_value in self.ListFields(): + _MaybeAddEncoder(cls, field_descriptor) + size += field_descriptor._sizer(field_value) + for tag_bytes, value_bytes in self._unknown_fields: + size += len(tag_bytes) + len(value_bytes) + + self._cached_byte_size = size + self._cached_byte_size_dirty = False + self._listener_for_children.dirty = False + return size + + cls.ByteSize = ByteSize + + +def _AddSerializeToStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def SerializeToString(self, **kwargs): + # Check if the message has all of its required fields set. + if not self.IsInitialized(): + raise message_mod.EncodeError( + 'Message %s is missing required fields: %s' % ( + self.DESCRIPTOR.full_name, ','.join(self.FindInitializationErrors()))) + return self.SerializePartialToString(**kwargs) + cls.SerializeToString = SerializeToString + + +def _AddSerializePartialToStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + + def SerializePartialToString(self, **kwargs): + out = BytesIO() + self._InternalSerialize(out.write, **kwargs) + return out.getvalue() + cls.SerializePartialToString = SerializePartialToString + + def InternalSerialize(self, write_bytes, deterministic=None): + if deterministic is None: + deterministic = ( + api_implementation.IsPythonDefaultSerializationDeterministic()) + else: + deterministic = bool(deterministic) + + descriptor = self.DESCRIPTOR + if descriptor._is_map_entry: + # Fields of map entry should always be serialized. + key_field = descriptor.fields_by_name['key'] + _MaybeAddEncoder(cls, key_field) + key_field._encoder(write_bytes, self.key, deterministic) + value_field = descriptor.fields_by_name['value'] + _MaybeAddEncoder(cls, value_field) + value_field._encoder(write_bytes, self.value, deterministic) + else: + for field_descriptor, field_value in self.ListFields(): + _MaybeAddEncoder(cls, field_descriptor) + field_descriptor._encoder(write_bytes, field_value, deterministic) + for tag_bytes, value_bytes in self._unknown_fields: + write_bytes(tag_bytes) + write_bytes(value_bytes) + cls._InternalSerialize = InternalSerialize + + +def _AddMergeFromStringMethod(message_descriptor, cls): + """Helper for _AddMessageMethods().""" + def MergeFromString(self, serialized): + serialized = memoryview(serialized) + length = len(serialized) + try: + if self._InternalParse(serialized, 0, length) != length: + # The only reason _InternalParse would return early is if it + # encountered an end-group tag. + raise message_mod.DecodeError('Unexpected end-group tag.') + except (IndexError, TypeError): + # Now ord(buf[p:p+1]) == ord('') gets TypeError. + raise message_mod.DecodeError('Truncated message.') + except struct.error as e: + raise message_mod.DecodeError(e) + return length # Return this for legacy reasons. + cls.MergeFromString = MergeFromString + + local_ReadTag = decoder.ReadTag + local_SkipField = decoder.SkipField + fields_by_tag = cls._fields_by_tag + message_set_decoders_by_tag = cls._message_set_decoders_by_tag + + def InternalParse(self, buffer, pos, end): + """Create a message from serialized bytes. + + Args: + self: Message, instance of the proto message object. + buffer: memoryview of the serialized data. + pos: int, position to start in the serialized data. + end: int, end position of the serialized data. + + Returns: + Message object. + """ + # Guard against internal misuse, since this function is called internally + # quite extensively, and its easy to accidentally pass bytes. + assert isinstance(buffer, memoryview) + self._Modified() + field_dict = self._fields + while pos != end: + (tag_bytes, new_pos) = local_ReadTag(buffer, pos) + field_decoder, field_des = message_set_decoders_by_tag.get( + tag_bytes, (None, None) + ) + if field_decoder: + pos = field_decoder(buffer, new_pos, end, self, field_dict) + continue + field_des, is_packed = fields_by_tag.get(tag_bytes, (None, None)) + if field_des is None: + if not self._unknown_fields: # pylint: disable=protected-access + self._unknown_fields = [] # pylint: disable=protected-access + # pylint: disable=protected-access + (tag, _) = decoder._DecodeVarint(tag_bytes, 0) + field_number, wire_type = wire_format.UnpackTag(tag) + if field_number == 0: + raise message_mod.DecodeError('Field number 0 is illegal.') + # TODO: remove old_pos. + old_pos = new_pos + (data, new_pos) = decoder._DecodeUnknownField( + buffer, new_pos, wire_type) # pylint: disable=protected-access + if new_pos == -1: + return pos + # TODO: remove _unknown_fields. + new_pos = local_SkipField(buffer, old_pos, end, tag_bytes) + if new_pos == -1: + return pos + self._unknown_fields.append( + (tag_bytes, buffer[old_pos:new_pos].tobytes())) + pos = new_pos + else: + _MaybeAddDecoder(cls, field_des) + field_decoder = field_des._decoders[is_packed] + pos = field_decoder(buffer, new_pos, end, self, field_dict) + if field_des.containing_oneof: + self._UpdateOneofState(field_des) + return pos + cls._InternalParse = InternalParse + + +def _AddIsInitializedMethod(message_descriptor, cls): + """Adds the IsInitialized and FindInitializationError methods to the + protocol message class.""" + + required_fields = [field for field in message_descriptor.fields + if field.label == _FieldDescriptor.LABEL_REQUIRED] + + def IsInitialized(self, errors=None): + """Checks if all required fields of a message are set. + + Args: + errors: A list which, if provided, will be populated with the field + paths of all missing required fields. + + Returns: + True iff the specified message has all required fields set. + """ + + # Performance is critical so we avoid HasField() and ListFields(). + + for field in required_fields: + if (field not in self._fields or + (field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE and + not self._fields[field]._is_present_in_parent)): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + + for field, value in list(self._fields.items()): # dict can change size! + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if field.label == _FieldDescriptor.LABEL_REPEATED: + if (field.message_type._is_map_entry): + continue + for element in value: + if not element.IsInitialized(): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + elif value._is_present_in_parent and not value.IsInitialized(): + if errors is not None: + errors.extend(self.FindInitializationErrors()) + return False + + return True + + cls.IsInitialized = IsInitialized + + def FindInitializationErrors(self): + """Finds required fields which are not initialized. + + Returns: + A list of strings. Each string is a path to an uninitialized field from + the top-level message, e.g. "foo.bar[5].baz". + """ + + errors = [] # simplify things + + for field in required_fields: + if not self.HasField(field.name): + errors.append(field.name) + + for field, value in self.ListFields(): + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if field.is_extension: + name = '(%s)' % field.full_name + else: + name = field.name + + if _IsMapField(field): + if _IsMessageMapField(field): + for key in value: + element = value[key] + prefix = '%s[%s].' % (name, key) + sub_errors = element.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + else: + # ScalarMaps can't have any initialization errors. + pass + elif field.label == _FieldDescriptor.LABEL_REPEATED: + for i in range(len(value)): + element = value[i] + prefix = '%s[%d].' % (name, i) + sub_errors = element.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + else: + prefix = name + '.' + sub_errors = value.FindInitializationErrors() + errors += [prefix + error for error in sub_errors] + + return errors + + cls.FindInitializationErrors = FindInitializationErrors + + +def _FullyQualifiedClassName(klass): + module = klass.__module__ + name = getattr(klass, '__qualname__', klass.__name__) + if module in (None, 'builtins', '__builtin__'): + return name + return module + '.' + name + + +def _AddMergeFromMethod(cls): + LABEL_REPEATED = _FieldDescriptor.LABEL_REPEATED + CPPTYPE_MESSAGE = _FieldDescriptor.CPPTYPE_MESSAGE + + def MergeFrom(self, msg): + if not isinstance(msg, cls): + raise TypeError( + 'Parameter to MergeFrom() must be instance of same class: ' + 'expected %s got %s.' % (_FullyQualifiedClassName(cls), + _FullyQualifiedClassName(msg.__class__))) + + assert msg is not self + self._Modified() + + fields = self._fields + + for field, value in msg._fields.items(): + if field.label == LABEL_REPEATED: + field_value = fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + fields[field] = field_value + field_value.MergeFrom(value) + elif field.cpp_type == CPPTYPE_MESSAGE: + if value._is_present_in_parent: + field_value = fields.get(field) + if field_value is None: + # Construct a new object to represent this field. + field_value = field._default_constructor(self) + fields[field] = field_value + field_value.MergeFrom(value) + else: + self._fields[field] = value + if field.containing_oneof: + self._UpdateOneofState(field) + + if msg._unknown_fields: + if not self._unknown_fields: + self._unknown_fields = [] + self._unknown_fields.extend(msg._unknown_fields) + + cls.MergeFrom = MergeFrom + + +def _AddWhichOneofMethod(message_descriptor, cls): + def WhichOneof(self, oneof_name): + """Returns the name of the currently set field inside a oneof, or None.""" + try: + field = message_descriptor.oneofs_by_name[oneof_name] + except KeyError: + raise ValueError( + 'Protocol message has no oneof "%s" field.' % oneof_name) + + nested_field = self._oneofs.get(field, None) + if nested_field is not None and self.HasField(nested_field.name): + return nested_field.name + else: + return None + + cls.WhichOneof = WhichOneof + + +def _Clear(self): + # Clear fields. + self._fields = {} + self._unknown_fields = () + + self._oneofs = {} + self._Modified() + + +def _UnknownFields(self): + raise NotImplementedError('Please use the add-on feaure ' + 'unknown_fields.UnknownFieldSet(message) in ' + 'unknown_fields.py instead.') + + +def _DiscardUnknownFields(self): + self._unknown_fields = [] + for field, value in self.ListFields(): + if field.cpp_type == _FieldDescriptor.CPPTYPE_MESSAGE: + if _IsMapField(field): + if _IsMessageMapField(field): + for key in value: + value[key].DiscardUnknownFields() + elif field.label == _FieldDescriptor.LABEL_REPEATED: + for sub_message in value: + sub_message.DiscardUnknownFields() + else: + value.DiscardUnknownFields() + + +def _SetListener(self, listener): + if listener is None: + self._listener = message_listener_mod.NullMessageListener() + else: + self._listener = listener + + +def _AddMessageMethods(message_descriptor, cls): + """Adds implementations of all Message methods to cls.""" + _AddListFieldsMethod(message_descriptor, cls) + _AddHasFieldMethod(message_descriptor, cls) + _AddClearFieldMethod(message_descriptor, cls) + if message_descriptor.is_extendable: + _AddClearExtensionMethod(cls) + _AddHasExtensionMethod(cls) + _AddEqualsMethod(message_descriptor, cls) + _AddStrMethod(message_descriptor, cls) + _AddReprMethod(message_descriptor, cls) + _AddUnicodeMethod(message_descriptor, cls) + _AddByteSizeMethod(message_descriptor, cls) + _AddSerializeToStringMethod(message_descriptor, cls) + _AddSerializePartialToStringMethod(message_descriptor, cls) + _AddMergeFromStringMethod(message_descriptor, cls) + _AddIsInitializedMethod(message_descriptor, cls) + _AddMergeFromMethod(cls) + _AddWhichOneofMethod(message_descriptor, cls) + # Adds methods which do not depend on cls. + cls.Clear = _Clear + cls.DiscardUnknownFields = _DiscardUnknownFields + cls._SetListener = _SetListener + + +def _AddPrivateHelperMethods(message_descriptor, cls): + """Adds implementation of private helper methods to cls.""" + + def Modified(self): + """Sets the _cached_byte_size_dirty bit to true, + and propagates this to our listener iff this was a state change. + """ + + # Note: Some callers check _cached_byte_size_dirty before calling + # _Modified() as an extra optimization. So, if this method is ever + # changed such that it does stuff even when _cached_byte_size_dirty is + # already true, the callers need to be updated. + if not self._cached_byte_size_dirty: + self._cached_byte_size_dirty = True + self._listener_for_children.dirty = True + self._is_present_in_parent = True + self._listener.Modified() + + def _UpdateOneofState(self, field): + """Sets field as the active field in its containing oneof. + + Will also delete currently active field in the oneof, if it is different + from the argument. Does not mark the message as modified. + """ + other_field = self._oneofs.setdefault(field.containing_oneof, field) + if other_field is not field: + del self._fields[other_field] + self._oneofs[field.containing_oneof] = field + + cls._Modified = Modified + cls.SetInParent = Modified + cls._UpdateOneofState = _UpdateOneofState + + +class _Listener(object): + + """MessageListener implementation that a parent message registers with its + child message. + + In order to support semantics like: + + foo.bar.baz.moo = 23 + assert foo.HasField('bar') + + ...child objects must have back references to their parents. + This helper class is at the heart of this support. + """ + + def __init__(self, parent_message): + """Args: + parent_message: The message whose _Modified() method we should call when + we receive Modified() messages. + """ + # This listener establishes a back reference from a child (contained) object + # to its parent (containing) object. We make this a weak reference to avoid + # creating cyclic garbage when the client finishes with the 'parent' object + # in the tree. + if isinstance(parent_message, weakref.ProxyType): + self._parent_message_weakref = parent_message + else: + self._parent_message_weakref = weakref.proxy(parent_message) + + # As an optimization, we also indicate directly on the listener whether + # or not the parent message is dirty. This way we can avoid traversing + # up the tree in the common case. + self.dirty = False + + def Modified(self): + if self.dirty: + return + try: + # Propagate the signal to our parents iff this is the first field set. + self._parent_message_weakref._Modified() + except ReferenceError: + # We can get here if a client has kept a reference to a child object, + # and is now setting a field on it, but the child's parent has been + # garbage-collected. This is not an error. + pass + + +class _OneofListener(_Listener): + """Special listener implementation for setting composite oneof fields.""" + + def __init__(self, parent_message, field): + """Args: + parent_message: The message whose _Modified() method we should call when + we receive Modified() messages. + field: The descriptor of the field being set in the parent message. + """ + super(_OneofListener, self).__init__(parent_message) + self._field = field + + def Modified(self): + """Also updates the state of the containing oneof in the parent message.""" + try: + self._parent_message_weakref._UpdateOneofState(self._field) + super(_OneofListener, self).Modified() + except ReferenceError: + pass diff --git a/google/protobuf/internal/testing_refleaks.py b/google/protobuf/internal/testing_refleaks.py new file mode 100644 index 0000000..ca0f0b9 --- /dev/null +++ b/google/protobuf/internal/testing_refleaks.py @@ -0,0 +1,119 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""A subclass of unittest.TestCase which checks for reference leaks. + +To use: +- Use testing_refleak.BaseTestCase instead of unittest.TestCase +- Configure and compile Python with --with-pydebug + +If sys.gettotalrefcount() is not available (because Python was built without +the Py_DEBUG option), then this module is a no-op and tests will run normally. +""" + +import copyreg +import gc +import sys +import unittest + + +class LocalTestResult(unittest.TestResult): + """A TestResult which forwards events to a parent object, except for Skips.""" + + def __init__(self, parent_result): + unittest.TestResult.__init__(self) + self.parent_result = parent_result + + def addError(self, test, error): + self.parent_result.addError(test, error) + + def addFailure(self, test, error): + self.parent_result.addFailure(test, error) + + def addSkip(self, test, reason): + pass + + +class ReferenceLeakCheckerMixin(object): + """A mixin class for TestCase, which checks reference counts.""" + + NB_RUNS = 3 + + def run(self, result=None): + testMethod = getattr(self, self._testMethodName) + expecting_failure_method = getattr(testMethod, "__unittest_expecting_failure__", False) + expecting_failure_class = getattr(self, "__unittest_expecting_failure__", False) + if expecting_failure_class or expecting_failure_method: + return + + # python_message.py registers all Message classes to some pickle global + # registry, which makes the classes immortal. + # We save a copy of this registry, and reset it before we could references. + self._saved_pickle_registry = copyreg.dispatch_table.copy() + + # Run the test twice, to warm up the instance attributes. + super(ReferenceLeakCheckerMixin, self).run(result=result) + super(ReferenceLeakCheckerMixin, self).run(result=result) + + oldrefcount = 0 + local_result = LocalTestResult(result) + num_flakes = 0 + + refcount_deltas = [] + while len(refcount_deltas) < self.NB_RUNS: + oldrefcount = self._getRefcounts() + super(ReferenceLeakCheckerMixin, self).run(result=local_result) + newrefcount = self._getRefcounts() + # If the GC was able to collect some objects after the call to run() that + # it could not collect before the call, then the counts won't match. + if newrefcount < oldrefcount and num_flakes < 2: + # This result is (probably) a flake -- garbage collectors aren't very + # predictable, but a lower ending refcount is the opposite of the + # failure we are testing for. If the result is repeatable, then we will + # eventually report it, but not after trying to eliminate it. + num_flakes += 1 + continue + num_flakes = 0 + refcount_deltas.append(newrefcount - oldrefcount) + print(refcount_deltas, self) + + try: + self.assertEqual(refcount_deltas, [0] * self.NB_RUNS) + except Exception: # pylint: disable=broad-except + result.addError(self, sys.exc_info()) + + def _getRefcounts(self): + copyreg.dispatch_table.clear() + copyreg.dispatch_table.update(self._saved_pickle_registry) + # It is sometimes necessary to gc.collect() multiple times, to ensure + # that all objects can be collected. + gc.collect() + gc.collect() + gc.collect() + return sys.gettotalrefcount() + + +if hasattr(sys, 'gettotalrefcount'): + + def TestCase(test_class): + new_bases = (ReferenceLeakCheckerMixin,) + test_class.__bases__ + new_class = type(test_class)( + test_class.__name__, new_bases, dict(test_class.__dict__)) + return new_class + SkipReferenceLeakChecker = unittest.skip + +else: + # When PyDEBUG is not enabled, run the tests normally. + + def TestCase(test_class): + return test_class + + def SkipReferenceLeakChecker(reason): + del reason # Don't skip, so don't need a reason. + def Same(func): + return func + return Same diff --git a/google/protobuf/internal/type_checkers.py b/google/protobuf/internal/type_checkers.py new file mode 100644 index 0000000..e152a43 --- /dev/null +++ b/google/protobuf/internal/type_checkers.py @@ -0,0 +1,408 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Provides type checking routines. + +This module defines type checking utilities in the forms of dictionaries: + +VALUE_CHECKERS: A dictionary of field types and a value validation object. +TYPE_TO_BYTE_SIZE_FN: A dictionary with field types and a size computing + function. +TYPE_TO_SERIALIZE_METHOD: A dictionary with field types and serialization + function. +FIELD_TYPE_TO_WIRE_TYPE: A dictionary with field typed and their + corresponding wire types. +TYPE_TO_DESERIALIZE_METHOD: A dictionary with field types and deserialization + function. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import ctypes +import numbers + +from google.protobuf.internal import decoder +from google.protobuf.internal import encoder +from google.protobuf.internal import wire_format +from google.protobuf import descriptor + +_FieldDescriptor = descriptor.FieldDescriptor + + +def TruncateToFourByteFloat(original): + return ctypes.c_float(original).value + + +def ToShortestFloat(original): + """Returns the shortest float that has same value in wire.""" + # All 4 byte floats have between 6 and 9 significant digits, so we + # start with 6 as the lower bound. + # It has to be iterative because use '.9g' directly can not get rid + # of the noises for most values. For example if set a float_field=0.9 + # use '.9g' will print 0.899999976. + precision = 6 + rounded = float('{0:.{1}g}'.format(original, precision)) + while TruncateToFourByteFloat(rounded) != original: + precision += 1 + rounded = float('{0:.{1}g}'.format(original, precision)) + return rounded + + +def GetTypeChecker(field): + """Returns a type checker for a message field of the specified types. + + Args: + field: FieldDescriptor object for this field. + + Returns: + An instance of TypeChecker which can be used to verify the types + of values assigned to a field of the specified type. + """ + if (field.cpp_type == _FieldDescriptor.CPPTYPE_STRING and + field.type == _FieldDescriptor.TYPE_STRING): + return UnicodeValueChecker() + if field.cpp_type == _FieldDescriptor.CPPTYPE_ENUM: + if field.enum_type.is_closed: + return EnumValueChecker(field.enum_type) + else: + # When open enums are supported, any int32 can be assigned. + return _VALUE_CHECKERS[_FieldDescriptor.CPPTYPE_INT32] + return _VALUE_CHECKERS[field.cpp_type] + + +# None of the typecheckers below make any attempt to guard against people +# subclassing builtin types and doing weird things. We're not trying to +# protect against malicious clients here, just people accidentally shooting +# themselves in the foot in obvious ways. +class TypeChecker(object): + + """Type checker used to catch type errors as early as possible + when the client is setting scalar fields in protocol messages. + """ + + def __init__(self, *acceptable_types): + self._acceptable_types = acceptable_types + + def CheckValue(self, proposed_value): + """Type check the provided value and return it. + + The returned value might have been normalized to another type. + """ + if not isinstance(proposed_value, self._acceptable_types): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), self._acceptable_types)) + raise TypeError(message) + return proposed_value + + +class TypeCheckerWithDefault(TypeChecker): + + def __init__(self, default_value, *acceptable_types): + TypeChecker.__init__(self, *acceptable_types) + self._default_value = default_value + + def DefaultValue(self): + return self._default_value + + +class BoolValueChecker(object): + """Type checker used for bool fields.""" + + def CheckValue(self, proposed_value): + if not hasattr(proposed_value, '__index__') or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (bool, int))) + raise TypeError(message) + return bool(proposed_value) + + def DefaultValue(self): + return False + + +# IntValueChecker and its subclasses perform integer type-checks +# and bounds-checks. +class IntValueChecker(object): + + """Checker used for integer fields. Performs type-check and range check.""" + + def CheckValue(self, proposed_value): + if not hasattr(proposed_value, '__index__') or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (int,))) + raise TypeError(message) + + if not self._MIN <= int(proposed_value) <= self._MAX: + raise ValueError('Value out of range: %d' % proposed_value) + # We force all values to int to make alternate implementations where the + # distinction is more significant (e.g. the C++ implementation) simpler. + proposed_value = int(proposed_value) + return proposed_value + + def DefaultValue(self): + return 0 + + +class EnumValueChecker(object): + + """Checker used for enum fields. Performs type-check and range check.""" + + def __init__(self, enum_type): + self._enum_type = enum_type + + def CheckValue(self, proposed_value): + if not isinstance(proposed_value, numbers.Integral): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (int,))) + raise TypeError(message) + if int(proposed_value) not in self._enum_type.values_by_number: + raise ValueError('Unknown enum value: %d' % proposed_value) + return proposed_value + + def DefaultValue(self): + return self._enum_type.values[0].number + + +class UnicodeValueChecker(object): + + """Checker used for string fields. + + Always returns a unicode value, even if the input is of type str. + """ + + def CheckValue(self, proposed_value): + if not isinstance(proposed_value, (bytes, str)): + message = ('%.1024r has type %s, but expected one of: %s' % + (proposed_value, type(proposed_value), (bytes, str))) + raise TypeError(message) + + # If the value is of type 'bytes' make sure that it is valid UTF-8 data. + if isinstance(proposed_value, bytes): + try: + proposed_value = proposed_value.decode('utf-8') + except UnicodeDecodeError: + raise ValueError('%.1024r has type bytes, but isn\'t valid UTF-8 ' + 'encoding. Non-UTF-8 strings must be converted to ' + 'unicode objects before being added.' % + (proposed_value)) + else: + try: + proposed_value.encode('utf8') + except UnicodeEncodeError: + raise ValueError('%.1024r isn\'t a valid unicode string and ' + 'can\'t be encoded in UTF-8.'% + (proposed_value)) + + return proposed_value + + def DefaultValue(self): + return u"" + + +class Int32ValueChecker(IntValueChecker): + # We're sure to use ints instead of longs here since comparison may be more + # efficient. + _MIN = -2147483648 + _MAX = 2147483647 + + +class Uint32ValueChecker(IntValueChecker): + _MIN = 0 + _MAX = (1 << 32) - 1 + + +class Int64ValueChecker(IntValueChecker): + _MIN = -(1 << 63) + _MAX = (1 << 63) - 1 + + +class Uint64ValueChecker(IntValueChecker): + _MIN = 0 + _MAX = (1 << 64) - 1 + + +# The max 4 bytes float is about 3.4028234663852886e+38 +_FLOAT_MAX = float.fromhex('0x1.fffffep+127') +_FLOAT_MIN = -_FLOAT_MAX +_INF = float('inf') +_NEG_INF = float('-inf') + + +class DoubleValueChecker(object): + """Checker used for double fields. + + Performs type-check and range check. + """ + + def CheckValue(self, proposed_value): + """Check and convert proposed_value to float.""" + if (not hasattr(proposed_value, '__float__') and + not hasattr(proposed_value, '__index__')) or ( + type(proposed_value).__module__ == 'numpy' and + type(proposed_value).__name__ == 'ndarray'): + message = ('%.1024r has type %s, but expected one of: int, float' % + (proposed_value, type(proposed_value))) + raise TypeError(message) + return float(proposed_value) + + def DefaultValue(self): + return 0.0 + + +class FloatValueChecker(DoubleValueChecker): + """Checker used for float fields. + + Performs type-check and range check. + + Values exceeding a 32-bit float will be converted to inf/-inf. + """ + + def CheckValue(self, proposed_value): + """Check and convert proposed_value to float.""" + converted_value = super().CheckValue(proposed_value) + # This inf rounding matches the C++ proto SafeDoubleToFloat logic. + if converted_value > _FLOAT_MAX: + return _INF + if converted_value < _FLOAT_MIN: + return _NEG_INF + + return TruncateToFourByteFloat(converted_value) + +# Type-checkers for all scalar CPPTYPEs. +_VALUE_CHECKERS = { + _FieldDescriptor.CPPTYPE_INT32: Int32ValueChecker(), + _FieldDescriptor.CPPTYPE_INT64: Int64ValueChecker(), + _FieldDescriptor.CPPTYPE_UINT32: Uint32ValueChecker(), + _FieldDescriptor.CPPTYPE_UINT64: Uint64ValueChecker(), + _FieldDescriptor.CPPTYPE_DOUBLE: DoubleValueChecker(), + _FieldDescriptor.CPPTYPE_FLOAT: FloatValueChecker(), + _FieldDescriptor.CPPTYPE_BOOL: BoolValueChecker(), + _FieldDescriptor.CPPTYPE_STRING: TypeCheckerWithDefault(b'', bytes), +} + + +# Map from field type to a function F, such that F(field_num, value) +# gives the total byte size for a value of the given type. This +# byte size includes tag information and any other additional space +# associated with serializing "value". +TYPE_TO_BYTE_SIZE_FN = { + _FieldDescriptor.TYPE_DOUBLE: wire_format.DoubleByteSize, + _FieldDescriptor.TYPE_FLOAT: wire_format.FloatByteSize, + _FieldDescriptor.TYPE_INT64: wire_format.Int64ByteSize, + _FieldDescriptor.TYPE_UINT64: wire_format.UInt64ByteSize, + _FieldDescriptor.TYPE_INT32: wire_format.Int32ByteSize, + _FieldDescriptor.TYPE_FIXED64: wire_format.Fixed64ByteSize, + _FieldDescriptor.TYPE_FIXED32: wire_format.Fixed32ByteSize, + _FieldDescriptor.TYPE_BOOL: wire_format.BoolByteSize, + _FieldDescriptor.TYPE_STRING: wire_format.StringByteSize, + _FieldDescriptor.TYPE_GROUP: wire_format.GroupByteSize, + _FieldDescriptor.TYPE_MESSAGE: wire_format.MessageByteSize, + _FieldDescriptor.TYPE_BYTES: wire_format.BytesByteSize, + _FieldDescriptor.TYPE_UINT32: wire_format.UInt32ByteSize, + _FieldDescriptor.TYPE_ENUM: wire_format.EnumByteSize, + _FieldDescriptor.TYPE_SFIXED32: wire_format.SFixed32ByteSize, + _FieldDescriptor.TYPE_SFIXED64: wire_format.SFixed64ByteSize, + _FieldDescriptor.TYPE_SINT32: wire_format.SInt32ByteSize, + _FieldDescriptor.TYPE_SINT64: wire_format.SInt64ByteSize + } + + +# Maps from field types to encoder constructors. +TYPE_TO_ENCODER = { + _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleEncoder, + _FieldDescriptor.TYPE_FLOAT: encoder.FloatEncoder, + _FieldDescriptor.TYPE_INT64: encoder.Int64Encoder, + _FieldDescriptor.TYPE_UINT64: encoder.UInt64Encoder, + _FieldDescriptor.TYPE_INT32: encoder.Int32Encoder, + _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Encoder, + _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Encoder, + _FieldDescriptor.TYPE_BOOL: encoder.BoolEncoder, + _FieldDescriptor.TYPE_STRING: encoder.StringEncoder, + _FieldDescriptor.TYPE_GROUP: encoder.GroupEncoder, + _FieldDescriptor.TYPE_MESSAGE: encoder.MessageEncoder, + _FieldDescriptor.TYPE_BYTES: encoder.BytesEncoder, + _FieldDescriptor.TYPE_UINT32: encoder.UInt32Encoder, + _FieldDescriptor.TYPE_ENUM: encoder.EnumEncoder, + _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Encoder, + _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Encoder, + _FieldDescriptor.TYPE_SINT32: encoder.SInt32Encoder, + _FieldDescriptor.TYPE_SINT64: encoder.SInt64Encoder, + } + + +# Maps from field types to sizer constructors. +TYPE_TO_SIZER = { + _FieldDescriptor.TYPE_DOUBLE: encoder.DoubleSizer, + _FieldDescriptor.TYPE_FLOAT: encoder.FloatSizer, + _FieldDescriptor.TYPE_INT64: encoder.Int64Sizer, + _FieldDescriptor.TYPE_UINT64: encoder.UInt64Sizer, + _FieldDescriptor.TYPE_INT32: encoder.Int32Sizer, + _FieldDescriptor.TYPE_FIXED64: encoder.Fixed64Sizer, + _FieldDescriptor.TYPE_FIXED32: encoder.Fixed32Sizer, + _FieldDescriptor.TYPE_BOOL: encoder.BoolSizer, + _FieldDescriptor.TYPE_STRING: encoder.StringSizer, + _FieldDescriptor.TYPE_GROUP: encoder.GroupSizer, + _FieldDescriptor.TYPE_MESSAGE: encoder.MessageSizer, + _FieldDescriptor.TYPE_BYTES: encoder.BytesSizer, + _FieldDescriptor.TYPE_UINT32: encoder.UInt32Sizer, + _FieldDescriptor.TYPE_ENUM: encoder.EnumSizer, + _FieldDescriptor.TYPE_SFIXED32: encoder.SFixed32Sizer, + _FieldDescriptor.TYPE_SFIXED64: encoder.SFixed64Sizer, + _FieldDescriptor.TYPE_SINT32: encoder.SInt32Sizer, + _FieldDescriptor.TYPE_SINT64: encoder.SInt64Sizer, + } + + +# Maps from field type to a decoder constructor. +TYPE_TO_DECODER = { + _FieldDescriptor.TYPE_DOUBLE: decoder.DoubleDecoder, + _FieldDescriptor.TYPE_FLOAT: decoder.FloatDecoder, + _FieldDescriptor.TYPE_INT64: decoder.Int64Decoder, + _FieldDescriptor.TYPE_UINT64: decoder.UInt64Decoder, + _FieldDescriptor.TYPE_INT32: decoder.Int32Decoder, + _FieldDescriptor.TYPE_FIXED64: decoder.Fixed64Decoder, + _FieldDescriptor.TYPE_FIXED32: decoder.Fixed32Decoder, + _FieldDescriptor.TYPE_BOOL: decoder.BoolDecoder, + _FieldDescriptor.TYPE_STRING: decoder.StringDecoder, + _FieldDescriptor.TYPE_GROUP: decoder.GroupDecoder, + _FieldDescriptor.TYPE_MESSAGE: decoder.MessageDecoder, + _FieldDescriptor.TYPE_BYTES: decoder.BytesDecoder, + _FieldDescriptor.TYPE_UINT32: decoder.UInt32Decoder, + _FieldDescriptor.TYPE_ENUM: decoder.EnumDecoder, + _FieldDescriptor.TYPE_SFIXED32: decoder.SFixed32Decoder, + _FieldDescriptor.TYPE_SFIXED64: decoder.SFixed64Decoder, + _FieldDescriptor.TYPE_SINT32: decoder.SInt32Decoder, + _FieldDescriptor.TYPE_SINT64: decoder.SInt64Decoder, + } + +# Maps from field type to expected wiretype. +FIELD_TYPE_TO_WIRE_TYPE = { + _FieldDescriptor.TYPE_DOUBLE: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_FLOAT: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_INT64: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_UINT64: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_INT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_FIXED64: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_FIXED32: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_BOOL: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_STRING: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_GROUP: wire_format.WIRETYPE_START_GROUP, + _FieldDescriptor.TYPE_MESSAGE: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_BYTES: + wire_format.WIRETYPE_LENGTH_DELIMITED, + _FieldDescriptor.TYPE_UINT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_ENUM: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_SFIXED32: wire_format.WIRETYPE_FIXED32, + _FieldDescriptor.TYPE_SFIXED64: wire_format.WIRETYPE_FIXED64, + _FieldDescriptor.TYPE_SINT32: wire_format.WIRETYPE_VARINT, + _FieldDescriptor.TYPE_SINT64: wire_format.WIRETYPE_VARINT, + } diff --git a/google/protobuf/internal/well_known_types.py b/google/protobuf/internal/well_known_types.py new file mode 100644 index 0000000..8a0a3b1 --- /dev/null +++ b/google/protobuf/internal/well_known_types.py @@ -0,0 +1,600 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Contains well known classes. + +This files defines well known classes which need extra maintenance including: + - Any + - Duration + - FieldMask + - Struct + - Timestamp +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + +import calendar +import collections.abc +import datetime +import warnings + +from google.protobuf.internal import field_mask + +FieldMask = field_mask.FieldMask + +_TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' +_NANOS_PER_SECOND = 1000000000 +_NANOS_PER_MILLISECOND = 1000000 +_NANOS_PER_MICROSECOND = 1000 +_MILLIS_PER_SECOND = 1000 +_MICROS_PER_SECOND = 1000000 +_SECONDS_PER_DAY = 24 * 3600 +_DURATION_SECONDS_MAX = 315576000000 +_TIMESTAMP_SECONDS_MIN = -62135596800 +_TIMESTAMP_SECONDS_MAX = 253402300799 + +_EPOCH_DATETIME_NAIVE = datetime.datetime(1970, 1, 1, tzinfo=None) +_EPOCH_DATETIME_AWARE = _EPOCH_DATETIME_NAIVE.replace( + tzinfo=datetime.timezone.utc +) + + +class Any(object): + """Class for Any Message type.""" + + __slots__ = () + + def Pack(self, msg, type_url_prefix='type.googleapis.com/', + deterministic=None): + """Packs the specified message into current Any message.""" + if len(type_url_prefix) < 1 or type_url_prefix[-1] != '/': + self.type_url = '%s/%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) + else: + self.type_url = '%s%s' % (type_url_prefix, msg.DESCRIPTOR.full_name) + self.value = msg.SerializeToString(deterministic=deterministic) + + def Unpack(self, msg): + """Unpacks the current Any message into specified message.""" + descriptor = msg.DESCRIPTOR + if not self.Is(descriptor): + return False + msg.ParseFromString(self.value) + return True + + def TypeName(self): + """Returns the protobuf type name of the inner message.""" + # Only last part is to be used: b/25630112 + return self.type_url.split('/')[-1] + + def Is(self, descriptor): + """Checks if this Any represents the given protobuf type.""" + return '/' in self.type_url and self.TypeName() == descriptor.full_name + + +class Timestamp(object): + """Class for Timestamp message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts Timestamp to RFC 3339 date string format. + + Returns: + A string converted from timestamp. The string is always Z-normalized + and uses 3, 6 or 9 fractional digits as required to represent the + exact time. Example of the return format: '1972-01-01T10:00:20.021Z' + """ + _CheckTimestampValid(self.seconds, self.nanos) + nanos = self.nanos + seconds = self.seconds % _SECONDS_PER_DAY + days = (self.seconds - seconds) // _SECONDS_PER_DAY + dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds) + + result = dt.isoformat() + if (nanos % 1e9) == 0: + # If there are 0 fractional digits, the fractional + # point '.' should be omitted when serializing. + return result + 'Z' + if (nanos % 1e6) == 0: + # Serialize 3 fractional digits. + return result + '.%03dZ' % (nanos / 1e6) + if (nanos % 1e3) == 0: + # Serialize 6 fractional digits. + return result + '.%06dZ' % (nanos / 1e3) + # Serialize 9 fractional digits. + return result + '.%09dZ' % nanos + + def FromJsonString(self, value): + """Parse a RFC 3339 date string format to Timestamp. + + Args: + value: A date string. Any fractional digits (or none) and any offset are + accepted as long as they fit into nano-seconds precision. + Example of accepted format: '1972-01-01T10:00:20.021-05:00' + + Raises: + ValueError: On parsing problems. + """ + if not isinstance(value, str): + raise ValueError('Timestamp JSON value not a string: {!r}'.format(value)) + timezone_offset = value.find('Z') + if timezone_offset == -1: + timezone_offset = value.find('+') + if timezone_offset == -1: + timezone_offset = value.rfind('-') + if timezone_offset == -1: + raise ValueError( + 'Failed to parse timestamp: missing valid timezone offset.') + time_value = value[0:timezone_offset] + # Parse datetime and nanos. + point_position = time_value.find('.') + if point_position == -1: + second_value = time_value + nano_value = '' + else: + second_value = time_value[:point_position] + nano_value = time_value[point_position + 1:] + if 't' in second_value: + raise ValueError( + 'time data \'{0}\' does not match format \'%Y-%m-%dT%H:%M:%S\', ' + 'lowercase \'t\' is not accepted'.format(second_value)) + date_object = datetime.datetime.strptime(second_value, _TIMESTAMPFOMAT) + td = date_object - datetime.datetime(1970, 1, 1) + seconds = td.seconds + td.days * _SECONDS_PER_DAY + if len(nano_value) > 9: + raise ValueError( + 'Failed to parse Timestamp: nanos {0} more than ' + '9 fractional digits.'.format(nano_value)) + if nano_value: + nanos = round(float('0.' + nano_value) * 1e9) + else: + nanos = 0 + # Parse timezone offsets. + if value[timezone_offset] == 'Z': + if len(value) != timezone_offset + 1: + raise ValueError('Failed to parse timestamp: invalid trailing' + ' data {0}.'.format(value)) + else: + timezone = value[timezone_offset:] + pos = timezone.find(':') + if pos == -1: + raise ValueError( + 'Invalid timezone offset value: {0}.'.format(timezone)) + if timezone[0] == '+': + seconds -= (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 + else: + seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60 + # Set seconds and nanos + _CheckTimestampValid(seconds, nanos) + self.seconds = int(seconds) + self.nanos = int(nanos) + + def GetCurrentTime(self): + """Get the current UTC into Timestamp.""" + self.FromDatetime(datetime.datetime.utcnow()) + + def ToNanoseconds(self): + """Converts Timestamp to nanoseconds since epoch.""" + _CheckTimestampValid(self.seconds, self.nanos) + return self.seconds * _NANOS_PER_SECOND + self.nanos + + def ToMicroseconds(self): + """Converts Timestamp to microseconds since epoch.""" + _CheckTimestampValid(self.seconds, self.nanos) + return (self.seconds * _MICROS_PER_SECOND + + self.nanos // _NANOS_PER_MICROSECOND) + + def ToMilliseconds(self): + """Converts Timestamp to milliseconds since epoch.""" + _CheckTimestampValid(self.seconds, self.nanos) + return (self.seconds * _MILLIS_PER_SECOND + + self.nanos // _NANOS_PER_MILLISECOND) + + def ToSeconds(self): + """Converts Timestamp to seconds since epoch.""" + _CheckTimestampValid(self.seconds, self.nanos) + return self.seconds + + def FromNanoseconds(self, nanos): + """Converts nanoseconds since epoch to Timestamp.""" + seconds = nanos // _NANOS_PER_SECOND + nanos = nanos % _NANOS_PER_SECOND + _CheckTimestampValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos + + def FromMicroseconds(self, micros): + """Converts microseconds since epoch to Timestamp.""" + seconds = micros // _MICROS_PER_SECOND + nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND + _CheckTimestampValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos + + def FromMilliseconds(self, millis): + """Converts milliseconds since epoch to Timestamp.""" + seconds = millis // _MILLIS_PER_SECOND + nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND + _CheckTimestampValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos + + def FromSeconds(self, seconds): + """Converts seconds since epoch to Timestamp.""" + _CheckTimestampValid(seconds, 0) + self.seconds = seconds + self.nanos = 0 + + def ToDatetime(self, tzinfo=None): + """Converts Timestamp to a datetime. + + Args: + tzinfo: A datetime.tzinfo subclass; defaults to None. + + Returns: + If tzinfo is None, returns a timezone-naive UTC datetime (with no timezone + information, i.e. not aware that it's UTC). + + Otherwise, returns a timezone-aware datetime in the input timezone. + """ + # Using datetime.fromtimestamp for this would avoid constructing an extra + # timedelta object and possibly an extra datetime. Unfortuantely, that has + # the disadvantage of not handling the full precision (on all platforms, see + # https://github.com/python/cpython/issues/109849) or full range (on some + # platforms, see https://github.com/python/cpython/issues/110042) of + # datetime. + _CheckTimestampValid(self.seconds, self.nanos) + delta = datetime.timedelta( + seconds=self.seconds, + microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND), + ) + if tzinfo is None: + return _EPOCH_DATETIME_NAIVE + delta + else: + # Note the tz conversion has to come after the timedelta arithmetic. + return (_EPOCH_DATETIME_AWARE + delta).astimezone(tzinfo) + + def FromDatetime(self, dt): + """Converts datetime to Timestamp. + + Args: + dt: A datetime. If it's timezone-naive, it's assumed to be in UTC. + """ + # Using this guide: http://wiki.python.org/moin/WorkingWithTime + # And this conversion guide: http://docs.python.org/library/time.html + + # Turn the date parameter into a tuple (struct_time) that can then be + # manipulated into a long value of seconds. During the conversion from + # struct_time to long, the source date in UTC, and so it follows that the + # correct transformation is calendar.timegm() + seconds = calendar.timegm(dt.utctimetuple()) + nanos = dt.microsecond * _NANOS_PER_MICROSECOND + _CheckTimestampValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos + + +def _CheckTimestampValid(seconds, nanos): + if seconds < _TIMESTAMP_SECONDS_MIN or seconds > _TIMESTAMP_SECONDS_MAX: + raise ValueError( + 'Timestamp is not valid: Seconds {0} must be in range ' + '[-62135596800, 253402300799].'.format(seconds)) + if nanos < 0 or nanos >= _NANOS_PER_SECOND: + raise ValueError( + 'Timestamp is not valid: Nanos {} must be in a range ' + '[0, 999999].'.format(nanos)) + + +class Duration(object): + """Class for Duration message type.""" + + __slots__ = () + + def ToJsonString(self): + """Converts Duration to string format. + + Returns: + A string converted from self. The string format will contains + 3, 6, or 9 fractional digits depending on the precision required to + represent the exact Duration value. For example: "1s", "1.010s", + "1.000000100s", "-3.100s" + """ + _CheckDurationValid(self.seconds, self.nanos) + if self.seconds < 0 or self.nanos < 0: + result = '-' + seconds = - self.seconds + int((0 - self.nanos) // 1e9) + nanos = (0 - self.nanos) % 1e9 + else: + result = '' + seconds = self.seconds + int(self.nanos // 1e9) + nanos = self.nanos % 1e9 + result += '%d' % seconds + if (nanos % 1e9) == 0: + # If there are 0 fractional digits, the fractional + # point '.' should be omitted when serializing. + return result + 's' + if (nanos % 1e6) == 0: + # Serialize 3 fractional digits. + return result + '.%03ds' % (nanos / 1e6) + if (nanos % 1e3) == 0: + # Serialize 6 fractional digits. + return result + '.%06ds' % (nanos / 1e3) + # Serialize 9 fractional digits. + return result + '.%09ds' % nanos + + def FromJsonString(self, value): + """Converts a string to Duration. + + Args: + value: A string to be converted. The string must end with 's'. Any + fractional digits (or none) are accepted as long as they fit into + precision. For example: "1s", "1.01s", "1.0000001s", "-3.100s + + Raises: + ValueError: On parsing problems. + """ + if not isinstance(value, str): + raise ValueError('Duration JSON value not a string: {!r}'.format(value)) + if len(value) < 1 or value[-1] != 's': + raise ValueError( + 'Duration must end with letter "s": {0}.'.format(value)) + try: + pos = value.find('.') + if pos == -1: + seconds = int(value[:-1]) + nanos = 0 + else: + seconds = int(value[:pos]) + if value[0] == '-': + nanos = int(round(float('-0{0}'.format(value[pos: -1])) *1e9)) + else: + nanos = int(round(float('0{0}'.format(value[pos: -1])) *1e9)) + _CheckDurationValid(seconds, nanos) + self.seconds = seconds + self.nanos = nanos + except ValueError as e: + raise ValueError( + 'Couldn\'t parse duration: {0} : {1}.'.format(value, e)) + + def ToNanoseconds(self): + """Converts a Duration to nanoseconds.""" + return self.seconds * _NANOS_PER_SECOND + self.nanos + + def ToMicroseconds(self): + """Converts a Duration to microseconds.""" + micros = _RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND) + return self.seconds * _MICROS_PER_SECOND + micros + + def ToMilliseconds(self): + """Converts a Duration to milliseconds.""" + millis = _RoundTowardZero(self.nanos, _NANOS_PER_MILLISECOND) + return self.seconds * _MILLIS_PER_SECOND + millis + + def ToSeconds(self): + """Converts a Duration to seconds.""" + return self.seconds + + def FromNanoseconds(self, nanos): + """Converts nanoseconds to Duration.""" + self._NormalizeDuration(nanos // _NANOS_PER_SECOND, + nanos % _NANOS_PER_SECOND) + + def FromMicroseconds(self, micros): + """Converts microseconds to Duration.""" + self._NormalizeDuration( + micros // _MICROS_PER_SECOND, + (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND) + + def FromMilliseconds(self, millis): + """Converts milliseconds to Duration.""" + self._NormalizeDuration( + millis // _MILLIS_PER_SECOND, + (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND) + + def FromSeconds(self, seconds): + """Converts seconds to Duration.""" + self.seconds = seconds + self.nanos = 0 + + def ToTimedelta(self) -> datetime.timedelta: + """Converts Duration to timedelta.""" + return datetime.timedelta( + seconds=self.seconds, microseconds=_RoundTowardZero( + self.nanos, _NANOS_PER_MICROSECOND)) + + def FromTimedelta(self, td): + """Converts timedelta to Duration.""" + self._NormalizeDuration(td.seconds + td.days * _SECONDS_PER_DAY, + td.microseconds * _NANOS_PER_MICROSECOND) + + def _NormalizeDuration(self, seconds, nanos): + """Set Duration by seconds and nanos.""" + # Force nanos to be negative if the duration is negative. + if seconds < 0 and nanos > 0: + seconds += 1 + nanos -= _NANOS_PER_SECOND + self.seconds = seconds + self.nanos = nanos + + +def _CheckDurationValid(seconds, nanos): + if seconds < -_DURATION_SECONDS_MAX or seconds > _DURATION_SECONDS_MAX: + raise ValueError( + 'Duration is not valid: Seconds {0} must be in range ' + '[-315576000000, 315576000000].'.format(seconds)) + if nanos <= -_NANOS_PER_SECOND or nanos >= _NANOS_PER_SECOND: + raise ValueError( + 'Duration is not valid: Nanos {0} must be in range ' + '[-999999999, 999999999].'.format(nanos)) + if (nanos < 0 and seconds > 0) or (nanos > 0 and seconds < 0): + raise ValueError( + 'Duration is not valid: Sign mismatch.') + + +def _RoundTowardZero(value, divider): + """Truncates the remainder part after division.""" + # For some languages, the sign of the remainder is implementation + # dependent if any of the operands is negative. Here we enforce + # "rounded toward zero" semantics. For example, for (-5) / 2 an + # implementation may give -3 as the result with the remainder being + # 1. This function ensures we always return -2 (closer to zero). + result = value // divider + remainder = value % divider + if result < 0 and remainder > 0: + return result + 1 + else: + return result + + +def _SetStructValue(struct_value, value): + if value is None: + struct_value.null_value = 0 + elif isinstance(value, bool): + # Note: this check must come before the number check because in Python + # True and False are also considered numbers. + struct_value.bool_value = value + elif isinstance(value, str): + struct_value.string_value = value + elif isinstance(value, (int, float)): + struct_value.number_value = value + elif isinstance(value, (dict, Struct)): + struct_value.struct_value.Clear() + struct_value.struct_value.update(value) + elif isinstance(value, (list, tuple, ListValue)): + struct_value.list_value.Clear() + struct_value.list_value.extend(value) + else: + raise ValueError('Unexpected type') + + +def _GetStructValue(struct_value): + which = struct_value.WhichOneof('kind') + if which == 'struct_value': + return struct_value.struct_value + elif which == 'null_value': + return None + elif which == 'number_value': + return struct_value.number_value + elif which == 'string_value': + return struct_value.string_value + elif which == 'bool_value': + return struct_value.bool_value + elif which == 'list_value': + return struct_value.list_value + elif which is None: + raise ValueError('Value not set') + + +class Struct(object): + """Class for Struct message type.""" + + __slots__ = () + + def __getitem__(self, key): + return _GetStructValue(self.fields[key]) + + def __contains__(self, item): + return item in self.fields + + def __setitem__(self, key, value): + _SetStructValue(self.fields[key], value) + + def __delitem__(self, key): + del self.fields[key] + + def __len__(self): + return len(self.fields) + + def __iter__(self): + return iter(self.fields) + + def keys(self): # pylint: disable=invalid-name + return self.fields.keys() + + def values(self): # pylint: disable=invalid-name + return [self[key] for key in self] + + def items(self): # pylint: disable=invalid-name + return [(key, self[key]) for key in self] + + def get_or_create_list(self, key): + """Returns a list for this key, creating if it didn't exist already.""" + if not self.fields[key].HasField('list_value'): + # Clear will mark list_value modified which will indeed create a list. + self.fields[key].list_value.Clear() + return self.fields[key].list_value + + def get_or_create_struct(self, key): + """Returns a struct for this key, creating if it didn't exist already.""" + if not self.fields[key].HasField('struct_value'): + # Clear will mark struct_value modified which will indeed create a struct. + self.fields[key].struct_value.Clear() + return self.fields[key].struct_value + + def update(self, dictionary): # pylint: disable=invalid-name + for key, value in dictionary.items(): + _SetStructValue(self.fields[key], value) + +collections.abc.MutableMapping.register(Struct) + + +class ListValue(object): + """Class for ListValue message type.""" + + __slots__ = () + + def __len__(self): + return len(self.values) + + def append(self, value): + _SetStructValue(self.values.add(), value) + + def extend(self, elem_seq): + for value in elem_seq: + self.append(value) + + def __getitem__(self, index): + """Retrieves item by the specified index.""" + return _GetStructValue(self.values.__getitem__(index)) + + def __setitem__(self, index, value): + _SetStructValue(self.values.__getitem__(index), value) + + def __delitem__(self, key): + del self.values[key] + + def items(self): + for i in range(len(self)): + yield self[i] + + def add_struct(self): + """Appends and returns a struct value as the next value in the list.""" + struct_value = self.values.add().struct_value + # Clear will mark struct_value modified which will indeed create a struct. + struct_value.Clear() + return struct_value + + def add_list(self): + """Appends and returns a list value as the next value in the list.""" + list_value = self.values.add().list_value + # Clear will mark list_value modified which will indeed create a list. + list_value.Clear() + return list_value + +collections.abc.MutableSequence.register(ListValue) + + +# LINT.IfChange(wktbases) +WKTBASES = { + 'google.protobuf.Any': Any, + 'google.protobuf.Duration': Duration, + 'google.protobuf.FieldMask': FieldMask, + 'google.protobuf.ListValue': ListValue, + 'google.protobuf.Struct': Struct, + 'google.protobuf.Timestamp': Timestamp, +} +# LINT.ThenChange(//depot/google.protobuf/compiler/python/pyi_generator.cc:wktbases) diff --git a/google/protobuf/internal/wire_format.py b/google/protobuf/internal/wire_format.py new file mode 100644 index 0000000..6237dab --- /dev/null +++ b/google/protobuf/internal/wire_format.py @@ -0,0 +1,245 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Constants and static functions to support protocol buffer wire format.""" + +__author__ = 'robinson@google.com (Will Robinson)' + +import struct +from google.protobuf import descriptor +from google.protobuf import message + + +TAG_TYPE_BITS = 3 # Number of bits used to hold type info in a proto tag. +TAG_TYPE_MASK = (1 << TAG_TYPE_BITS) - 1 # 0x7 + +# These numbers identify the wire type of a protocol buffer value. +# We use the least-significant TAG_TYPE_BITS bits of the varint-encoded +# tag-and-type to store one of these WIRETYPE_* constants. +# These values must match WireType enum in //google/protobuf/wire_format.h. +WIRETYPE_VARINT = 0 +WIRETYPE_FIXED64 = 1 +WIRETYPE_LENGTH_DELIMITED = 2 +WIRETYPE_START_GROUP = 3 +WIRETYPE_END_GROUP = 4 +WIRETYPE_FIXED32 = 5 +_WIRETYPE_MAX = 5 + + +# Bounds for various integer types. +INT32_MAX = int((1 << 31) - 1) +INT32_MIN = int(-(1 << 31)) +UINT32_MAX = (1 << 32) - 1 + +INT64_MAX = (1 << 63) - 1 +INT64_MIN = -(1 << 63) +UINT64_MAX = (1 << 64) - 1 + +# "struct" format strings that will encode/decode the specified formats. +FORMAT_UINT32_LITTLE_ENDIAN = '> TAG_TYPE_BITS), (tag & TAG_TYPE_MASK) + + +def ZigZagEncode(value): + """ZigZag Transform: Encodes signed integers so that they can be + effectively used with varint encoding. See wire_format.h for + more details. + """ + if value >= 0: + return value << 1 + return (value << 1) ^ (~0) + + +def ZigZagDecode(value): + """Inverse of ZigZagEncode().""" + if not value & 0x1: + return value >> 1 + return (value >> 1) ^ (~0) + + + +# The *ByteSize() functions below return the number of bytes required to +# serialize "field number + type" information and then serialize the value. + + +def Int32ByteSize(field_number, int32): + return Int64ByteSize(field_number, int32) + + +def Int32ByteSizeNoTag(int32): + return _VarUInt64ByteSizeNoTag(0xffffffffffffffff & int32) + + +def Int64ByteSize(field_number, int64): + # Have to convert to uint before calling UInt64ByteSize(). + return UInt64ByteSize(field_number, 0xffffffffffffffff & int64) + + +def UInt32ByteSize(field_number, uint32): + return UInt64ByteSize(field_number, uint32) + + +def UInt64ByteSize(field_number, uint64): + return TagByteSize(field_number) + _VarUInt64ByteSizeNoTag(uint64) + + +def SInt32ByteSize(field_number, int32): + return UInt32ByteSize(field_number, ZigZagEncode(int32)) + + +def SInt64ByteSize(field_number, int64): + return UInt64ByteSize(field_number, ZigZagEncode(int64)) + + +def Fixed32ByteSize(field_number, fixed32): + return TagByteSize(field_number) + 4 + + +def Fixed64ByteSize(field_number, fixed64): + return TagByteSize(field_number) + 8 + + +def SFixed32ByteSize(field_number, sfixed32): + return TagByteSize(field_number) + 4 + + +def SFixed64ByteSize(field_number, sfixed64): + return TagByteSize(field_number) + 8 + + +def FloatByteSize(field_number, flt): + return TagByteSize(field_number) + 4 + + +def DoubleByteSize(field_number, double): + return TagByteSize(field_number) + 8 + + +def BoolByteSize(field_number, b): + return TagByteSize(field_number) + 1 + + +def EnumByteSize(field_number, enum): + return UInt32ByteSize(field_number, enum) + + +def StringByteSize(field_number, string): + return BytesByteSize(field_number, string.encode('utf-8')) + + +def BytesByteSize(field_number, b): + return (TagByteSize(field_number) + + _VarUInt64ByteSizeNoTag(len(b)) + + len(b)) + + +def GroupByteSize(field_number, message): + return (2 * TagByteSize(field_number) # START and END group. + + message.ByteSize()) + + +def MessageByteSize(field_number, message): + return (TagByteSize(field_number) + + _VarUInt64ByteSizeNoTag(message.ByteSize()) + + message.ByteSize()) + + +def MessageSetItemByteSize(field_number, msg): + # First compute the sizes of the tags. + # There are 2 tags for the beginning and ending of the repeated group, that + # is field number 1, one with field number 2 (type_id) and one with field + # number 3 (message). + total_size = (2 * TagByteSize(1) + TagByteSize(2) + TagByteSize(3)) + + # Add the number of bytes for type_id. + total_size += _VarUInt64ByteSizeNoTag(field_number) + + message_size = msg.ByteSize() + + # The number of bytes for encoding the length of the message. + total_size += _VarUInt64ByteSizeNoTag(message_size) + + # The size of the message. + total_size += message_size + return total_size + + +def TagByteSize(field_number): + """Returns the bytes required to serialize a tag with this field number.""" + # Just pass in type 0, since the type won't affect the tag+type size. + return _VarUInt64ByteSizeNoTag(PackTag(field_number, 0)) + + +# Private helper function for the *ByteSize() functions above. + +def _VarUInt64ByteSizeNoTag(uint64): + """Returns the number of bytes required to serialize a single varint + using boundary value comparisons. (unrolled loop optimization -WPierce) + uint64 must be unsigned. + """ + if uint64 <= 0x7f: return 1 + if uint64 <= 0x3fff: return 2 + if uint64 <= 0x1fffff: return 3 + if uint64 <= 0xfffffff: return 4 + if uint64 <= 0x7ffffffff: return 5 + if uint64 <= 0x3ffffffffff: return 6 + if uint64 <= 0x1ffffffffffff: return 7 + if uint64 <= 0xffffffffffffff: return 8 + if uint64 <= 0x7fffffffffffffff: return 9 + if uint64 > UINT64_MAX: + raise message.EncodeError('Value out of range: %d' % uint64) + return 10 + + +NON_PACKABLE_TYPES = ( + descriptor.FieldDescriptor.TYPE_STRING, + descriptor.FieldDescriptor.TYPE_GROUP, + descriptor.FieldDescriptor.TYPE_MESSAGE, + descriptor.FieldDescriptor.TYPE_BYTES +) + + +def IsTypePackable(field_type): + """Return true iff packable = true is valid for fields of this type. + + Args: + field_type: a FieldDescriptor::Type value. + + Returns: + True iff fields of this type are packable. + """ + return field_type not in NON_PACKABLE_TYPES diff --git a/google/protobuf/json_format.py b/google/protobuf/json_format.py new file mode 100644 index 0000000..8a2e249 --- /dev/null +++ b/google/protobuf/json_format.py @@ -0,0 +1,1060 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Contains routines for printing protocol messages in JSON format. + +Simple usage example: + + # Create a proto object and serialize it to a json format string. + message = my_proto_pb2.MyMessage(foo='bar') + json_string = json_format.MessageToJson(message) + + # Parse a json format string to proto object. + message = json_format.Parse(json_string, my_proto_pb2.MyMessage()) +""" + +__author__ = 'jieluo@google.com (Jie Luo)' + + +import base64 +from collections import OrderedDict +import json +import math +from operator import methodcaller +import re + +from google.protobuf import descriptor +from google.protobuf import message_factory +from google.protobuf import symbol_database +from google.protobuf.internal import type_checkers + + +_INT_TYPES = frozenset([ + descriptor.FieldDescriptor.CPPTYPE_INT32, + descriptor.FieldDescriptor.CPPTYPE_UINT32, + descriptor.FieldDescriptor.CPPTYPE_INT64, + descriptor.FieldDescriptor.CPPTYPE_UINT64, +]) +_INT64_TYPES = frozenset([ + descriptor.FieldDescriptor.CPPTYPE_INT64, + descriptor.FieldDescriptor.CPPTYPE_UINT64, +]) +_FLOAT_TYPES = frozenset([ + descriptor.FieldDescriptor.CPPTYPE_FLOAT, + descriptor.FieldDescriptor.CPPTYPE_DOUBLE, +]) +_INFINITY = 'Infinity' +_NEG_INFINITY = '-Infinity' +_NAN = 'NaN' + +_UNPAIRED_SURROGATE_PATTERN = re.compile( + '[\ud800-\udbff](?![\udc00-\udfff])|(? self.max_recursion_depth: + raise ParseError( + 'Message too deep. Max recursion depth is {0}'.format( + self.max_recursion_depth + ) + ) + message_descriptor = message.DESCRIPTOR + full_name = message_descriptor.full_name + if not path: + path = message_descriptor.name + if _IsWrapperMessage(message_descriptor): + self._ConvertWrapperMessage(value, message, path) + elif full_name in _WKTJSONMETHODS: + methodcaller(_WKTJSONMETHODS[full_name][1], value, message, path)(self) + else: + self._ConvertFieldValuePair(value, message, path) + self.recursion_depth -= 1 + + def _ConvertFieldValuePair(self, js, message, path): + """Convert field value pairs into regular message. + + Args: + js: A JSON object to convert the field value pairs. + message: A regular protocol message to record the data. + path: parent path to log parse error info. + + Raises: + ParseError: In case of problems converting. + """ + names = [] + message_descriptor = message.DESCRIPTOR + fields_by_json_name = dict( + (f.json_name, f) for f in message_descriptor.fields + ) + for name in js: + try: + field = fields_by_json_name.get(name, None) + if not field: + field = message_descriptor.fields_by_name.get(name, None) + if not field and _VALID_EXTENSION_NAME.match(name): + if not message_descriptor.is_extendable: + raise ParseError( + 'Message type {0} does not have extensions at {1}'.format( + message_descriptor.full_name, path + ) + ) + identifier = name[1:-1] # strip [] brackets + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(identifier) + # pylint: enable=protected-access + if not field: + # Try looking for extension by the message type name, dropping the + # field name following the final . separator in full_name. + identifier = '.'.join(identifier.split('.')[:-1]) + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(identifier) + # pylint: enable=protected-access + if not field: + if self.ignore_unknown_fields: + continue + raise ParseError( + ( + 'Message type "{0}" has no field named "{1}" at "{2}".\n' + ' Available Fields(except extensions): "{3}"' + ).format( + message_descriptor.full_name, + name, + path, + [f.json_name for f in message_descriptor.fields], + ) + ) + if name in names: + raise ParseError( + 'Message type "{0}" should not have multiple ' + '"{1}" fields at "{2}".'.format( + message.DESCRIPTOR.full_name, name, path + ) + ) + names.append(name) + value = js[name] + # Check no other oneof field is parsed. + if field.containing_oneof is not None and value is not None: + oneof_name = field.containing_oneof.name + if oneof_name in names: + raise ParseError( + 'Message type "{0}" should not have multiple ' + '"{1}" oneof fields at "{2}".'.format( + message.DESCRIPTOR.full_name, oneof_name, path + ) + ) + names.append(oneof_name) + + if value is None: + if ( + field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE + and field.message_type.full_name == 'google.protobuf.Value' + ): + sub_message = getattr(message, field.name) + sub_message.null_value = 0 + elif ( + field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM + and field.enum_type.full_name == 'google.protobuf.NullValue' + ): + setattr(message, field.name, 0) + else: + message.ClearField(field.name) + continue + + # Parse field value. + if _IsMapEntry(field): + message.ClearField(field.name) + self._ConvertMapFieldValue( + value, message, field, '{0}.{1}'.format(path, name) + ) + elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + message.ClearField(field.name) + if not isinstance(value, list): + raise ParseError( + 'repeated field {0} must be in [] which is {1} at {2}'.format( + name, value, path + ) + ) + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + # Repeated message field. + for index, item in enumerate(value): + sub_message = getattr(message, field.name).add() + # None is a null_value in Value. + if ( + item is None + and sub_message.DESCRIPTOR.full_name + != 'google.protobuf.Value' + ): + raise ParseError( + 'null is not allowed to be used as an element' + ' in a repeated field at {0}.{1}[{2}]'.format( + path, name, index + ) + ) + self.ConvertMessage( + item, sub_message, '{0}.{1}[{2}]'.format(path, name, index) + ) + else: + # Repeated scalar field. + for index, item in enumerate(value): + if item is None: + raise ParseError( + 'null is not allowed to be used as an element' + ' in a repeated field at {0}.{1}[{2}]'.format( + path, name, index + ) + ) + self._ConvertAndAppendScalar( + message, field, item, '{0}.{1}[{2}]'.format(path, name, index)) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + if field.is_extension: + sub_message = message.Extensions[field] + else: + sub_message = getattr(message, field.name) + sub_message.SetInParent() + self.ConvertMessage(value, sub_message, '{0}.{1}'.format(path, name)) + else: + if field.is_extension: + self._ConvertAndSetScalarExtension(message, field, value, '{0}.{1}'.format(path, name)) + else: + self._ConvertAndSetScalar(message, field, value, '{0}.{1}'.format(path, name)) + except ParseError as e: + if field and field.containing_oneof is None: + raise ParseError( + 'Failed to parse {0} field: {1}.'.format(name, e) + ) from e + else: + raise ParseError(str(e)) from e + except ValueError as e: + raise ParseError( + 'Failed to parse {0} field: {1}.'.format(name, e) + ) from e + except TypeError as e: + raise ParseError( + 'Failed to parse {0} field: {1}.'.format(name, e) + ) from e + + def _ConvertAnyMessage(self, value, message, path): + """Convert a JSON representation into Any message.""" + if isinstance(value, dict) and not value: + return + try: + type_url = value['@type'] + except KeyError as e: + raise ParseError( + '@type is missing when parsing any message at {0}'.format(path) + ) from e + + try: + sub_message = _CreateMessageFromTypeUrl(type_url, self.descriptor_pool) + except TypeError as e: + raise ParseError('{0} at {1}'.format(e, path)) from e + message_descriptor = sub_message.DESCRIPTOR + full_name = message_descriptor.full_name + if _IsWrapperMessage(message_descriptor): + self._ConvertWrapperMessage( + value['value'], sub_message, '{0}.value'.format(path) + ) + elif full_name in _WKTJSONMETHODS: + methodcaller( + _WKTJSONMETHODS[full_name][1], + value['value'], + sub_message, + '{0}.value'.format(path), + )(self) + else: + del value['@type'] + self._ConvertFieldValuePair(value, sub_message, path) + value['@type'] = type_url + # Sets Any message + message.value = sub_message.SerializeToString() + message.type_url = type_url + + def _ConvertGenericMessage(self, value, message, path): + """Convert a JSON representation into message with FromJsonString.""" + # Duration, Timestamp, FieldMask have a FromJsonString method to do the + # conversion. Users can also call the method directly. + try: + message.FromJsonString(value) + except ValueError as e: + raise ParseError('{0} at {1}'.format(e, path)) from e + + def _ConvertValueMessage(self, value, message, path): + """Convert a JSON representation into Value message.""" + if isinstance(value, dict): + self._ConvertStructMessage(value, message.struct_value, path) + elif isinstance(value, list): + self._ConvertListValueMessage(value, message.list_value, path) + elif value is None: + message.null_value = 0 + elif isinstance(value, bool): + message.bool_value = value + elif isinstance(value, str): + message.string_value = value + elif isinstance(value, _INT_OR_FLOAT): + message.number_value = value + else: + raise ParseError( + 'Value {0} has unexpected type {1} at {2}'.format( + value, type(value), path + ) + ) + + def _ConvertListValueMessage(self, value, message, path): + """Convert a JSON representation into ListValue message.""" + if not isinstance(value, list): + raise ParseError( + 'ListValue must be in [] which is {0} at {1}'.format(value, path) + ) + message.ClearField('values') + for index, item in enumerate(value): + self._ConvertValueMessage( + item, message.values.add(), '{0}[{1}]'.format(path, index) + ) + + def _ConvertStructMessage(self, value, message, path): + """Convert a JSON representation into Struct message.""" + if not isinstance(value, dict): + raise ParseError( + 'Struct must be in a dict which is {0} at {1}'.format(value, path) + ) + # Clear will mark the struct as modified so it will be created even if + # there are no values. + message.Clear() + for key in value: + self._ConvertValueMessage( + value[key], message.fields[key], '{0}.{1}'.format(path, key) + ) + return + + def _ConvertWrapperMessage(self, value, message, path): + """Convert a JSON representation into Wrapper message.""" + field = message.DESCRIPTOR.fields_by_name['value'] + self._ConvertAndSetScalar(message, field, value, path='{0}.value'.format(path)) + + def _ConvertMapFieldValue(self, value, message, field, path): + """Convert map field value for a message map field. + + Args: + value: A JSON object to convert the map field value. + message: A protocol message to record the converted data. + field: The descriptor of the map field to be converted. + path: parent path to log parse error info. + + Raises: + ParseError: In case of convert problems. + """ + if not isinstance(value, dict): + raise ParseError( + 'Map field {0} must be in a dict which is {1} at {2}'.format( + field.name, value, path + ) + ) + key_field = field.message_type.fields_by_name['key'] + value_field = field.message_type.fields_by_name['value'] + for key in value: + key_value = _ConvertScalarFieldValue( + key, key_field, '{0}.key'.format(path), True + ) + if value_field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + self.ConvertMessage( + value[key], + getattr(message, field.name)[key_value], + '{0}[{1}]'.format(path, key_value), + ) + else: + self._ConvertAndSetScalarToMapKey( + message, + field, + key_value, + value[key], + path='{0}[{1}]'.format(path, key_value)) + + def _ConvertAndSetScalarExtension(self, message, extension_field, js_value, path): + """Convert scalar from js_value and assign it to message.Extensions[extension_field].""" + try: + message.Extensions[extension_field] = _ConvertScalarFieldValue( + js_value, extension_field, path) + except EnumStringValueParseError: + if not self.ignore_unknown_fields: + raise + + def _ConvertAndSetScalar(self, message, field, js_value, path): + """Convert scalar from js_value and assign it to message.field.""" + try: + setattr( + message, + field.name, + _ConvertScalarFieldValue(js_value, field, path)) + except EnumStringValueParseError: + if not self.ignore_unknown_fields: + raise + + def _ConvertAndAppendScalar(self, message, repeated_field, js_value, path): + """Convert scalar from js_value and append it to message.repeated_field.""" + try: + getattr(message, repeated_field.name).append( + _ConvertScalarFieldValue(js_value, repeated_field, path)) + except EnumStringValueParseError: + if not self.ignore_unknown_fields: + raise + + def _ConvertAndSetScalarToMapKey(self, message, map_field, converted_key, js_value, path): + """Convert scalar from 'js_value' and add it to message.map_field[converted_key].""" + try: + getattr(message, map_field.name)[converted_key] = _ConvertScalarFieldValue( + js_value, map_field.message_type.fields_by_name['value'], path, + ) + except EnumStringValueParseError: + if not self.ignore_unknown_fields: + raise + + +def _ConvertScalarFieldValue(value, field, path, require_str=False): + """Convert a single scalar field value. + + Args: + value: A scalar value to convert the scalar field value. + field: The descriptor of the field to convert. + path: parent path to log parse error info. + require_str: If True, the field value must be a str. + + Returns: + The converted scalar field value + + Raises: + ParseError: In case of convert problems. + EnumStringValueParseError: In case of unknown enum string value. + """ + try: + if field.cpp_type in _INT_TYPES: + return _ConvertInteger(value) + elif field.cpp_type in _FLOAT_TYPES: + return _ConvertFloat(value, field) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: + return _ConvertBool(value, require_str) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: + if field.type == descriptor.FieldDescriptor.TYPE_BYTES: + if isinstance(value, str): + encoded = value.encode('utf-8') + else: + encoded = value + # Add extra padding '=' + padded_value = encoded + b'=' * (4 - len(encoded) % 4) + return base64.urlsafe_b64decode(padded_value) + else: + # Checking for unpaired surrogates appears to be unreliable, + # depending on the specific Python version, so we check manually. + if _UNPAIRED_SURROGATE_PATTERN.search(value): + raise ParseError('Unpaired surrogate') + return value + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: + # Convert an enum value. + enum_value = field.enum_type.values_by_name.get(value, None) + if enum_value is None: + try: + number = int(value) + enum_value = field.enum_type.values_by_number.get(number, None) + except ValueError as e: + # Since parsing to integer failed and lookup in values_by_name didn't + # find this name, we have an enum string value which is unknown. + raise EnumStringValueParseError( + 'Invalid enum value {0} for enum type {1}'.format( + value, field.enum_type.full_name + ) + ) from e + if enum_value is None: + if field.enum_type.is_closed: + raise ParseError( + 'Invalid enum value {0} for enum type {1}'.format( + value, field.enum_type.full_name + ) + ) + else: + return number + return enum_value.number + except EnumStringValueParseError as e: + raise EnumStringValueParseError('{0} at {1}'.format(e, path)) from e + except ParseError as e: + raise ParseError('{0} at {1}'.format(e, path)) from e + + +def _ConvertInteger(value): + """Convert an integer. + + Args: + value: A scalar value to convert. + + Returns: + The integer value. + + Raises: + ParseError: If an integer couldn't be consumed. + """ + if isinstance(value, float) and not value.is_integer(): + raise ParseError("Couldn't parse integer: {0}".format(value)) + + if isinstance(value, str) and value.find(' ') != -1: + raise ParseError('Couldn\'t parse integer: "{0}"'.format(value)) + + if isinstance(value, bool): + raise ParseError( + 'Bool value {0} is not acceptable for integer field'.format(value) + ) + + return int(value) + + +def _ConvertFloat(value, field): + """Convert an floating point number.""" + if isinstance(value, float): + if math.isnan(value): + raise ParseError('Couldn\'t parse NaN, use quoted "NaN" instead') + if math.isinf(value): + if value > 0: + raise ParseError( + "Couldn't parse Infinity or value too large, " + 'use quoted "Infinity" instead' + ) + else: + raise ParseError( + "Couldn't parse -Infinity or value too small, " + 'use quoted "-Infinity" instead' + ) + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: + # pylint: disable=protected-access + if value > type_checkers._FLOAT_MAX: + raise ParseError('Float value too large') + # pylint: disable=protected-access + if value < type_checkers._FLOAT_MIN: + raise ParseError('Float value too small') + if value == 'nan': + raise ParseError('Couldn\'t parse float "nan", use "NaN" instead') + try: + # Assume Python compatible syntax. + return float(value) + except ValueError as e: + # Check alternative spellings. + if value == _NEG_INFINITY: + return float('-inf') + elif value == _INFINITY: + return float('inf') + elif value == _NAN: + return float('nan') + else: + raise ParseError("Couldn't parse float: {0}".format(value)) from e + + +def _ConvertBool(value, require_str): + """Convert a boolean value. + + Args: + value: A scalar value to convert. + require_str: If True, value must be a str. + + Returns: + The bool parsed. + + Raises: + ParseError: If a boolean value couldn't be consumed. + """ + if require_str: + if value == 'true': + return True + elif value == 'false': + return False + else: + raise ParseError('Expected "true" or "false", not {0}'.format(value)) + + if not isinstance(value, bool): + raise ParseError('Expected true or false without quotes') + return value + + +_WKTJSONMETHODS = { + 'google.protobuf.Any': ['_AnyMessageToJsonObject', '_ConvertAnyMessage'], + 'google.protobuf.Duration': [ + '_GenericMessageToJsonObject', + '_ConvertGenericMessage', + ], + 'google.protobuf.FieldMask': [ + '_GenericMessageToJsonObject', + '_ConvertGenericMessage', + ], + 'google.protobuf.ListValue': [ + '_ListValueMessageToJsonObject', + '_ConvertListValueMessage', + ], + 'google.protobuf.Struct': [ + '_StructMessageToJsonObject', + '_ConvertStructMessage', + ], + 'google.protobuf.Timestamp': [ + '_GenericMessageToJsonObject', + '_ConvertGenericMessage', + ], + 'google.protobuf.Value': [ + '_ValueMessageToJsonObject', + '_ConvertValueMessage', + ], +} diff --git a/google/protobuf/message.py b/google/protobuf/message.py new file mode 100644 index 0000000..3226b6e --- /dev/null +++ b/google/protobuf/message.py @@ -0,0 +1,394 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +# TODO: We should just make these methods all "pure-virtual" and move +# all implementation out, into reflection.py for now. + + +"""Contains an abstract base class for protocol messages.""" + +__author__ = 'robinson@google.com (Will Robinson)' + +class Error(Exception): + """Base error type for this module.""" + pass + + +class DecodeError(Error): + """Exception raised when deserializing messages.""" + pass + + +class EncodeError(Error): + """Exception raised when serializing messages.""" + pass + + +class Message(object): + + """Abstract base class for protocol messages. + + Protocol message classes are almost always generated by the protocol + compiler. These generated types subclass Message and implement the methods + shown below. + """ + + # TODO: Link to an HTML document here. + + # TODO: Document that instances of this class will also + # have an Extensions attribute with __getitem__ and __setitem__. + # Again, not sure how to best convey this. + + # TODO: Document these fields and methods. + + __slots__ = [] + + #: The :class:`google.protobuf.Descriptor` + # for this message type. + DESCRIPTOR = None + + def __deepcopy__(self, memo=None): + clone = type(self)() + clone.MergeFrom(self) + return clone + + def __eq__(self, other_msg): + """Recursively compares two messages by value and structure.""" + raise NotImplementedError + + def __ne__(self, other_msg): + # Can't just say self != other_msg, since that would infinitely recurse. :) + return not self == other_msg + + def __hash__(self): + raise TypeError('unhashable object') + + def __str__(self): + """Outputs a human-readable representation of the message.""" + raise NotImplementedError + + def __unicode__(self): + """Outputs a human-readable representation of the message.""" + raise NotImplementedError + + def MergeFrom(self, other_msg): + """Merges the contents of the specified message into current message. + + This method merges the contents of the specified message into the current + message. Singular fields that are set in the specified message overwrite + the corresponding fields in the current message. Repeated fields are + appended. Singular sub-messages and groups are recursively merged. + + Args: + other_msg (Message): A message to merge into the current message. + """ + raise NotImplementedError + + def CopyFrom(self, other_msg): + """Copies the content of the specified message into the current message. + + The method clears the current message and then merges the specified + message using MergeFrom. + + Args: + other_msg (Message): A message to copy into the current one. + """ + if self is other_msg: + return + self.Clear() + self.MergeFrom(other_msg) + + def Clear(self): + """Clears all data that was set in the message.""" + raise NotImplementedError + + def SetInParent(self): + """Mark this as present in the parent. + + This normally happens automatically when you assign a field of a + sub-message, but sometimes you want to make the sub-message + present while keeping it empty. If you find yourself using this, + you may want to reconsider your design. + """ + raise NotImplementedError + + def IsInitialized(self): + """Checks if the message is initialized. + + Returns: + bool: The method returns True if the message is initialized (i.e. all of + its required fields are set). + """ + raise NotImplementedError + + # TODO: MergeFromString() should probably return None and be + # implemented in terms of a helper that returns the # of bytes read. Our + # deserialization routines would use the helper when recursively + # deserializing, but the end user would almost always just want the no-return + # MergeFromString(). + + def MergeFromString(self, serialized): + """Merges serialized protocol buffer data into this message. + + When we find a field in `serialized` that is already present + in this message: + + - If it's a "repeated" field, we append to the end of our list. + - Else, if it's a scalar, we overwrite our field. + - Else, (it's a nonrepeated composite), we recursively merge + into the existing composite. + + Args: + serialized (bytes): Any object that allows us to call + ``memoryview(serialized)`` to access a string of bytes using the + buffer interface. + + Returns: + int: The number of bytes read from `serialized`. + For non-group messages, this will always be `len(serialized)`, + but for messages which are actually groups, this will + generally be less than `len(serialized)`, since we must + stop when we reach an ``END_GROUP`` tag. Note that if + we *do* stop because of an ``END_GROUP`` tag, the number + of bytes returned does not include the bytes + for the ``END_GROUP`` tag information. + + Raises: + DecodeError: if the input cannot be parsed. + """ + # TODO: Document handling of unknown fields. + # TODO: When we switch to a helper, this will return None. + raise NotImplementedError + + def ParseFromString(self, serialized): + """Parse serialized protocol buffer data in binary form into this message. + + Like :func:`MergeFromString()`, except we clear the object first. + + Raises: + message.DecodeError if the input cannot be parsed. + """ + self.Clear() + return self.MergeFromString(serialized) + + def SerializeToString(self, **kwargs): + """Serializes the protocol message to a binary string. + + Keyword Args: + deterministic (bool): If true, requests deterministic serialization + of the protobuf, with predictable ordering of map keys. + + Returns: + A binary string representation of the message if all of the required + fields in the message are set (i.e. the message is initialized). + + Raises: + EncodeError: if the message isn't initialized (see :func:`IsInitialized`). + """ + raise NotImplementedError + + def SerializePartialToString(self, **kwargs): + """Serializes the protocol message to a binary string. + + This method is similar to SerializeToString but doesn't check if the + message is initialized. + + Keyword Args: + deterministic (bool): If true, requests deterministic serialization + of the protobuf, with predictable ordering of map keys. + + Returns: + bytes: A serialized representation of the partial message. + """ + raise NotImplementedError + + # TODO: Decide whether we like these better + # than auto-generated has_foo() and clear_foo() methods + # on the instances themselves. This way is less consistent + # with C++, but it makes reflection-type access easier and + # reduces the number of magically autogenerated things. + # + # TODO: Be sure to document (and test) exactly + # which field names are accepted here. Are we case-sensitive? + # What do we do with fields that share names with Python keywords + # like 'lambda' and 'yield'? + # + # nnorwitz says: + # """ + # Typically (in python), an underscore is appended to names that are + # keywords. So they would become lambda_ or yield_. + # """ + def ListFields(self): + """Returns a list of (FieldDescriptor, value) tuples for present fields. + + A message field is non-empty if HasField() would return true. A singular + primitive field is non-empty if HasField() would return true in proto2 or it + is non zero in proto3. A repeated field is non-empty if it contains at least + one element. The fields are ordered by field number. + + Returns: + list[tuple(FieldDescriptor, value)]: field descriptors and values + for all fields in the message which are not empty. The values vary by + field type. + """ + raise NotImplementedError + + def HasField(self, field_name): + """Checks if a certain field is set for the message. + + For a oneof group, checks if any field inside is set. Note that if the + field_name is not defined in the message descriptor, :exc:`ValueError` will + be raised. + + Args: + field_name (str): The name of the field to check for presence. + + Returns: + bool: Whether a value has been set for the named field. + + Raises: + ValueError: if the `field_name` is not a member of this message. + """ + raise NotImplementedError + + def ClearField(self, field_name): + """Clears the contents of a given field. + + Inside a oneof group, clears the field set. If the name neither refers to a + defined field or oneof group, :exc:`ValueError` is raised. + + Args: + field_name (str): The name of the field to check for presence. + + Raises: + ValueError: if the `field_name` is not a member of this message. + """ + raise NotImplementedError + + def WhichOneof(self, oneof_group): + """Returns the name of the field that is set inside a oneof group. + + If no field is set, returns None. + + Args: + oneof_group (str): the name of the oneof group to check. + + Returns: + str or None: The name of the group that is set, or None. + + Raises: + ValueError: no group with the given name exists + """ + raise NotImplementedError + + def HasExtension(self, field_descriptor): + """Checks if a certain extension is present for this message. + + Extensions are retrieved using the :attr:`Extensions` mapping (if present). + + Args: + field_descriptor: The field descriptor for the extension to check. + + Returns: + bool: Whether the extension is present for this message. + + Raises: + KeyError: if the extension is repeated. Similar to repeated fields, + there is no separate notion of presence: a "not present" repeated + extension is an empty list. + """ + raise NotImplementedError + + def ClearExtension(self, field_descriptor): + """Clears the contents of a given extension. + + Args: + field_descriptor: The field descriptor for the extension to clear. + """ + raise NotImplementedError + + def UnknownFields(self): + """Returns the UnknownFieldSet. + + Returns: + UnknownFieldSet: The unknown fields stored in this message. + """ + raise NotImplementedError + + def DiscardUnknownFields(self): + """Clears all fields in the :class:`UnknownFieldSet`. + + This operation is recursive for nested message. + """ + raise NotImplementedError + + def ByteSize(self): + """Returns the serialized size of this message. + + Recursively calls ByteSize() on all contained messages. + + Returns: + int: The number of bytes required to serialize this message. + """ + raise NotImplementedError + + @classmethod + def FromString(cls, s): + raise NotImplementedError + + def _SetListener(self, message_listener): + """Internal method used by the protocol message implementation. + Clients should not call this directly. + + Sets a listener that this message will call on certain state transitions. + + The purpose of this method is to register back-edges from children to + parents at runtime, for the purpose of setting "has" bits and + byte-size-dirty bits in the parent and ancestor objects whenever a child or + descendant object is modified. + + If the client wants to disconnect this Message from the object tree, she + explicitly sets callback to None. + + If message_listener is None, unregisters any existing listener. Otherwise, + message_listener must implement the MessageListener interface in + internal/message_listener.py, and we discard any listener registered + via a previous _SetListener() call. + """ + raise NotImplementedError + + def __getstate__(self): + """Support the pickle protocol.""" + return dict(serialized=self.SerializePartialToString()) + + def __setstate__(self, state): + """Support the pickle protocol.""" + self.__init__() + serialized = state['serialized'] + # On Python 3, using encoding='latin1' is required for unpickling + # protos pickled by Python 2. + if not isinstance(serialized, bytes): + serialized = serialized.encode('latin1') + self.ParseFromString(serialized) + + def __reduce__(self): + message_descriptor = self.DESCRIPTOR + if message_descriptor.containing_type is None: + return type(self), (), self.__getstate__() + # the message type must be nested. + # Python does not pickle nested classes; use the symbol_database on the + # receiving end. + container = message_descriptor + return (_InternalConstructMessage, (container.full_name,), + self.__getstate__()) + + +def _InternalConstructMessage(full_name): + """Constructs a nested message.""" + from google.protobuf import symbol_database # pylint:disable=g-import-not-at-top + + return symbol_database.Default().GetSymbol(full_name)() diff --git a/google/protobuf/message_factory.py b/google/protobuf/message_factory.py new file mode 100644 index 0000000..56fff6d --- /dev/null +++ b/google/protobuf/message_factory.py @@ -0,0 +1,233 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Provides a factory class for generating dynamic messages. + +The easiest way to use this class is if you have access to the FileDescriptor +protos containing the messages you want to create you can just do the following: + +message_classes = message_factory.GetMessages(iterable_of_file_descriptors) +my_proto_instance = message_classes['some.proto.package.MessageName']() +""" + +__author__ = 'matthewtoia@google.com (Matt Toia)' + +import warnings + +from google.protobuf.internal import api_implementation +from google.protobuf import descriptor_pool +from google.protobuf import message + +if api_implementation.Type() == 'python': + from google.protobuf.internal import python_message as message_impl +else: + from google.protobuf.pyext import cpp_message as message_impl # pylint: disable=g-import-not-at-top + + +# The type of all Message classes. +_GENERATED_PROTOCOL_MESSAGE_TYPE = message_impl.GeneratedProtocolMessageType + + +def GetMessageClass(descriptor): + """Obtains a proto2 message class based on the passed in descriptor. + + Passing a descriptor with a fully qualified name matching a previous + invocation will cause the same class to be returned. + + Args: + descriptor: The descriptor to build from. + + Returns: + A class describing the passed in descriptor. + """ + concrete_class = getattr(descriptor, '_concrete_class', None) + if concrete_class: + return concrete_class + return _InternalCreateMessageClass(descriptor) + + +def GetMessageClassesForFiles(files, pool): + """Gets all the messages from specified files. + + This will find and resolve dependencies, failing if the descriptor + pool cannot satisfy them. + + Args: + files: The file names to extract messages from. + pool: The descriptor pool to find the files including the dependent + files. + + Returns: + A dictionary mapping proto names to the message classes. + """ + result = {} + for file_name in files: + file_desc = pool.FindFileByName(file_name) + for desc in file_desc.message_types_by_name.values(): + result[desc.full_name] = GetMessageClass(desc) + + # While the extension FieldDescriptors are created by the descriptor pool, + # the python classes created in the factory need them to be registered + # explicitly, which is done below. + # + # The call to RegisterExtension will specifically check if the + # extension was already registered on the object and either + # ignore the registration if the original was the same, or raise + # an error if they were different. + + for extension in file_desc.extensions_by_name.values(): + extended_class = GetMessageClass(extension.containing_type) + if api_implementation.Type() != 'python': + # TODO: Remove this check here. Duplicate extension + # register check should be in descriptor_pool. + if extension is not pool.FindExtensionByNumber( + extension.containing_type, extension.number + ): + raise ValueError('Double registration of Extensions') + # Recursively load protos for extension field, in order to be able to + # fully represent the extension. This matches the behavior for regular + # fields too. + if extension.message_type: + GetMessageClass(extension.message_type) + return result + + +def _InternalCreateMessageClass(descriptor): + """Builds a proto2 message class based on the passed in descriptor. + + Args: + descriptor: The descriptor to build from. + + Returns: + A class describing the passed in descriptor. + """ + descriptor_name = descriptor.name + result_class = _GENERATED_PROTOCOL_MESSAGE_TYPE( + descriptor_name, + (message.Message,), + { + 'DESCRIPTOR': descriptor, + # If module not set, it wrongly points to message_factory module. + '__module__': None, + }) + for field in descriptor.fields: + if field.message_type: + GetMessageClass(field.message_type) + for extension in result_class.DESCRIPTOR.extensions: + extended_class = GetMessageClass(extension.containing_type) + if api_implementation.Type() != 'python': + # TODO: Remove this check here. Duplicate extension + # register check should be in descriptor_pool. + pool = extension.containing_type.file.pool + if extension is not pool.FindExtensionByNumber( + extension.containing_type, extension.number + ): + raise ValueError('Double registration of Extensions') + if extension.message_type: + GetMessageClass(extension.message_type) + return result_class + + +# Deprecated. Please use GetMessageClass() or GetMessageClassesForFiles() +# method above instead. +class MessageFactory(object): + """Factory for creating Proto2 messages from descriptors in a pool.""" + + def __init__(self, pool=None): + """Initializes a new factory.""" + self.pool = pool or descriptor_pool.DescriptorPool() + + def GetPrototype(self, descriptor): + """Obtains a proto2 message class based on the passed in descriptor. + + Passing a descriptor with a fully qualified name matching a previous + invocation will cause the same class to be returned. + + Args: + descriptor: The descriptor to build from. + + Returns: + A class describing the passed in descriptor. + """ + warnings.warn( + 'MessageFactory class is deprecated. Please use ' + 'GetMessageClass() instead of MessageFactory.GetPrototype. ' + 'MessageFactory class will be removed after 2024.', + stacklevel=2, + ) + return GetMessageClass(descriptor) + + def CreatePrototype(self, descriptor): + """Builds a proto2 message class based on the passed in descriptor. + + Don't call this function directly, it always creates a new class. Call + GetMessageClass() instead. + + Args: + descriptor: The descriptor to build from. + + Returns: + A class describing the passed in descriptor. + """ + warnings.warn( + 'Directly call CreatePrototype is wrong. Please use ' + 'GetMessageClass() method instead. Directly use ' + 'CreatePrototype will raise error after July 2023.', + stacklevel=2, + ) + return _InternalCreateMessageClass(descriptor) + + def GetMessages(self, files): + """Gets all the messages from a specified file. + + This will find and resolve dependencies, failing if the descriptor + pool cannot satisfy them. + + Args: + files: The file names to extract messages from. + + Returns: + A dictionary mapping proto names to the message classes. This will include + any dependent messages as well as any messages defined in the same file as + a specified message. + """ + warnings.warn( + 'MessageFactory class is deprecated. Please use ' + 'GetMessageClassesForFiles() instead of ' + 'MessageFactory.GetMessages(). MessageFactory class ' + 'will be removed after 2024.', + stacklevel=2, + ) + return GetMessageClassesForFiles(files, self.pool) + + +def GetMessages(file_protos, pool=None): + """Builds a dictionary of all the messages available in a set of files. + + Args: + file_protos: Iterable of FileDescriptorProto to build messages out of. + pool: The descriptor pool to add the file protos. + + Returns: + A dictionary mapping proto names to the message classes. This will include + any dependent messages as well as any messages defined in the same file as + a specified message. + """ + # The cpp implementation of the protocol buffer library requires to add the + # message in topological order of the dependency graph. + des_pool = pool or descriptor_pool.DescriptorPool() + file_by_name = {file_proto.name: file_proto for file_proto in file_protos} + def _AddFile(file_proto): + for dependency in file_proto.dependency: + if dependency in file_by_name: + # Remove from elements to be visited, in order to cut cycles. + _AddFile(file_by_name.pop(dependency)) + des_pool.Add(file_proto) + while file_by_name: + _AddFile(file_by_name.popitem()[1]) + return GetMessageClassesForFiles( + [file_proto.name for file_proto in file_protos], des_pool) diff --git a/google/protobuf/proto_builder.py b/google/protobuf/proto_builder.py new file mode 100644 index 0000000..803d004 --- /dev/null +++ b/google/protobuf/proto_builder.py @@ -0,0 +1,111 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Dynamic Protobuf class creator.""" + +from collections import OrderedDict +import hashlib +import os + +from google.protobuf import descriptor_pb2 +from google.protobuf import descriptor +from google.protobuf import descriptor_pool +from google.protobuf import message_factory + + +def _GetMessageFromFactory(pool, full_name): + """Get a proto class from the MessageFactory by name. + + Args: + pool: a descriptor pool. + full_name: str, the fully qualified name of the proto type. + Returns: + A class, for the type identified by full_name. + Raises: + KeyError, if the proto is not found in the factory's descriptor pool. + """ + proto_descriptor = pool.FindMessageTypeByName(full_name) + proto_cls = message_factory.GetMessageClass(proto_descriptor) + return proto_cls + + +def MakeSimpleProtoClass(fields, full_name=None, pool=None): + """Create a Protobuf class whose fields are basic types. + + Note: this doesn't validate field names! + + Args: + fields: dict of {name: field_type} mappings for each field in the proto. If + this is an OrderedDict the order will be maintained, otherwise the + fields will be sorted by name. + full_name: optional str, the fully-qualified name of the proto type. + pool: optional DescriptorPool instance. + Returns: + a class, the new protobuf class with a FileDescriptor. + """ + pool_instance = pool or descriptor_pool.DescriptorPool() + if full_name is not None: + try: + proto_cls = _GetMessageFromFactory(pool_instance, full_name) + return proto_cls + except KeyError: + # The factory's DescriptorPool doesn't know about this class yet. + pass + + # Get a list of (name, field_type) tuples from the fields dict. If fields was + # an OrderedDict we keep the order, but otherwise we sort the field to ensure + # consistent ordering. + field_items = fields.items() + if not isinstance(fields, OrderedDict): + field_items = sorted(field_items) + + # Use a consistent file name that is unlikely to conflict with any imported + # proto files. + fields_hash = hashlib.sha1() + for f_name, f_type in field_items: + fields_hash.update(f_name.encode('utf-8')) + fields_hash.update(str(f_type).encode('utf-8')) + proto_file_name = fields_hash.hexdigest() + '.proto' + + # If the proto is anonymous, use the same hash to name it. + if full_name is None: + full_name = ('net.proto2.python.public.proto_builder.AnonymousProto_' + + fields_hash.hexdigest()) + try: + proto_cls = _GetMessageFromFactory(pool_instance, full_name) + return proto_cls + except KeyError: + # The factory's DescriptorPool doesn't know about this class yet. + pass + + # This is the first time we see this proto: add a new descriptor to the pool. + pool_instance.Add( + _MakeFileDescriptorProto(proto_file_name, full_name, field_items)) + return _GetMessageFromFactory(pool_instance, full_name) + + +def _MakeFileDescriptorProto(proto_file_name, full_name, field_items): + """Populate FileDescriptorProto for MessageFactory's DescriptorPool.""" + package, name = full_name.rsplit('.', 1) + file_proto = descriptor_pb2.FileDescriptorProto() + file_proto.name = os.path.join(package.replace('.', '/'), proto_file_name) + file_proto.package = package + desc_proto = file_proto.message_type.add() + desc_proto.name = name + for f_number, (f_name, f_type) in enumerate(field_items, 1): + field_proto = desc_proto.field.add() + field_proto.name = f_name + # # If the number falls in the reserved range, reassign it to the correct + # # number after the range. + if f_number >= descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER: + f_number += ( + descriptor.FieldDescriptor.LAST_RESERVED_FIELD_NUMBER - + descriptor.FieldDescriptor.FIRST_RESERVED_FIELD_NUMBER + 1) + field_proto.number = f_number + field_proto.label = descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL + field_proto.type = f_type + return file_proto diff --git a/google/protobuf/pyext/__init__.py b/google/protobuf/pyext/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/google/protobuf/pyext/cpp_message.py b/google/protobuf/pyext/cpp_message.py new file mode 100644 index 0000000..623b52f --- /dev/null +++ b/google/protobuf/pyext/cpp_message.py @@ -0,0 +1,49 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Protocol message implementation hooks for C++ implementation. + +Contains helper functions used to create protocol message classes from +Descriptor objects at runtime backed by the protocol buffer C++ API. +""" + +__author__ = 'tibell@google.com (Johan Tibell)' + +from google.protobuf.internal import api_implementation + + +# pylint: disable=protected-access +_message = api_implementation._c_module +# TODO: Remove this import after fix api_implementation +if _message is None: + from google.protobuf.pyext import _message + + +class GeneratedProtocolMessageType(_message.MessageMeta): + + """Metaclass for protocol message classes created at runtime from Descriptors. + + The protocol compiler currently uses this metaclass to create protocol + message classes at runtime. Clients can also manually create their own + classes at runtime, as in this example: + + mydescriptor = Descriptor(.....) + factory = symbol_database.Default() + factory.pool.AddDescriptor(mydescriptor) + MyProtoClass = factory.GetPrototype(mydescriptor) + myproto_instance = MyProtoClass() + myproto.foo_field = 23 + ... + + The above example will not work for nested types. If you wish to include them, + use reflection.MakeClass() instead of manually instantiating the class in + order to create the appropriate class structure. + """ + + # Must be consistent with the protocol-compiler code in + # proto2/compiler/internal/generator.*. + _DESCRIPTOR_KEY = 'DESCRIPTOR' diff --git a/google/protobuf/reflection.py b/google/protobuf/reflection.py new file mode 100644 index 0000000..2089f01 --- /dev/null +++ b/google/protobuf/reflection.py @@ -0,0 +1,72 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +# This code is meant to work on Python 2.4 and above only. + +"""Contains a metaclass and helper functions used to create +protocol message classes from Descriptor objects at runtime. + +Recall that a metaclass is the "type" of a class. +(A class is to a metaclass what an instance is to a class.) + +In this case, we use the GeneratedProtocolMessageType metaclass +to inject all the useful functionality into the classes +output by the protocol compiler at compile-time. + +The upshot of all this is that the real implementation +details for ALL pure-Python protocol buffers are *here in +this file*. +""" + +__author__ = 'robinson@google.com (Will Robinson)' + + +from google.protobuf import message_factory +from google.protobuf import symbol_database + +# The type of all Message classes. +# Part of the public interface, but normally only used by message factories. +GeneratedProtocolMessageType = message_factory._GENERATED_PROTOCOL_MESSAGE_TYPE + +MESSAGE_CLASS_CACHE = {} + + +# Deprecated. Please NEVER use reflection.ParseMessage(). +def ParseMessage(descriptor, byte_str): + """Generate a new Message instance from this Descriptor and a byte string. + + DEPRECATED: ParseMessage is deprecated because it is using MakeClass(). + Please use MessageFactory.GetPrototype() instead. + + Args: + descriptor: Protobuf Descriptor object + byte_str: Serialized protocol buffer byte string + + Returns: + Newly created protobuf Message object. + """ + result_class = MakeClass(descriptor) + new_msg = result_class() + new_msg.ParseFromString(byte_str) + return new_msg + + +# Deprecated. Please NEVER use reflection.MakeClass(). +def MakeClass(descriptor): + """Construct a class object for a protobuf described by descriptor. + + DEPRECATED: use MessageFactory.GetPrototype() instead. + + Args: + descriptor: A descriptor.Descriptor object describing the protobuf. + Returns: + The Message class object described by the descriptor. + """ + # Original implementation leads to duplicate message classes, which won't play + # well with extensions. Message factory info is also missing. + # Redirect to message_factory. + return message_factory.GetMessageClass(descriptor) diff --git a/google/protobuf/runtime_version.py b/google/protobuf/runtime_version.py new file mode 100644 index 0000000..36d67dc --- /dev/null +++ b/google/protobuf/runtime_version.py @@ -0,0 +1,97 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Protobuf Runtime versions and validators. + +It should only be accessed by Protobuf gencodes and tests. DO NOT USE it +elsewhere. +""" + +__author__ = 'shaod@google.com (Dennis Shao)' + +from enum import Enum +import os + + +class Domain(Enum): + GOOGLE_INTERNAL = 1 + PUBLIC = 2 + + +class VersionError(Exception): + """Exception class for version violation.""" + + +# The versions of this Python Protobuf runtime to be changed automatically by +# the Protobuf release process. Do not edit them manually. +DOMAIN = Domain.PUBLIC +MAJOR = 5 +MINOR = 27 +PATCH = 3 +SUFFIX = '' + + +def ValidateProtobufRuntimeVersion( + gen_domain, gen_major, gen_minor, gen_patch, gen_suffix, location +): + """Function to validate versions. + + Args: + gen_domain: The domain where the code was generated from. + gen_major: The major version number of the gencode. + gen_minor: The minor version number of the gencode. + gen_patch: The patch version number of the gencode. + gen_suffix: The version suffix e.g. '-dev', '-rc1' of the gencode. + location: The proto location that causes the version violation. + + Raises: + VersionError: if gencode version is invalid or incompatible with the + runtime. + """ + + disable_flag = os.getenv('TEMORARILY_DISABLE_PROTOBUF_VERSION_CHECK') + if disable_flag is not None and disable_flag.lower() == 'true': + return + + version = f'{MAJOR}.{MINOR}.{PATCH}{SUFFIX}' + gen_version = f'{gen_major}.{gen_minor}.{gen_patch}{gen_suffix}' + + if gen_major < 0 or gen_minor < 0 or gen_patch < 0: + raise VersionError(f'Invalid gencode version: {gen_version}') + + error_prompt = ( + 'See Protobuf version guarantees at' + ' https://protobuf.dev/support/cross-version-runtime-guarantee.' + ) + + if gen_domain != DOMAIN: + raise VersionError( + 'Detected mismatched Protobuf Gencode/Runtime domains when loading' + f' {location}: gencode {gen_domain.name} runtime {DOMAIN.name}.' + ' Cross-domain usage of Protobuf is not supported.' + ) + + if gen_major != MAJOR: + raise VersionError( + 'Detected mismatched Protobuf Gencode/Runtime major versions when' + f' loading {location}: gencode {gen_version} runtime {version}.' + f' Same major version is required. {error_prompt}' + ) + + if MINOR < gen_minor or (MINOR == gen_minor and PATCH < gen_patch): + raise VersionError( + 'Detected incompatible Protobuf Gencode/Runtime versions when loading' + f' {location}: gencode {gen_version} runtime {version}. Runtime version' + f' cannot be older than the linked gencode version. {error_prompt}' + ) + + if gen_suffix != SUFFIX: + raise VersionError( + 'Detected mismatched Protobuf Gencode/Runtime version suffixes when' + f' loading {location}: gencode {gen_version} runtime {version}.' + f' Version suffixes must be the same. {error_prompt}' + ) diff --git a/google/protobuf/service.py b/google/protobuf/service.py new file mode 100644 index 0000000..d3e1920 --- /dev/null +++ b/google/protobuf/service.py @@ -0,0 +1,205 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""DEPRECATED: Declares the RPC service interfaces. + +This module declares the abstract interfaces underlying proto2 RPC +services. These are intended to be independent of any particular RPC +implementation, so that proto2 services can be used on top of a variety +of implementations. Starting with version 2.3.0, RPC implementations should +not try to build on these, but should instead provide code generator plugins +which generate code specific to the particular RPC implementation. This way +the generated code can be more appropriate for the implementation in use +and can avoid unnecessary layers of indirection. +""" + +__author__ = 'petar@google.com (Petar Petrov)' + + +class RpcException(Exception): + """Exception raised on failed blocking RPC method call.""" + pass + + +class Service(object): + + """Abstract base interface for protocol-buffer-based RPC services. + + Services themselves are abstract classes (implemented either by servers or as + stubs), but they subclass this base interface. The methods of this + interface can be used to call the methods of the service without knowing + its exact type at compile time (analogous to the Message interface). + """ + + def GetDescriptor(): + """Retrieves this service's descriptor.""" + raise NotImplementedError + + def CallMethod(self, method_descriptor, rpc_controller, + request, done): + """Calls a method of the service specified by method_descriptor. + + If "done" is None then the call is blocking and the response + message will be returned directly. Otherwise the call is asynchronous + and "done" will later be called with the response value. + + In the blocking case, RpcException will be raised on error. + + Preconditions: + + * method_descriptor.service == GetDescriptor + * request is of the exact same classes as returned by + GetRequestClass(method). + * After the call has started, the request must not be modified. + * "rpc_controller" is of the correct type for the RPC implementation being + used by this Service. For stubs, the "correct type" depends on the + RpcChannel which the stub is using. + + Postconditions: + + * "done" will be called when the method is complete. This may be + before CallMethod() returns or it may be at some point in the future. + * If the RPC failed, the response value passed to "done" will be None. + Further details about the failure can be found by querying the + RpcController. + """ + raise NotImplementedError + + def GetRequestClass(self, method_descriptor): + """Returns the class of the request message for the specified method. + + CallMethod() requires that the request is of a particular subclass of + Message. GetRequestClass() gets the default instance of this required + type. + + Example: + method = service.GetDescriptor().FindMethodByName("Foo") + request = stub.GetRequestClass(method)() + request.ParseFromString(input) + service.CallMethod(method, request, callback) + """ + raise NotImplementedError + + def GetResponseClass(self, method_descriptor): + """Returns the class of the response message for the specified method. + + This method isn't really needed, as the RpcChannel's CallMethod constructs + the response protocol message. It's provided anyway in case it is useful + for the caller to know the response type in advance. + """ + raise NotImplementedError + + +class RpcController(object): + + """An RpcController mediates a single method call. + + The primary purpose of the controller is to provide a way to manipulate + settings specific to the RPC implementation and to find out about RPC-level + errors. The methods provided by the RpcController interface are intended + to be a "least common denominator" set of features which we expect all + implementations to support. Specific implementations may provide more + advanced features (e.g. deadline propagation). + """ + + # Client-side methods below + + def Reset(self): + """Resets the RpcController to its initial state. + + After the RpcController has been reset, it may be reused in + a new call. Must not be called while an RPC is in progress. + """ + raise NotImplementedError + + def Failed(self): + """Returns true if the call failed. + + After a call has finished, returns true if the call failed. The possible + reasons for failure depend on the RPC implementation. Failed() must not + be called before a call has finished. If Failed() returns true, the + contents of the response message are undefined. + """ + raise NotImplementedError + + def ErrorText(self): + """If Failed is true, returns a human-readable description of the error.""" + raise NotImplementedError + + def StartCancel(self): + """Initiate cancellation. + + Advises the RPC system that the caller desires that the RPC call be + canceled. The RPC system may cancel it immediately, may wait awhile and + then cancel it, or may not even cancel the call at all. If the call is + canceled, the "done" callback will still be called and the RpcController + will indicate that the call failed at that time. + """ + raise NotImplementedError + + # Server-side methods below + + def SetFailed(self, reason): + """Sets a failure reason. + + Causes Failed() to return true on the client side. "reason" will be + incorporated into the message returned by ErrorText(). If you find + you need to return machine-readable information about failures, you + should incorporate it into your response protocol buffer and should + NOT call SetFailed(). + """ + raise NotImplementedError + + def IsCanceled(self): + """Checks if the client cancelled the RPC. + + If true, indicates that the client canceled the RPC, so the server may + as well give up on replying to it. The server should still call the + final "done" callback. + """ + raise NotImplementedError + + def NotifyOnCancel(self, callback): + """Sets a callback to invoke on cancel. + + Asks that the given callback be called when the RPC is canceled. The + callback will always be called exactly once. If the RPC completes without + being canceled, the callback will be called after completion. If the RPC + has already been canceled when NotifyOnCancel() is called, the callback + will be called immediately. + + NotifyOnCancel() must be called no more than once per request. + """ + raise NotImplementedError + + +class RpcChannel(object): + + """Abstract interface for an RPC channel. + + An RpcChannel represents a communication line to a service which can be used + to call that service's methods. The service may be running on another + machine. Normally, you should not use an RpcChannel directly, but instead + construct a stub {@link Service} wrapping it. Example: + + Example: + RpcChannel channel = rpcImpl.Channel("remotehost.example.com:1234") + RpcController controller = rpcImpl.Controller() + MyService service = MyService_Stub(channel) + service.MyMethod(controller, request, callback) + """ + + def CallMethod(self, method_descriptor, rpc_controller, + request, response_class, done): + """Calls the method identified by the descriptor. + + Call the given method of the remote service. The signature of this + procedure looks the same as Service.CallMethod(), but the requirements + are less strict in one important way: the request object doesn't have to + be of any specific class as long as its descriptor is method.input_type. + """ + raise NotImplementedError diff --git a/google/protobuf/service_reflection.py b/google/protobuf/service_reflection.py new file mode 100644 index 0000000..7ba3d0b --- /dev/null +++ b/google/protobuf/service_reflection.py @@ -0,0 +1,272 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Contains metaclasses used to create protocol service and service stub +classes from ServiceDescriptor objects at runtime. + +The GeneratedServiceType and GeneratedServiceStubType metaclasses are used to +inject all useful functionality into the classes output by the protocol +compiler at compile-time. +""" + +__author__ = 'petar@google.com (Petar Petrov)' + + +class GeneratedServiceType(type): + + """Metaclass for service classes created at runtime from ServiceDescriptors. + + Implementations for all methods described in the Service class are added here + by this class. We also create properties to allow getting/setting all fields + in the protocol message. + + The protocol compiler currently uses this metaclass to create protocol service + classes at runtime. Clients can also manually create their own classes at + runtime, as in this example:: + + mydescriptor = ServiceDescriptor(.....) + class MyProtoService(service.Service): + __metaclass__ = GeneratedServiceType + DESCRIPTOR = mydescriptor + myservice_instance = MyProtoService() + # ... + """ + + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __init__(cls, name, bases, dictionary): + """Creates a message service class. + + Args: + name: Name of the class (ignored, but required by the metaclass + protocol). + bases: Base classes of the class being constructed. + dictionary: The class dictionary of the class being constructed. + dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object + describing this protocol service type. + """ + # Don't do anything if this class doesn't have a descriptor. This happens + # when a service class is subclassed. + if GeneratedServiceType._DESCRIPTOR_KEY not in dictionary: + return + + descriptor = dictionary[GeneratedServiceType._DESCRIPTOR_KEY] + service_builder = _ServiceBuilder(descriptor) + service_builder.BuildService(cls) + cls.DESCRIPTOR = descriptor + + +class GeneratedServiceStubType(GeneratedServiceType): + + """Metaclass for service stubs created at runtime from ServiceDescriptors. + + This class has similar responsibilities as GeneratedServiceType, except that + it creates the service stub classes. + """ + + _DESCRIPTOR_KEY = 'DESCRIPTOR' + + def __init__(cls, name, bases, dictionary): + """Creates a message service stub class. + + Args: + name: Name of the class (ignored, here). + bases: Base classes of the class being constructed. + dictionary: The class dictionary of the class being constructed. + dictionary[_DESCRIPTOR_KEY] must contain a ServiceDescriptor object + describing this protocol service type. + """ + super(GeneratedServiceStubType, cls).__init__(name, bases, dictionary) + # Don't do anything if this class doesn't have a descriptor. This happens + # when a service stub is subclassed. + if GeneratedServiceStubType._DESCRIPTOR_KEY not in dictionary: + return + + descriptor = dictionary[GeneratedServiceStubType._DESCRIPTOR_KEY] + service_stub_builder = _ServiceStubBuilder(descriptor) + service_stub_builder.BuildServiceStub(cls) + + +class _ServiceBuilder(object): + + """This class constructs a protocol service class using a service descriptor. + + Given a service descriptor, this class constructs a class that represents + the specified service descriptor. One service builder instance constructs + exactly one service class. That means all instances of that class share the + same builder. + """ + + def __init__(self, service_descriptor): + """Initializes an instance of the service class builder. + + Args: + service_descriptor: ServiceDescriptor to use when constructing the + service class. + """ + self.descriptor = service_descriptor + + def BuildService(builder, cls): + """Constructs the service class. + + Args: + cls: The class that will be constructed. + """ + + # CallMethod needs to operate with an instance of the Service class. This + # internal wrapper function exists only to be able to pass the service + # instance to the method that does the real CallMethod work. + # Making sure to use exact argument names from the abstract interface in + # service.py to match the type signature + def _WrapCallMethod(self, method_descriptor, rpc_controller, request, done): + return builder._CallMethod(self, method_descriptor, rpc_controller, + request, done) + + def _WrapGetRequestClass(self, method_descriptor): + return builder._GetRequestClass(method_descriptor) + + def _WrapGetResponseClass(self, method_descriptor): + return builder._GetResponseClass(method_descriptor) + + builder.cls = cls + cls.CallMethod = _WrapCallMethod + cls.GetDescriptor = staticmethod(lambda: builder.descriptor) + cls.GetDescriptor.__doc__ = 'Returns the service descriptor.' + cls.GetRequestClass = _WrapGetRequestClass + cls.GetResponseClass = _WrapGetResponseClass + for method in builder.descriptor.methods: + setattr(cls, method.name, builder._GenerateNonImplementedMethod(method)) + + def _CallMethod(self, srvc, method_descriptor, + rpc_controller, request, callback): + """Calls the method described by a given method descriptor. + + Args: + srvc: Instance of the service for which this method is called. + method_descriptor: Descriptor that represent the method to call. + rpc_controller: RPC controller to use for this method's execution. + request: Request protocol message. + callback: A callback to invoke after the method has completed. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'CallMethod() given method descriptor for wrong service type.') + method = getattr(srvc, method_descriptor.name) + return method(rpc_controller, request, callback) + + def _GetRequestClass(self, method_descriptor): + """Returns the class of the request protocol message. + + Args: + method_descriptor: Descriptor of the method for which to return the + request protocol message class. + + Returns: + A class that represents the input protocol message of the specified + method. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'GetRequestClass() given method descriptor for wrong service type.') + return method_descriptor.input_type._concrete_class + + def _GetResponseClass(self, method_descriptor): + """Returns the class of the response protocol message. + + Args: + method_descriptor: Descriptor of the method for which to return the + response protocol message class. + + Returns: + A class that represents the output protocol message of the specified + method. + """ + if method_descriptor.containing_service != self.descriptor: + raise RuntimeError( + 'GetResponseClass() given method descriptor for wrong service type.') + return method_descriptor.output_type._concrete_class + + def _GenerateNonImplementedMethod(self, method): + """Generates and returns a method that can be set for a service methods. + + Args: + method: Descriptor of the service method for which a method is to be + generated. + + Returns: + A method that can be added to the service class. + """ + return lambda inst, rpc_controller, request, callback: ( + self._NonImplementedMethod(method.name, rpc_controller, callback)) + + def _NonImplementedMethod(self, method_name, rpc_controller, callback): + """The body of all methods in the generated service class. + + Args: + method_name: Name of the method being executed. + rpc_controller: RPC controller used to execute this method. + callback: A callback which will be invoked when the method finishes. + """ + rpc_controller.SetFailed('Method %s not implemented.' % method_name) + callback(None) + + +class _ServiceStubBuilder(object): + + """Constructs a protocol service stub class using a service descriptor. + + Given a service descriptor, this class constructs a suitable stub class. + A stub is just a type-safe wrapper around an RpcChannel which emulates a + local implementation of the service. + + One service stub builder instance constructs exactly one class. It means all + instances of that class share the same service stub builder. + """ + + def __init__(self, service_descriptor): + """Initializes an instance of the service stub class builder. + + Args: + service_descriptor: ServiceDescriptor to use when constructing the + stub class. + """ + self.descriptor = service_descriptor + + def BuildServiceStub(self, cls): + """Constructs the stub class. + + Args: + cls: The class that will be constructed. + """ + + def _ServiceStubInit(stub, rpc_channel): + stub.rpc_channel = rpc_channel + self.cls = cls + cls.__init__ = _ServiceStubInit + for method in self.descriptor.methods: + setattr(cls, method.name, self._GenerateStubMethod(method)) + + def _GenerateStubMethod(self, method): + return (lambda inst, rpc_controller, request, callback=None: + self._StubMethod(inst, method, rpc_controller, request, callback)) + + def _StubMethod(self, stub, method_descriptor, + rpc_controller, request, callback): + """The body of all service methods in the generated stub class. + + Args: + stub: Stub instance. + method_descriptor: Descriptor of the invoked method. + rpc_controller: Rpc controller to execute the method. + request: Request protocol message. + callback: A callback to execute when the method finishes. + Returns: + Response message (in case of blocking call). + """ + return stub.rpc_channel.CallMethod( + method_descriptor, rpc_controller, request, + method_descriptor.output_type._concrete_class, callback) diff --git a/google/protobuf/source_context_pb2.py b/google/protobuf/source_context_pb2.py new file mode 100644 index 0000000..788a848 --- /dev/null +++ b/google/protobuf/source_context_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/source_context.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/source_context.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$google/protobuf/source_context.proto\x12\x0fgoogle.protobuf\",\n\rSourceContext\x12\x1b\n\tfile_name\x18\x01 \x01(\tR\x08\x66ileNameB\x8a\x01\n\x13\x63om.google.protobufB\x12SourceContextProtoP\x01Z6google.golang.org/protobuf/types/known/sourcecontextpb\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.source_context_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\022SourceContextProtoP\001Z6google.golang.org/protobuf/types/known/sourcecontextpb\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _globals['_SOURCECONTEXT']._serialized_start=57 + _globals['_SOURCECONTEXT']._serialized_end=101 +# @@protoc_insertion_point(module_scope) diff --git a/google/protobuf/struct_pb2.py b/google/protobuf/struct_pb2.py new file mode 100644 index 0000000..1252c75 --- /dev/null +++ b/google/protobuf/struct_pb2.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/struct.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/struct.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cgoogle/protobuf/struct.proto\x12\x0fgoogle.protobuf\"\x98\x01\n\x06Struct\x12;\n\x06\x66ields\x18\x01 \x03(\x0b\x32#.google.protobuf.Struct.FieldsEntryR\x06\x66ields\x1aQ\n\x0b\x46ieldsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.ValueR\x05value:\x02\x38\x01\"\xb2\x02\n\x05Value\x12;\n\nnull_value\x18\x01 \x01(\x0e\x32\x1a.google.protobuf.NullValueH\x00R\tnullValue\x12#\n\x0cnumber_value\x18\x02 \x01(\x01H\x00R\x0bnumberValue\x12#\n\x0cstring_value\x18\x03 \x01(\tH\x00R\x0bstringValue\x12\x1f\n\nbool_value\x18\x04 \x01(\x08H\x00R\tboolValue\x12<\n\x0cstruct_value\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructH\x00R\x0bstructValue\x12;\n\nlist_value\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.ListValueH\x00R\tlistValueB\x06\n\x04kind\";\n\tListValue\x12.\n\x06values\x18\x01 \x03(\x0b\x32\x16.google.protobuf.ValueR\x06values*\x1b\n\tNullValue\x12\x0e\n\nNULL_VALUE\x10\x00\x42\x7f\n\x13\x63om.google.protobufB\x0bStructProtoP\x01Z/google.golang.org/protobuf/types/known/structpb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.struct_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\013StructProtoP\001Z/google.golang.org/protobuf/types/known/structpb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _globals['_STRUCT_FIELDSENTRY']._loaded_options = None + _globals['_STRUCT_FIELDSENTRY']._serialized_options = b'8\001' + _globals['_NULLVALUE']._serialized_start=574 + _globals['_NULLVALUE']._serialized_end=601 + _globals['_STRUCT']._serialized_start=50 + _globals['_STRUCT']._serialized_end=202 + _globals['_STRUCT_FIELDSENTRY']._serialized_start=121 + _globals['_STRUCT_FIELDSENTRY']._serialized_end=202 + _globals['_VALUE']._serialized_start=205 + _globals['_VALUE']._serialized_end=511 + _globals['_LISTVALUE']._serialized_start=513 + _globals['_LISTVALUE']._serialized_end=572 +# @@protoc_insertion_point(module_scope) diff --git a/google/protobuf/symbol_database.py b/google/protobuf/symbol_database.py new file mode 100644 index 0000000..1941e81 --- /dev/null +++ b/google/protobuf/symbol_database.py @@ -0,0 +1,197 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""A database of Python protocol buffer generated symbols. + +SymbolDatabase is the MessageFactory for messages generated at compile time, +and makes it easy to create new instances of a registered type, given only the +type's protocol buffer symbol name. + +Example usage:: + + db = symbol_database.SymbolDatabase() + + # Register symbols of interest, from one or multiple files. + db.RegisterFileDescriptor(my_proto_pb2.DESCRIPTOR) + db.RegisterMessage(my_proto_pb2.MyMessage) + db.RegisterEnumDescriptor(my_proto_pb2.MyEnum.DESCRIPTOR) + + # The database can be used as a MessageFactory, to generate types based on + # their name: + types = db.GetMessages(['my_proto.proto']) + my_message_instance = types['MyMessage']() + + # The database's underlying descriptor pool can be queried, so it's not + # necessary to know a type's filename to be able to generate it: + filename = db.pool.FindFileContainingSymbol('MyMessage') + my_message_instance = db.GetMessages([filename])['MyMessage']() + + # This functionality is also provided directly via a convenience method: + my_message_instance = db.GetSymbol('MyMessage')() +""" + +import warnings + +from google.protobuf.internal import api_implementation +from google.protobuf import descriptor_pool +from google.protobuf import message_factory + + +class SymbolDatabase(): + """A database of Python generated symbols.""" + + # local cache of registered classes. + _classes = {} + + def __init__(self, pool=None): + """Initializes a new SymbolDatabase.""" + self.pool = pool or descriptor_pool.DescriptorPool() + + def GetPrototype(self, descriptor): + warnings.warn('SymbolDatabase.GetPrototype() is deprecated. Please ' + 'use message_factory.GetMessageClass() instead. ' + 'SymbolDatabase.GetPrototype() will be removed soon.') + return message_factory.GetMessageClass(descriptor) + + def CreatePrototype(self, descriptor): + warnings.warn('Directly call CreatePrototype() is wrong. Please use ' + 'message_factory.GetMessageClass() instead. ' + 'SymbolDatabase.CreatePrototype() will be removed soon.') + return message_factory._InternalCreateMessageClass(descriptor) + + def GetMessages(self, files): + warnings.warn('SymbolDatabase.GetMessages() is deprecated. Please use ' + 'message_factory.GetMessageClassedForFiles() instead. ' + 'SymbolDatabase.GetMessages() will be removed soon.') + return message_factory.GetMessageClassedForFiles(files, self.pool) + + def RegisterMessage(self, message): + """Registers the given message type in the local database. + + Calls to GetSymbol() and GetMessages() will return messages registered here. + + Args: + message: A :class:`google.protobuf.message.Message` subclass (or + instance); its descriptor will be registered. + + Returns: + The provided message. + """ + + desc = message.DESCRIPTOR + self._classes[desc] = message + self.RegisterMessageDescriptor(desc) + return message + + def RegisterMessageDescriptor(self, message_descriptor): + """Registers the given message descriptor in the local database. + + Args: + message_descriptor (Descriptor): the message descriptor to add. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddDescriptor(message_descriptor) + + def RegisterEnumDescriptor(self, enum_descriptor): + """Registers the given enum descriptor in the local database. + + Args: + enum_descriptor (EnumDescriptor): The enum descriptor to register. + + Returns: + EnumDescriptor: The provided descriptor. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddEnumDescriptor(enum_descriptor) + return enum_descriptor + + def RegisterServiceDescriptor(self, service_descriptor): + """Registers the given service descriptor in the local database. + + Args: + service_descriptor (ServiceDescriptor): the service descriptor to + register. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._AddServiceDescriptor(service_descriptor) + + def RegisterFileDescriptor(self, file_descriptor): + """Registers the given file descriptor in the local database. + + Args: + file_descriptor (FileDescriptor): The file descriptor to register. + """ + if api_implementation.Type() == 'python': + # pylint: disable=protected-access + self.pool._InternalAddFileDescriptor(file_descriptor) + + def GetSymbol(self, symbol): + """Tries to find a symbol in the local database. + + Currently, this method only returns message.Message instances, however, if + may be extended in future to support other symbol types. + + Args: + symbol (str): a protocol buffer symbol. + + Returns: + A Python class corresponding to the symbol. + + Raises: + KeyError: if the symbol could not be found. + """ + + return self._classes[self.pool.FindMessageTypeByName(symbol)] + + def GetMessages(self, files): + # TODO: Fix the differences with MessageFactory. + """Gets all registered messages from a specified file. + + Only messages already created and registered will be returned; (this is the + case for imported _pb2 modules) + But unlike MessageFactory, this version also returns already defined nested + messages, but does not register any message extensions. + + Args: + files (list[str]): The file names to extract messages from. + + Returns: + A dictionary mapping proto names to the message classes. + + Raises: + KeyError: if a file could not be found. + """ + + def _GetAllMessages(desc): + """Walk a message Descriptor and recursively yields all message names.""" + yield desc + for msg_desc in desc.nested_types: + for nested_desc in _GetAllMessages(msg_desc): + yield nested_desc + + result = {} + for file_name in files: + file_desc = self.pool.FindFileByName(file_name) + for msg_desc in file_desc.message_types_by_name.values(): + for desc in _GetAllMessages(msg_desc): + try: + result[desc.full_name] = self._classes[desc] + except KeyError: + # This descriptor has no registered class, skip it. + pass + return result + + +_DEFAULT = SymbolDatabase(pool=descriptor_pool.Default()) + + +def Default(): + """Returns the default SymbolDatabase.""" + return _DEFAULT diff --git a/google/protobuf/testdata/__init__.py b/google/protobuf/testdata/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/google/protobuf/text_encoding.py b/google/protobuf/text_encoding.py new file mode 100644 index 0000000..03c27dc --- /dev/null +++ b/google/protobuf/text_encoding.py @@ -0,0 +1,106 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Encoding related utilities.""" +import re + +def _AsciiIsPrint(i): + return i >= 32 and i < 127 + +def _MakeStrEscapes(): + ret = {} + for i in range(0, 128): + if not _AsciiIsPrint(i): + ret[i] = r'\%03o' % i + ret[ord('\t')] = r'\t' # optional escape + ret[ord('\n')] = r'\n' # optional escape + ret[ord('\r')] = r'\r' # optional escape + ret[ord('"')] = r'\"' # necessary escape + ret[ord('\'')] = r"\'" # optional escape + ret[ord('\\')] = r'\\' # necessary escape + return ret + +# Maps int -> char, performing string escapes. +_str_escapes = _MakeStrEscapes() + +# Maps int -> char, performing byte escaping and string escapes +_byte_escapes = {i: chr(i) for i in range(0, 256)} +_byte_escapes.update(_str_escapes) +_byte_escapes.update({i: r'\%03o' % i for i in range(128, 256)}) + + +def _DecodeUtf8EscapeErrors(text_bytes): + ret = '' + while text_bytes: + try: + ret += text_bytes.decode('utf-8').translate(_str_escapes) + text_bytes = '' + except UnicodeDecodeError as e: + ret += text_bytes[:e.start].decode('utf-8').translate(_str_escapes) + ret += _byte_escapes[text_bytes[e.start]] + text_bytes = text_bytes[e.start+1:] + return ret + + +def CEscape(text, as_utf8) -> str: + """Escape a bytes string for use in an text protocol buffer. + + Args: + text: A byte string to be escaped. + as_utf8: Specifies if result may contain non-ASCII characters. + In Python 3 this allows unescaped non-ASCII Unicode characters. + In Python 2 the return value will be valid UTF-8 rather than only ASCII. + Returns: + Escaped string (str). + """ + # Python's text.encode() 'string_escape' or 'unicode_escape' codecs do not + # satisfy our needs; they encodes unprintable characters using two-digit hex + # escapes whereas our C++ unescaping function allows hex escapes to be any + # length. So, "\0011".encode('string_escape') ends up being "\\x011", which + # will be decoded in C++ as a single-character string with char code 0x11. + text_is_unicode = isinstance(text, str) + if as_utf8: + if text_is_unicode: + return text.translate(_str_escapes) + else: + return _DecodeUtf8EscapeErrors(text) + else: + if text_is_unicode: + text = text.encode('utf-8') + return ''.join([_byte_escapes[c] for c in text]) + + +_CUNESCAPE_HEX = re.compile(r'(\\+)x([0-9a-fA-F])(?![0-9a-fA-F])') + + +def CUnescape(text: str) -> bytes: + """Unescape a text string with C-style escape sequences to UTF-8 bytes. + + Args: + text: The data to parse in a str. + Returns: + A byte string. + """ + + def ReplaceHex(m): + # Only replace the match if the number of leading back slashes is odd. i.e. + # the slash itself is not escaped. + if len(m.group(1)) & 1: + return m.group(1) + 'x0' + m.group(2) + return m.group(0) + + # This is required because the 'string_escape' encoding doesn't + # allow single-digit hex escapes (like '\xf'). + result = _CUNESCAPE_HEX.sub(ReplaceHex, text) + + # Replaces Unicode escape sequences with their character equivalents. + result = result.encode('raw_unicode_escape').decode('raw_unicode_escape') + # Encode Unicode characters as UTF-8, then decode to Latin-1 escaping + # unprintable characters. + result = result.encode('utf-8').decode('unicode_escape') + # Convert Latin-1 text back to a byte string (latin-1 codec also works here). + return result.encode('latin-1') diff --git a/google/protobuf/text_format.py b/google/protobuf/text_format.py new file mode 100644 index 0000000..bc61109 --- /dev/null +++ b/google/protobuf/text_format.py @@ -0,0 +1,1864 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Contains routines for printing protocol messages in text format. + +Simple usage example:: + + # Create a proto object and serialize it to a text proto string. + message = my_proto_pb2.MyMessage(foo='bar') + text_proto = text_format.MessageToString(message) + + # Parse a text proto string. + message = text_format.Parse(text_proto, my_proto_pb2.MyMessage()) +""" + +__author__ = 'kenton@google.com (Kenton Varda)' + +# TODO Import thread contention leads to test failures. +import encodings.raw_unicode_escape # pylint: disable=unused-import +import encodings.unicode_escape # pylint: disable=unused-import +import io +import math +import re + +from google.protobuf.internal import decoder +from google.protobuf.internal import type_checkers +from google.protobuf import descriptor +from google.protobuf import text_encoding +from google.protobuf import unknown_fields + +# pylint: disable=g-import-not-at-top +__all__ = ['MessageToString', 'Parse', 'PrintMessage', 'PrintField', + 'PrintFieldValue', 'Merge', 'MessageToBytes'] + +_INTEGER_CHECKERS = (type_checkers.Uint32ValueChecker(), + type_checkers.Int32ValueChecker(), + type_checkers.Uint64ValueChecker(), + type_checkers.Int64ValueChecker()) +_FLOAT_INFINITY = re.compile('-?inf(?:inity)?f?$', re.IGNORECASE) +_FLOAT_NAN = re.compile('nanf?$', re.IGNORECASE) +_QUOTES = frozenset(("'", '"')) +_ANY_FULL_TYPE_NAME = 'google.protobuf.Any' +_DEBUG_STRING_SILENT_MARKER = '\t ' + +_as_utf8_default = True + + +class Error(Exception): + """Top-level module error for text_format.""" + + +class ParseError(Error): + """Thrown in case of text parsing or tokenizing error.""" + + def __init__(self, message=None, line=None, column=None): + if message is not None and line is not None: + loc = str(line) + if column is not None: + loc += ':{0}'.format(column) + message = '{0} : {1}'.format(loc, message) + if message is not None: + super(ParseError, self).__init__(message) + else: + super(ParseError, self).__init__() + self._line = line + self._column = column + + def GetLine(self): + return self._line + + def GetColumn(self): + return self._column + + +class TextWriter(object): + + def __init__(self, as_utf8): + self._writer = io.StringIO() + + def write(self, val): + return self._writer.write(val) + + def close(self): + return self._writer.close() + + def getvalue(self): + return self._writer.getvalue() + + +def MessageToString( + message, + as_utf8=_as_utf8_default, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + indent=0, + message_formatter=None, + print_unknown_fields=False, + force_colon=False) -> str: + """Convert protobuf message to text format. + + Double values can be formatted compactly with 15 digits of + precision (which is the most that IEEE 754 "double" can guarantee) + using double_format='.15g'. To ensure that converting to text and back to a + proto will result in an identical value, double_format='.17g' should be used. + + Args: + message: The protocol buffers message. + as_utf8: Return unescaped Unicode for non-ASCII characters. + as_one_line: Don't introduce newlines between fields. + use_short_repeated_primitives: Use short repeated format for primitives. + pointy_brackets: If True, use angle brackets instead of curly braces for + nesting. + use_index_order: If True, fields of a proto message will be printed using + the order defined in source code instead of the field number, extensions + will be printed at the end of the message and their relative order is + determined by the extension number. By default, use the field number + order. + float_format (str): If set, use this to specify float field formatting + (per the "Format Specification Mini-Language"); otherwise, shortest float + that has same value in wire will be printed. Also affect double field + if double_format is not set but float_format is set. + double_format (str): If set, use this to specify double field formatting + (per the "Format Specification Mini-Language"); if it is not set but + float_format is set, use float_format. Otherwise, use ``str()`` + use_field_number: If True, print field numbers instead of names. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + indent (int): The initial indent level, in terms of spaces, for pretty + print. + message_formatter (function(message, indent, as_one_line) -> unicode|None): + Custom formatter for selected sub-messages (usually based on message + type). Use to pretty print parts of the protobuf for easier diffing. + print_unknown_fields: If True, unknown fields will be printed. + force_colon: If set, a colon will be added after the field name even if the + field is a proto message. + + Returns: + str: A string of the text formatted protocol buffer message. + """ + out = TextWriter(as_utf8) + printer = _Printer( + out, + indent, + as_utf8, + as_one_line, + use_short_repeated_primitives, + pointy_brackets, + use_index_order, + float_format, + double_format, + use_field_number, + descriptor_pool, + message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintMessage(message) + result = out.getvalue() + out.close() + if as_one_line: + return result.rstrip() + return result + + +def MessageToBytes(message, **kwargs) -> bytes: + """Convert protobuf message to encoded text format. See MessageToString.""" + text = MessageToString(message, **kwargs) + if isinstance(text, bytes): + return text + codec = 'utf-8' if kwargs.get('as_utf8') else 'ascii' + return text.encode(codec) + + +def _IsMapEntry(field): + return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.message_type.has_options and + field.message_type.GetOptions().map_entry) + + +def _IsGroupLike(field): + """Determines if a field is consistent with a proto2 group. + + Args: + field: The field descriptor. + + Returns: + True if this field is group-like, false otherwise. + """ + # Groups are always tag-delimited. + if field.type != descriptor.FieldDescriptor.TYPE_GROUP: + return False + + # Group fields always are always the lowercase type name. + if field.name != field.message_type.name.lower(): + return False + + if field.message_type.file != field.file: + return False + + # Group messages are always defined in the same scope as the field. File + # level extensions will compare NULL == NULL here, which is why the file + # comparison above is necessary to ensure both come from the same file. + return ( + field.message_type.containing_type == field.extension_scope + if field.is_extension + else field.message_type.containing_type == field.containing_type + ) + + +def PrintMessage(message, + out, + indent=0, + as_utf8=_as_utf8_default, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Convert the message to text format and write it to the out stream. + + Args: + message: The Message object to convert to text format. + out: A file handle to write the message to. + indent: The initial indent level for pretty print. + as_utf8: Return unescaped Unicode for non-ASCII characters. + as_one_line: Don't introduce newlines between fields. + use_short_repeated_primitives: Use short repeated format for primitives. + pointy_brackets: If True, use angle brackets instead of curly braces for + nesting. + use_index_order: If True, print fields of a proto message using the order + defined in source code instead of the field number. By default, use the + field number order. + float_format: If set, use this to specify float field formatting + (per the "Format Specification Mini-Language"); otherwise, shortest + float that has same value in wire will be printed. Also affect double + field if double_format is not set but float_format is set. + double_format: If set, use this to specify double field formatting + (per the "Format Specification Mini-Language"); if it is not set but + float_format is set, use float_format. Otherwise, str() is used. + use_field_number: If True, print field numbers instead of names. + descriptor_pool: A DescriptorPool used to resolve Any types. + message_formatter: A function(message, indent, as_one_line): unicode|None + to custom format selected sub-messages (usually based on message type). + Use to pretty print parts of the protobuf for easier diffing. + print_unknown_fields: If True, unknown fields will be printed. + force_colon: If set, a colon will be added after the field name even if + the field is a proto message. + """ + printer = _Printer( + out=out, indent=indent, as_utf8=as_utf8, + as_one_line=as_one_line, + use_short_repeated_primitives=use_short_repeated_primitives, + pointy_brackets=pointy_brackets, + use_index_order=use_index_order, + float_format=float_format, + double_format=double_format, + use_field_number=use_field_number, + descriptor_pool=descriptor_pool, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintMessage(message) + + +def PrintField(field, + value, + out, + indent=0, + as_utf8=_as_utf8_default, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Print a single field name/value pair.""" + printer = _Printer(out, indent, as_utf8, as_one_line, + use_short_repeated_primitives, pointy_brackets, + use_index_order, float_format, double_format, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintField(field, value) + + +def PrintFieldValue(field, + value, + out, + indent=0, + as_utf8=_as_utf8_default, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Print a single field value (not including name).""" + printer = _Printer(out, indent, as_utf8, as_one_line, + use_short_repeated_primitives, pointy_brackets, + use_index_order, float_format, double_format, + message_formatter=message_formatter, + print_unknown_fields=print_unknown_fields, + force_colon=force_colon) + printer.PrintFieldValue(field, value) + + +def _BuildMessageFromTypeName(type_name, descriptor_pool): + """Returns a protobuf message instance. + + Args: + type_name: Fully-qualified protobuf message type name string. + descriptor_pool: DescriptorPool instance. + + Returns: + A Message instance of type matching type_name, or None if the a Descriptor + wasn't found matching type_name. + """ + # pylint: disable=g-import-not-at-top + if descriptor_pool is None: + from google.protobuf import descriptor_pool as pool_mod + descriptor_pool = pool_mod.Default() + from google.protobuf import message_factory + try: + message_descriptor = descriptor_pool.FindMessageTypeByName(type_name) + except KeyError: + return None + message_type = message_factory.GetMessageClass(message_descriptor) + return message_type() + + +# These values must match WireType enum in //google/protobuf/wire_format.h. +WIRETYPE_LENGTH_DELIMITED = 2 +WIRETYPE_START_GROUP = 3 + + +class _Printer(object): + """Text format printer for protocol message.""" + + def __init__( + self, + out, + indent=0, + as_utf8=_as_utf8_default, + as_one_line=False, + use_short_repeated_primitives=False, + pointy_brackets=False, + use_index_order=False, + float_format=None, + double_format=None, + use_field_number=False, + descriptor_pool=None, + message_formatter=None, + print_unknown_fields=False, + force_colon=False): + """Initialize the Printer. + + Double values can be formatted compactly with 15 digits of precision + (which is the most that IEEE 754 "double" can guarantee) using + double_format='.15g'. To ensure that converting to text and back to a proto + will result in an identical value, double_format='.17g' should be used. + + Args: + out: To record the text format result. + indent: The initial indent level for pretty print. + as_utf8: Return unescaped Unicode for non-ASCII characters. + as_one_line: Don't introduce newlines between fields. + use_short_repeated_primitives: Use short repeated format for primitives. + pointy_brackets: If True, use angle brackets instead of curly braces for + nesting. + use_index_order: If True, print fields of a proto message using the order + defined in source code instead of the field number. By default, use the + field number order. + float_format: If set, use this to specify float field formatting + (per the "Format Specification Mini-Language"); otherwise, shortest + float that has same value in wire will be printed. Also affect double + field if double_format is not set but float_format is set. + double_format: If set, use this to specify double field formatting + (per the "Format Specification Mini-Language"); if it is not set but + float_format is set, use float_format. Otherwise, str() is used. + use_field_number: If True, print field numbers instead of names. + descriptor_pool: A DescriptorPool used to resolve Any types. + message_formatter: A function(message, indent, as_one_line): unicode|None + to custom format selected sub-messages (usually based on message type). + Use to pretty print parts of the protobuf for easier diffing. + print_unknown_fields: If True, unknown fields will be printed. + force_colon: If set, a colon will be added after the field name even if + the field is a proto message. + """ + self.out = out + self.indent = indent + self.as_utf8 = as_utf8 + self.as_one_line = as_one_line + self.use_short_repeated_primitives = use_short_repeated_primitives + self.pointy_brackets = pointy_brackets + self.use_index_order = use_index_order + self.float_format = float_format + if double_format is not None: + self.double_format = double_format + else: + self.double_format = float_format + self.use_field_number = use_field_number + self.descriptor_pool = descriptor_pool + self.message_formatter = message_formatter + self.print_unknown_fields = print_unknown_fields + self.force_colon = force_colon + + def _TryPrintAsAnyMessage(self, message): + """Serializes if message is a google.protobuf.Any field.""" + if '/' not in message.type_url: + return False + packed_message = _BuildMessageFromTypeName(message.TypeName(), + self.descriptor_pool) + if packed_message: + packed_message.MergeFromString(message.value) + colon = ':' if self.force_colon else '' + self.out.write('%s[%s]%s ' % (self.indent * ' ', message.type_url, colon)) + self._PrintMessageFieldValue(packed_message) + self.out.write(' ' if self.as_one_line else '\n') + return True + else: + return False + + def _TryCustomFormatMessage(self, message): + formatted = self.message_formatter(message, self.indent, self.as_one_line) + if formatted is None: + return False + + out = self.out + out.write(' ' * self.indent) + out.write(formatted) + out.write(' ' if self.as_one_line else '\n') + return True + + def PrintMessage(self, message): + """Convert protobuf message to text format. + + Args: + message: The protocol buffers message. + """ + if self.message_formatter and self._TryCustomFormatMessage(message): + return + if (message.DESCRIPTOR.full_name == _ANY_FULL_TYPE_NAME and + self._TryPrintAsAnyMessage(message)): + return + fields = message.ListFields() + if self.use_index_order: + fields.sort( + key=lambda x: x[0].number if x[0].is_extension else x[0].index) + for field, value in fields: + if _IsMapEntry(field): + for key in sorted(value): + # This is slow for maps with submessage entries because it copies the + # entire tree. Unfortunately this would take significant refactoring + # of this file to work around. + # + # TODO: refactor and optimize if this becomes an issue. + entry_submsg = value.GetEntryClass()(key=key, value=value[key]) + self.PrintField(field, entry_submsg) + elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if (self.use_short_repeated_primitives + and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE + and field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_STRING): + self._PrintShortRepeatedPrimitivesValue(field, value) + else: + for element in value: + self.PrintField(field, element) + else: + self.PrintField(field, value) + + if self.print_unknown_fields: + self._PrintUnknownFields(unknown_fields.UnknownFieldSet(message)) + + def _PrintUnknownFields(self, unknown_field_set): + """Print unknown fields.""" + out = self.out + for field in unknown_field_set: + out.write(' ' * self.indent) + out.write(str(field.field_number)) + if field.wire_type == WIRETYPE_START_GROUP: + if self.as_one_line: + out.write(' { ') + else: + out.write(' {\n') + self.indent += 2 + + self._PrintUnknownFields(field.data) + + if self.as_one_line: + out.write('} ') + else: + self.indent -= 2 + out.write(' ' * self.indent + '}\n') + elif field.wire_type == WIRETYPE_LENGTH_DELIMITED: + try: + # If this field is parseable as a Message, it is probably + # an embedded message. + # pylint: disable=protected-access + (embedded_unknown_message, pos) = decoder._DecodeUnknownFieldSet( + memoryview(field.data), 0, len(field.data)) + except Exception: # pylint: disable=broad-except + pos = 0 + + if pos == len(field.data): + if self.as_one_line: + out.write(' { ') + else: + out.write(' {\n') + self.indent += 2 + + self._PrintUnknownFields(embedded_unknown_message) + + if self.as_one_line: + out.write('} ') + else: + self.indent -= 2 + out.write(' ' * self.indent + '}\n') + else: + # A string or bytes field. self.as_utf8 may not work. + out.write(': \"') + out.write(text_encoding.CEscape(field.data, False)) + out.write('\" ' if self.as_one_line else '\"\n') + else: + # varint, fixed32, fixed64 + out.write(': ') + out.write(str(field.data)) + out.write(' ' if self.as_one_line else '\n') + + def _PrintFieldName(self, field): + """Print field name.""" + out = self.out + out.write(' ' * self.indent) + if self.use_field_number: + out.write(str(field.number)) + else: + if field.is_extension: + out.write('[') + if (field.containing_type.GetOptions().message_set_wire_format and + field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and + field.label == descriptor.FieldDescriptor.LABEL_OPTIONAL): + out.write(field.message_type.full_name) + else: + out.write(field.full_name) + out.write(']') + elif _IsGroupLike(field): + # For groups, use the capitalized name. + out.write(field.message_type.name) + else: + out.write(field.name) + + if (self.force_colon or + field.cpp_type != descriptor.FieldDescriptor.CPPTYPE_MESSAGE): + # The colon is optional in this case, but our cross-language golden files + # don't include it. Here, the colon is only included if force_colon is + # set to True + out.write(':') + + def PrintField(self, field, value): + """Print a single field name/value pair.""" + self._PrintFieldName(field) + self.out.write(' ') + self.PrintFieldValue(field, value) + self.out.write(' ' if self.as_one_line else '\n') + + def _PrintShortRepeatedPrimitivesValue(self, field, value): + """"Prints short repeated primitives value.""" + # Note: this is called only when value has at least one element. + self._PrintFieldName(field) + self.out.write(' [') + for i in range(len(value) - 1): + self.PrintFieldValue(field, value[i]) + self.out.write(', ') + self.PrintFieldValue(field, value[-1]) + self.out.write(']') + self.out.write(' ' if self.as_one_line else '\n') + + def _PrintMessageFieldValue(self, value): + if self.pointy_brackets: + openb = '<' + closeb = '>' + else: + openb = '{' + closeb = '}' + + if self.as_one_line: + self.out.write('%s ' % openb) + self.PrintMessage(value) + self.out.write(closeb) + else: + self.out.write('%s\n' % openb) + self.indent += 2 + self.PrintMessage(value) + self.indent -= 2 + self.out.write(' ' * self.indent + closeb) + + def PrintFieldValue(self, field, value): + """Print a single field value (not including name). + + For repeated fields, the value should be a single element. + + Args: + field: The descriptor of the field to be printed. + value: The value of the field. + """ + out = self.out + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + self._PrintMessageFieldValue(value) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM: + enum_value = field.enum_type.values_by_number.get(value, None) + if enum_value is not None: + out.write(enum_value.name) + else: + out.write(str(value)) + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING: + out.write('\"') + if isinstance(value, str) and not self.as_utf8: + out_value = value.encode('utf-8') + else: + out_value = value + if field.type == descriptor.FieldDescriptor.TYPE_BYTES: + # We always need to escape all binary data in TYPE_BYTES fields. + out_as_utf8 = False + else: + out_as_utf8 = self.as_utf8 + out.write(text_encoding.CEscape(out_value, out_as_utf8)) + out.write('\"') + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL: + if value: + out.write('true') + else: + out.write('false') + elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_FLOAT: + if self.float_format is not None: + out.write('{1:{0}}'.format(self.float_format, value)) + else: + if math.isnan(value): + out.write(str(value)) + else: + out.write(str(type_checkers.ToShortestFloat(value))) + elif (field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_DOUBLE and + self.double_format is not None): + out.write('{1:{0}}'.format(self.double_format, value)) + else: + out.write(str(value)) + + +def Parse(text, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + NOTE: for historical reasons this function does not clear the input + message. This is different from what the binary msg.ParseFrom(...) does. + If text contains a field already set in message, the value is appended if the + field is repeated. Otherwise, an error is raised. + + Example:: + + a = MyProto() + a.repeated_field.append('test') + b = MyProto() + + # Repeated fields are combined + text_format.Parse(repr(a), b) + text_format.Parse(repr(a), b) # repeated_field contains ["test", "test"] + + # Non-repeated fields cannot be overwritten + a.singular_field = 1 + b.singular_field = 2 + text_format.Parse(repr(a), b) # ParseError + + # Binary version: + b.ParseFromString(a.SerializeToString()) # repeated_field is now "test" + + Caller is responsible for clearing the message as needed. + + Args: + text (str): Message text representation. + message (Message): A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + Message: The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + return ParseLines(text.split(b'\n' if isinstance(text, bytes) else u'\n'), + message, + allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + + +def Merge(text, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + Like Parse(), but allows repeated values for a non-repeated field, and uses + the last one. This means any non-repeated, top-level fields specified in text + replace those in the message. + + Args: + text (str): Message text representation. + message (Message): A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool (DescriptorPool): Descriptor pool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + Message: The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + return MergeLines( + text.split(b'\n' if isinstance(text, bytes) else u'\n'), + message, + allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + + +def ParseLines(lines, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + See Parse() for caveats. + + Args: + lines: An iterable of lines of a message's text representation. + message: A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool: A DescriptorPool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + parser = _Parser(allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + return parser.ParseLines(lines, message) + + +def MergeLines(lines, + message, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + """Parses a text representation of a protocol message into a message. + + See Merge() for more details. + + Args: + lines: An iterable of lines of a message's text representation. + message: A protocol buffer message to merge into. + allow_unknown_extension: if True, skip over missing extensions and keep + parsing + allow_field_number: if True, both field number and field name are allowed. + descriptor_pool: A DescriptorPool used to resolve Any types. + allow_unknown_field: if True, skip over unknown field and keep + parsing. Avoid to use this option if possible. It may hide some + errors (e.g. spelling error on field name) + + Returns: + The same message passed as argument. + + Raises: + ParseError: On text parsing problems. + """ + parser = _Parser(allow_unknown_extension, + allow_field_number, + descriptor_pool=descriptor_pool, + allow_unknown_field=allow_unknown_field) + return parser.MergeLines(lines, message) + + +class _Parser(object): + """Text format parser for protocol message.""" + + def __init__(self, + allow_unknown_extension=False, + allow_field_number=False, + descriptor_pool=None, + allow_unknown_field=False): + self.allow_unknown_extension = allow_unknown_extension + self.allow_field_number = allow_field_number + self.descriptor_pool = descriptor_pool + self.allow_unknown_field = allow_unknown_field + + def ParseLines(self, lines, message): + """Parses a text representation of a protocol message into a message.""" + self._allow_multiple_scalars = False + self._ParseOrMerge(lines, message) + return message + + def MergeLines(self, lines, message): + """Merges a text representation of a protocol message into a message.""" + self._allow_multiple_scalars = True + self._ParseOrMerge(lines, message) + return message + + def _ParseOrMerge(self, lines, message): + """Converts a text representation of a protocol message into a message. + + Args: + lines: Lines of a message's text representation. + message: A protocol buffer message to merge into. + + Raises: + ParseError: On text parsing problems. + """ + # Tokenize expects native str lines. + try: + str_lines = ( + line if isinstance(line, str) else line.decode('utf-8') + for line in lines) + tokenizer = Tokenizer(str_lines) + except UnicodeDecodeError as e: + raise ParseError from e + if message: + self.root_type = message.DESCRIPTOR.full_name + while not tokenizer.AtEnd(): + self._MergeField(tokenizer, message) + + def _MergeField(self, tokenizer, message): + """Merges a single protocol message field into a message. + + Args: + tokenizer: A tokenizer to parse the field name and values. + message: A protocol message to record the data. + + Raises: + ParseError: In case of text parsing problems. + """ + message_descriptor = message.DESCRIPTOR + if (message_descriptor.full_name == _ANY_FULL_TYPE_NAME and + tokenizer.TryConsume('[')): + type_url_prefix, packed_type_name = self._ConsumeAnyTypeUrl(tokenizer) + tokenizer.Consume(']') + tokenizer.TryConsume(':') + self._DetectSilentMarker(tokenizer, message_descriptor.full_name, + type_url_prefix + '/' + packed_type_name) + if tokenizer.TryConsume('<'): + expanded_any_end_token = '>' + else: + tokenizer.Consume('{') + expanded_any_end_token = '}' + expanded_any_sub_message = _BuildMessageFromTypeName(packed_type_name, + self.descriptor_pool) + # Direct comparison with None is used instead of implicit bool conversion + # to avoid false positives with falsy initial values, e.g. for + # google.protobuf.ListValue. + if expanded_any_sub_message is None: + raise ParseError('Type %s not found in descriptor pool' % + packed_type_name) + while not tokenizer.TryConsume(expanded_any_end_token): + if tokenizer.AtEnd(): + raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % + (expanded_any_end_token,)) + self._MergeField(tokenizer, expanded_any_sub_message) + deterministic = False + + message.Pack(expanded_any_sub_message, + type_url_prefix=type_url_prefix, + deterministic=deterministic) + return + + if tokenizer.TryConsume('['): + name = [tokenizer.ConsumeIdentifier()] + while tokenizer.TryConsume('.'): + name.append(tokenizer.ConsumeIdentifier()) + name = '.'.join(name) + + if not message_descriptor.is_extendable: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" does not have extensions.' % + message_descriptor.full_name) + # pylint: disable=protected-access + field = message.Extensions._FindExtensionByName(name) + # pylint: enable=protected-access + if not field: + if self.allow_unknown_extension: + field = None + else: + raise tokenizer.ParseErrorPreviousToken( + 'Extension "%s" not registered. ' + 'Did you import the _pb2 module which defines it? ' + 'If you are trying to place the extension in the MessageSet ' + 'field of another message that is in an Any or MessageSet field, ' + 'that message\'s _pb2 module must be imported as well' % name) + elif message_descriptor != field.containing_type: + raise tokenizer.ParseErrorPreviousToken( + 'Extension "%s" does not extend message type "%s".' % + (name, message_descriptor.full_name)) + + tokenizer.Consume(']') + + else: + name = tokenizer.ConsumeIdentifierOrNumber() + if self.allow_field_number and name.isdigit(): + number = ParseInteger(name, True, True) + field = message_descriptor.fields_by_number.get(number, None) + if not field and message_descriptor.is_extendable: + field = message.Extensions._FindExtensionByNumber(number) + else: + field = message_descriptor.fields_by_name.get(name, None) + + # Group names are expected to be capitalized as they appear in the + # .proto file, which actually matches their type names, not their field + # names. + if not field: + field = message_descriptor.fields_by_name.get(name.lower(), None) + if field and not _IsGroupLike(field): + field = None + if field and field.message_type.name != name: + field = None + + if not field and not self.allow_unknown_field: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" has no field named "%s".' % + (message_descriptor.full_name, name)) + + if field: + if not self._allow_multiple_scalars and field.containing_oneof: + # Check if there's a different field set in this oneof. + # Note that we ignore the case if the same field was set before, and we + # apply _allow_multiple_scalars to non-scalar fields as well. + which_oneof = message.WhichOneof(field.containing_oneof.name) + if which_oneof is not None and which_oneof != field.name: + raise tokenizer.ParseErrorPreviousToken( + 'Field "%s" is specified along with field "%s", another member ' + 'of oneof "%s" for message type "%s".' % + (field.name, which_oneof, field.containing_oneof.name, + message_descriptor.full_name)) + + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + tokenizer.TryConsume(':') + self._DetectSilentMarker(tokenizer, message_descriptor.full_name, + field.full_name) + merger = self._MergeMessageField + else: + tokenizer.Consume(':') + self._DetectSilentMarker(tokenizer, message_descriptor.full_name, + field.full_name) + merger = self._MergeScalarField + + if (field.label == descriptor.FieldDescriptor.LABEL_REPEATED and + tokenizer.TryConsume('[')): + # Short repeated format, e.g. "foo: [1, 2, 3]" + if not tokenizer.TryConsume(']'): + while True: + merger(tokenizer, message, field) + if tokenizer.TryConsume(']'): + break + tokenizer.Consume(',') + + else: + merger(tokenizer, message, field) + + else: # Proto field is unknown. + assert (self.allow_unknown_extension or self.allow_unknown_field) + self._SkipFieldContents(tokenizer, name, message_descriptor.full_name) + + # For historical reasons, fields may optionally be separated by commas or + # semicolons. + if not tokenizer.TryConsume(','): + tokenizer.TryConsume(';') + + def _LogSilentMarker(self, immediate_message_type, field_name): + pass + + def _DetectSilentMarker(self, tokenizer, immediate_message_type, field_name): + if tokenizer.contains_silent_marker_before_current_token: + self._LogSilentMarker(immediate_message_type, field_name) + + def _ConsumeAnyTypeUrl(self, tokenizer): + """Consumes a google.protobuf.Any type URL and returns the type name.""" + # Consume "type.googleapis.com/". + prefix = [tokenizer.ConsumeIdentifier()] + tokenizer.Consume('.') + prefix.append(tokenizer.ConsumeIdentifier()) + tokenizer.Consume('.') + prefix.append(tokenizer.ConsumeIdentifier()) + tokenizer.Consume('/') + # Consume the fully-qualified type name. + name = [tokenizer.ConsumeIdentifier()] + while tokenizer.TryConsume('.'): + name.append(tokenizer.ConsumeIdentifier()) + return '.'.join(prefix), '.'.join(name) + + def _MergeMessageField(self, tokenizer, message, field): + """Merges a single scalar field into a message. + + Args: + tokenizer: A tokenizer to parse the field value. + message: The message of which field is a member. + field: The descriptor of the field to be merged. + + Raises: + ParseError: In case of text parsing problems. + """ + is_map_entry = _IsMapEntry(field) + + if tokenizer.TryConsume('<'): + end_token = '>' + else: + tokenizer.Consume('{') + end_token = '}' + + if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if field.is_extension: + sub_message = message.Extensions[field].add() + elif is_map_entry: + sub_message = getattr(message, field.name).GetEntryClass()() + else: + sub_message = getattr(message, field.name).add() + else: + if field.is_extension: + if (not self._allow_multiple_scalars and + message.HasExtension(field)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" extensions.' % + (message.DESCRIPTOR.full_name, field.full_name)) + sub_message = message.Extensions[field] + else: + # Also apply _allow_multiple_scalars to message field. + # TODO: Change to _allow_singular_overwrites. + if (not self._allow_multiple_scalars and + message.HasField(field.name)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" fields.' % + (message.DESCRIPTOR.full_name, field.name)) + sub_message = getattr(message, field.name) + sub_message.SetInParent() + + while not tokenizer.TryConsume(end_token): + if tokenizer.AtEnd(): + raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % (end_token,)) + self._MergeField(tokenizer, sub_message) + + if is_map_entry: + value_cpptype = field.message_type.fields_by_name['value'].cpp_type + if value_cpptype == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + value = getattr(message, field.name)[sub_message.key] + value.CopyFrom(sub_message.value) + else: + getattr(message, field.name)[sub_message.key] = sub_message.value + + def _MergeScalarField(self, tokenizer, message, field): + """Merges a single scalar field into a message. + + Args: + tokenizer: A tokenizer to parse the field value. + message: A protocol message to record the data. + field: The descriptor of the field to be merged. + + Raises: + ParseError: In case of text parsing problems. + RuntimeError: On runtime errors. + """ + _ = self.allow_unknown_extension + value = None + + if field.type in (descriptor.FieldDescriptor.TYPE_INT32, + descriptor.FieldDescriptor.TYPE_SINT32, + descriptor.FieldDescriptor.TYPE_SFIXED32): + value = _ConsumeInt32(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_INT64, + descriptor.FieldDescriptor.TYPE_SINT64, + descriptor.FieldDescriptor.TYPE_SFIXED64): + value = _ConsumeInt64(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_UINT32, + descriptor.FieldDescriptor.TYPE_FIXED32): + value = _ConsumeUint32(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_UINT64, + descriptor.FieldDescriptor.TYPE_FIXED64): + value = _ConsumeUint64(tokenizer) + elif field.type in (descriptor.FieldDescriptor.TYPE_FLOAT, + descriptor.FieldDescriptor.TYPE_DOUBLE): + value = tokenizer.ConsumeFloat() + elif field.type == descriptor.FieldDescriptor.TYPE_BOOL: + value = tokenizer.ConsumeBool() + elif field.type == descriptor.FieldDescriptor.TYPE_STRING: + value = tokenizer.ConsumeString() + elif field.type == descriptor.FieldDescriptor.TYPE_BYTES: + value = tokenizer.ConsumeByteString() + elif field.type == descriptor.FieldDescriptor.TYPE_ENUM: + value = tokenizer.ConsumeEnum(field) + else: + raise RuntimeError('Unknown field type %d' % field.type) + + if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: + if field.is_extension: + message.Extensions[field].append(value) + else: + getattr(message, field.name).append(value) + else: + if field.is_extension: + if (not self._allow_multiple_scalars and + field.has_presence and + message.HasExtension(field)): + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" extensions.' % + (message.DESCRIPTOR.full_name, field.full_name)) + else: + message.Extensions[field] = value + else: + duplicate_error = False + if not self._allow_multiple_scalars: + if field.has_presence: + duplicate_error = message.HasField(field.name) + else: + # For field that doesn't represent presence, try best effort to + # check multiple scalars by compare to default values. + duplicate_error = bool(getattr(message, field.name)) + + if duplicate_error: + raise tokenizer.ParseErrorPreviousToken( + 'Message type "%s" should not have multiple "%s" fields.' % + (message.DESCRIPTOR.full_name, field.name)) + else: + setattr(message, field.name, value) + + def _SkipFieldContents(self, tokenizer, field_name, immediate_message_type): + """Skips over contents (value or message) of a field. + + Args: + tokenizer: A tokenizer to parse the field name and values. + field_name: The field name currently being parsed. + immediate_message_type: The type of the message immediately containing + the silent marker. + """ + # Try to guess the type of this field. + # If this field is not a message, there should be a ":" between the + # field name and the field value and also the field value should not + # start with "{" or "<" which indicates the beginning of a message body. + # If there is no ":" or there is a "{" or "<" after ":", this field has + # to be a message or the input is ill-formed. + if tokenizer.TryConsume( + ':') and not tokenizer.LookingAt('{') and not tokenizer.LookingAt('<'): + self._DetectSilentMarker(tokenizer, immediate_message_type, field_name) + if tokenizer.LookingAt('['): + self._SkipRepeatedFieldValue(tokenizer) + else: + self._SkipFieldValue(tokenizer) + else: + self._DetectSilentMarker(tokenizer, immediate_message_type, field_name) + self._SkipFieldMessage(tokenizer, immediate_message_type) + + def _SkipField(self, tokenizer, immediate_message_type): + """Skips over a complete field (name and value/message). + + Args: + tokenizer: A tokenizer to parse the field name and values. + immediate_message_type: The type of the message immediately containing + the silent marker. + """ + field_name = '' + if tokenizer.TryConsume('['): + # Consume extension or google.protobuf.Any type URL + field_name += '[' + tokenizer.ConsumeIdentifier() + num_identifiers = 1 + while tokenizer.TryConsume('.'): + field_name += '.' + tokenizer.ConsumeIdentifier() + num_identifiers += 1 + # This is possibly a type URL for an Any message. + if num_identifiers == 3 and tokenizer.TryConsume('/'): + field_name += '/' + tokenizer.ConsumeIdentifier() + while tokenizer.TryConsume('.'): + field_name += '.' + tokenizer.ConsumeIdentifier() + tokenizer.Consume(']') + field_name += ']' + else: + field_name += tokenizer.ConsumeIdentifierOrNumber() + + self._SkipFieldContents(tokenizer, field_name, immediate_message_type) + + # For historical reasons, fields may optionally be separated by commas or + # semicolons. + if not tokenizer.TryConsume(','): + tokenizer.TryConsume(';') + + def _SkipFieldMessage(self, tokenizer, immediate_message_type): + """Skips over a field message. + + Args: + tokenizer: A tokenizer to parse the field name and values. + immediate_message_type: The type of the message immediately containing + the silent marker + """ + if tokenizer.TryConsume('<'): + delimiter = '>' + else: + tokenizer.Consume('{') + delimiter = '}' + + while not tokenizer.LookingAt('>') and not tokenizer.LookingAt('}'): + self._SkipField(tokenizer, immediate_message_type) + + tokenizer.Consume(delimiter) + + def _SkipFieldValue(self, tokenizer): + """Skips over a field value. + + Args: + tokenizer: A tokenizer to parse the field name and values. + + Raises: + ParseError: In case an invalid field value is found. + """ + if (not tokenizer.TryConsumeByteString()and + not tokenizer.TryConsumeIdentifier() and + not _TryConsumeInt64(tokenizer) and + not _TryConsumeUint64(tokenizer) and + not tokenizer.TryConsumeFloat()): + raise ParseError('Invalid field value: ' + tokenizer.token) + + def _SkipRepeatedFieldValue(self, tokenizer): + """Skips over a repeated field value. + + Args: + tokenizer: A tokenizer to parse the field value. + """ + tokenizer.Consume('[') + if not tokenizer.LookingAt(']'): + self._SkipFieldValue(tokenizer) + while tokenizer.TryConsume(','): + self._SkipFieldValue(tokenizer) + tokenizer.Consume(']') + + +class Tokenizer(object): + """Protocol buffer text representation tokenizer. + + This class handles the lower level string parsing by splitting it into + meaningful tokens. + + It was directly ported from the Java protocol buffer API. + """ + + _WHITESPACE = re.compile(r'\s+') + _COMMENT = re.compile(r'(\s*#.*$)', re.MULTILINE) + _WHITESPACE_OR_COMMENT = re.compile(r'(\s|(#.*$))+', re.MULTILINE) + _TOKEN = re.compile('|'.join([ + r'[a-zA-Z_][0-9a-zA-Z_+-]*', # an identifier + r'([0-9+-]|(\.[0-9]))[0-9a-zA-Z_.+-]*', # a number + ] + [ # quoted str for each quote mark + # Avoid backtracking! https://stackoverflow.com/a/844267 + r'{qt}[^{qt}\n\\]*((\\.)+[^{qt}\n\\]*)*({qt}|\\?$)'.format(qt=mark) + for mark in _QUOTES + ])) + + _IDENTIFIER = re.compile(r'[^\d\W]\w*') + _IDENTIFIER_OR_NUMBER = re.compile(r'\w+') + + def __init__(self, lines, skip_comments=True): + self._position = 0 + self._line = -1 + self._column = 0 + self._token_start = None + self.token = '' + self._lines = iter(lines) + self._current_line = '' + self._previous_line = 0 + self._previous_column = 0 + self._more_lines = True + self._skip_comments = skip_comments + self._whitespace_pattern = (skip_comments and self._WHITESPACE_OR_COMMENT + or self._WHITESPACE) + self.contains_silent_marker_before_current_token = False + + self._SkipWhitespace() + self.NextToken() + + def LookingAt(self, token): + return self.token == token + + def AtEnd(self): + """Checks the end of the text was reached. + + Returns: + True iff the end was reached. + """ + return not self.token + + def _PopLine(self): + while len(self._current_line) <= self._column: + try: + self._current_line = next(self._lines) + except StopIteration: + self._current_line = '' + self._more_lines = False + return + else: + self._line += 1 + self._column = 0 + + def _SkipWhitespace(self): + while True: + self._PopLine() + match = self._whitespace_pattern.match(self._current_line, self._column) + if not match: + break + self.contains_silent_marker_before_current_token = match.group(0) == ( + ' ' + _DEBUG_STRING_SILENT_MARKER) + length = len(match.group(0)) + self._column += length + + def TryConsume(self, token): + """Tries to consume a given piece of text. + + Args: + token: Text to consume. + + Returns: + True iff the text was consumed. + """ + if self.token == token: + self.NextToken() + return True + return False + + def Consume(self, token): + """Consumes a piece of text. + + Args: + token: Text to consume. + + Raises: + ParseError: If the text couldn't be consumed. + """ + if not self.TryConsume(token): + raise self.ParseError('Expected "%s".' % token) + + def ConsumeComment(self): + result = self.token + if not self._COMMENT.match(result): + raise self.ParseError('Expected comment.') + self.NextToken() + return result + + def ConsumeCommentOrTrailingComment(self): + """Consumes a comment, returns a 2-tuple (trailing bool, comment str).""" + + # Tokenizer initializes _previous_line and _previous_column to 0. As the + # tokenizer starts, it looks like there is a previous token on the line. + just_started = self._line == 0 and self._column == 0 + + before_parsing = self._previous_line + comment = self.ConsumeComment() + + # A trailing comment is a comment on the same line than the previous token. + trailing = (self._previous_line == before_parsing + and not just_started) + + return trailing, comment + + def TryConsumeIdentifier(self): + try: + self.ConsumeIdentifier() + return True + except ParseError: + return False + + def ConsumeIdentifier(self): + """Consumes protocol message field identifier. + + Returns: + Identifier string. + + Raises: + ParseError: If an identifier couldn't be consumed. + """ + result = self.token + if not self._IDENTIFIER.match(result): + raise self.ParseError('Expected identifier.') + self.NextToken() + return result + + def TryConsumeIdentifierOrNumber(self): + try: + self.ConsumeIdentifierOrNumber() + return True + except ParseError: + return False + + def ConsumeIdentifierOrNumber(self): + """Consumes protocol message field identifier. + + Returns: + Identifier string. + + Raises: + ParseError: If an identifier couldn't be consumed. + """ + result = self.token + if not self._IDENTIFIER_OR_NUMBER.match(result): + raise self.ParseError('Expected identifier or number, got %s.' % result) + self.NextToken() + return result + + def TryConsumeInteger(self): + try: + self.ConsumeInteger() + return True + except ParseError: + return False + + def ConsumeInteger(self): + """Consumes an integer number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an integer couldn't be consumed. + """ + try: + result = _ParseAbstractInteger(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def TryConsumeFloat(self): + try: + self.ConsumeFloat() + return True + except ParseError: + return False + + def ConsumeFloat(self): + """Consumes an floating point number. + + Returns: + The number parsed. + + Raises: + ParseError: If a floating point number couldn't be consumed. + """ + try: + result = ParseFloat(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ConsumeBool(self): + """Consumes a boolean value. + + Returns: + The bool parsed. + + Raises: + ParseError: If a boolean value couldn't be consumed. + """ + try: + result = ParseBool(self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def TryConsumeByteString(self): + try: + self.ConsumeByteString() + return True + except ParseError: + return False + + def ConsumeString(self): + """Consumes a string value. + + Returns: + The string parsed. + + Raises: + ParseError: If a string value couldn't be consumed. + """ + the_bytes = self.ConsumeByteString() + try: + return str(the_bytes, 'utf-8') + except UnicodeDecodeError as e: + raise self._StringParseError(e) + + def ConsumeByteString(self): + """Consumes a byte array value. + + Returns: + The array parsed (as a string). + + Raises: + ParseError: If a byte array value couldn't be consumed. + """ + the_list = [self._ConsumeSingleByteString()] + while self.token and self.token[0] in _QUOTES: + the_list.append(self._ConsumeSingleByteString()) + return b''.join(the_list) + + def _ConsumeSingleByteString(self): + """Consume one token of a string literal. + + String literals (whether bytes or text) can come in multiple adjacent + tokens which are automatically concatenated, like in C or Python. This + method only consumes one token. + + Returns: + The token parsed. + Raises: + ParseError: When the wrong format data is found. + """ + text = self.token + if len(text) < 1 or text[0] not in _QUOTES: + raise self.ParseError('Expected string but found: %r' % (text,)) + + if len(text) < 2 or text[-1] != text[0]: + raise self.ParseError('String missing ending quote: %r' % (text,)) + + try: + result = text_encoding.CUnescape(text[1:-1]) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ConsumeEnum(self, field): + try: + result = ParseEnum(field, self.token) + except ValueError as e: + raise self.ParseError(str(e)) + self.NextToken() + return result + + def ParseErrorPreviousToken(self, message): + """Creates and *returns* a ParseError for the previously read token. + + Args: + message: A message to set for the exception. + + Returns: + A ParseError instance. + """ + return ParseError(message, self._previous_line + 1, + self._previous_column + 1) + + def ParseError(self, message): + """Creates and *returns* a ParseError for the current token.""" + return ParseError('\'' + self._current_line + '\': ' + message, + self._line + 1, self._column + 1) + + def _StringParseError(self, e): + return self.ParseError('Couldn\'t parse string: ' + str(e)) + + def NextToken(self): + """Reads the next meaningful token.""" + self._previous_line = self._line + self._previous_column = self._column + self.contains_silent_marker_before_current_token = False + + self._column += len(self.token) + self._SkipWhitespace() + + if not self._more_lines: + self.token = '' + return + + match = self._TOKEN.match(self._current_line, self._column) + if not match and not self._skip_comments: + match = self._COMMENT.match(self._current_line, self._column) + if match: + token = match.group(0) + self.token = token + else: + self.token = self._current_line[self._column] + +# Aliased so it can still be accessed by current visibility violators. +# TODO: Migrate violators to textformat_tokenizer. +_Tokenizer = Tokenizer # pylint: disable=invalid-name + + +def _ConsumeInt32(tokenizer): + """Consumes a signed 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If a signed 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=True, is_long=False) + + +def _ConsumeUint32(tokenizer): + """Consumes an unsigned 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an unsigned 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=False, is_long=False) + + +def _TryConsumeInt64(tokenizer): + try: + _ConsumeInt64(tokenizer) + return True + except ParseError: + return False + + +def _ConsumeInt64(tokenizer): + """Consumes a signed 32bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If a signed 32bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=True, is_long=True) + + +def _TryConsumeUint64(tokenizer): + try: + _ConsumeUint64(tokenizer) + return True + except ParseError: + return False + + +def _ConsumeUint64(tokenizer): + """Consumes an unsigned 64bit integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + + Returns: + The integer parsed. + + Raises: + ParseError: If an unsigned 64bit integer couldn't be consumed. + """ + return _ConsumeInteger(tokenizer, is_signed=False, is_long=True) + + +def _ConsumeInteger(tokenizer, is_signed=False, is_long=False): + """Consumes an integer number from tokenizer. + + Args: + tokenizer: A tokenizer used to parse the number. + is_signed: True if a signed integer must be parsed. + is_long: True if a long integer must be parsed. + + Returns: + The integer parsed. + + Raises: + ParseError: If an integer with given characteristics couldn't be consumed. + """ + try: + result = ParseInteger(tokenizer.token, is_signed=is_signed, is_long=is_long) + except ValueError as e: + raise tokenizer.ParseError(str(e)) + tokenizer.NextToken() + return result + + +def ParseInteger(text, is_signed=False, is_long=False): + """Parses an integer. + + Args: + text: The text to parse. + is_signed: True if a signed integer must be parsed. + is_long: True if a long integer must be parsed. + + Returns: + The integer value. + + Raises: + ValueError: Thrown Iff the text is not a valid integer. + """ + # Do the actual parsing. Exception handling is propagated to caller. + result = _ParseAbstractInteger(text) + + # Check if the integer is sane. Exceptions handled by callers. + checker = _INTEGER_CHECKERS[2 * int(is_long) + int(is_signed)] + checker.CheckValue(result) + return result + + +def _ParseAbstractInteger(text): + """Parses an integer without checking size/signedness. + + Args: + text: The text to parse. + + Returns: + The integer value. + + Raises: + ValueError: Thrown Iff the text is not a valid integer. + """ + # Do the actual parsing. Exception handling is propagated to caller. + orig_text = text + c_octal_match = re.match(r'(-?)0(\d+)$', text) + if c_octal_match: + # Python 3 no longer supports 0755 octal syntax without the 'o', so + # we always use the '0o' prefix for multi-digit numbers starting with 0. + text = c_octal_match.group(1) + '0o' + c_octal_match.group(2) + try: + return int(text, 0) + except ValueError: + raise ValueError('Couldn\'t parse integer: %s' % orig_text) + + +def ParseFloat(text): + """Parse a floating point number. + + Args: + text: Text to parse. + + Returns: + The number parsed. + + Raises: + ValueError: If a floating point number couldn't be parsed. + """ + try: + # Assume Python compatible syntax. + return float(text) + except ValueError: + # Check alternative spellings. + if _FLOAT_INFINITY.match(text): + if text[0] == '-': + return float('-inf') + else: + return float('inf') + elif _FLOAT_NAN.match(text): + return float('nan') + else: + # assume '1.0f' format + try: + return float(text.rstrip('f')) + except ValueError: + raise ValueError('Couldn\'t parse float: %s' % text) + + +def ParseBool(text): + """Parse a boolean value. + + Args: + text: Text to parse. + + Returns: + Boolean values parsed + + Raises: + ValueError: If text is not a valid boolean. + """ + if text in ('true', 't', '1', 'True'): + return True + elif text in ('false', 'f', '0', 'False'): + return False + else: + raise ValueError('Expected "true" or "false".') + + +def ParseEnum(field, value): + """Parse an enum value. + + The value can be specified by a number (the enum value), or by + a string literal (the enum name). + + Args: + field: Enum field descriptor. + value: String value. + + Returns: + Enum value number. + + Raises: + ValueError: If the enum value could not be parsed. + """ + enum_descriptor = field.enum_type + try: + number = int(value, 0) + except ValueError: + # Identifier. + enum_value = enum_descriptor.values_by_name.get(value, None) + if enum_value is None: + raise ValueError('Enum type "%s" has no value named %s.' % + (enum_descriptor.full_name, value)) + else: + if not field.enum_type.is_closed: + return number + enum_value = enum_descriptor.values_by_number.get(number, None) + if enum_value is None: + raise ValueError('Enum type "%s" has no value with number %d.' % + (enum_descriptor.full_name, number)) + return enum_value.number diff --git a/google/protobuf/timestamp_pb2.py b/google/protobuf/timestamp_pb2.py new file mode 100644 index 0000000..5a3b4b5 --- /dev/null +++ b/google/protobuf/timestamp_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/timestamp.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/timestamp.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fgoogle/protobuf/timestamp.proto\x12\x0fgoogle.protobuf\";\n\tTimestamp\x12\x18\n\x07seconds\x18\x01 \x01(\x03R\x07seconds\x12\x14\n\x05nanos\x18\x02 \x01(\x05R\x05nanosB\x85\x01\n\x13\x63om.google.protobufB\x0eTimestampProtoP\x01Z2google.golang.org/protobuf/types/known/timestamppb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.timestamp_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\016TimestampProtoP\001Z2google.golang.org/protobuf/types/known/timestamppb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _globals['_TIMESTAMP']._serialized_start=52 + _globals['_TIMESTAMP']._serialized_end=111 +# @@protoc_insertion_point(module_scope) diff --git a/google/protobuf/type_pb2.py b/google/protobuf/type_pb2.py new file mode 100644 index 0000000..e6d651b --- /dev/null +++ b/google/protobuf/type_pb2.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/type.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/type.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import source_context_pb2 as google_dot_protobuf_dot_source__context__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1agoogle/protobuf/type.proto\x12\x0fgoogle.protobuf\x1a\x19google/protobuf/any.proto\x1a$google/protobuf/source_context.proto\"\xa7\x02\n\x04Type\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12.\n\x06\x66ields\x18\x02 \x03(\x0b\x32\x16.google.protobuf.FieldR\x06\x66ields\x12\x16\n\x06oneofs\x18\x03 \x03(\tR\x06oneofs\x12\x31\n\x07options\x18\x04 \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\x12\x45\n\x0esource_context\x18\x05 \x01(\x0b\x32\x1e.google.protobuf.SourceContextR\rsourceContext\x12/\n\x06syntax\x18\x06 \x01(\x0e\x32\x17.google.protobuf.SyntaxR\x06syntax\x12\x18\n\x07\x65\x64ition\x18\x07 \x01(\tR\x07\x65\x64ition\"\xb4\x06\n\x05\x46ield\x12/\n\x04kind\x18\x01 \x01(\x0e\x32\x1b.google.protobuf.Field.KindR\x04kind\x12\x44\n\x0b\x63\x61rdinality\x18\x02 \x01(\x0e\x32\".google.protobuf.Field.CardinalityR\x0b\x63\x61rdinality\x12\x16\n\x06number\x18\x03 \x01(\x05R\x06number\x12\x12\n\x04name\x18\x04 \x01(\tR\x04name\x12\x19\n\x08type_url\x18\x06 \x01(\tR\x07typeUrl\x12\x1f\n\x0boneof_index\x18\x07 \x01(\x05R\noneofIndex\x12\x16\n\x06packed\x18\x08 \x01(\x08R\x06packed\x12\x31\n\x07options\x18\t \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\x12\x1b\n\tjson_name\x18\n \x01(\tR\x08jsonName\x12#\n\rdefault_value\x18\x0b \x01(\tR\x0c\x64\x65\x66\x61ultValue\"\xc8\x02\n\x04Kind\x12\x10\n\x0cTYPE_UNKNOWN\x10\x00\x12\x0f\n\x0bTYPE_DOUBLE\x10\x01\x12\x0e\n\nTYPE_FLOAT\x10\x02\x12\x0e\n\nTYPE_INT64\x10\x03\x12\x0f\n\x0bTYPE_UINT64\x10\x04\x12\x0e\n\nTYPE_INT32\x10\x05\x12\x10\n\x0cTYPE_FIXED64\x10\x06\x12\x10\n\x0cTYPE_FIXED32\x10\x07\x12\r\n\tTYPE_BOOL\x10\x08\x12\x0f\n\x0bTYPE_STRING\x10\t\x12\x0e\n\nTYPE_GROUP\x10\n\x12\x10\n\x0cTYPE_MESSAGE\x10\x0b\x12\x0e\n\nTYPE_BYTES\x10\x0c\x12\x0f\n\x0bTYPE_UINT32\x10\r\x12\r\n\tTYPE_ENUM\x10\x0e\x12\x11\n\rTYPE_SFIXED32\x10\x0f\x12\x11\n\rTYPE_SFIXED64\x10\x10\x12\x0f\n\x0bTYPE_SINT32\x10\x11\x12\x0f\n\x0bTYPE_SINT64\x10\x12\"t\n\x0b\x43\x61rdinality\x12\x17\n\x13\x43\x41RDINALITY_UNKNOWN\x10\x00\x12\x18\n\x14\x43\x41RDINALITY_OPTIONAL\x10\x01\x12\x18\n\x14\x43\x41RDINALITY_REQUIRED\x10\x02\x12\x18\n\x14\x43\x41RDINALITY_REPEATED\x10\x03\"\x99\x02\n\x04\x45num\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x38\n\tenumvalue\x18\x02 \x03(\x0b\x32\x1a.google.protobuf.EnumValueR\tenumvalue\x12\x31\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\x12\x45\n\x0esource_context\x18\x04 \x01(\x0b\x32\x1e.google.protobuf.SourceContextR\rsourceContext\x12/\n\x06syntax\x18\x05 \x01(\x0e\x32\x17.google.protobuf.SyntaxR\x06syntax\x12\x18\n\x07\x65\x64ition\x18\x06 \x01(\tR\x07\x65\x64ition\"j\n\tEnumValue\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n\x06number\x18\x02 \x01(\x05R\x06number\x12\x31\n\x07options\x18\x03 \x03(\x0b\x32\x17.google.protobuf.OptionR\x07options\"H\n\x06Option\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.AnyR\x05value*C\n\x06Syntax\x12\x11\n\rSYNTAX_PROTO2\x10\x00\x12\x11\n\rSYNTAX_PROTO3\x10\x01\x12\x13\n\x0fSYNTAX_EDITIONS\x10\x02\x42{\n\x13\x63om.google.protobufB\tTypeProtoP\x01Z-google.golang.org/protobuf/types/known/typepb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.type_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\tTypeProtoP\001Z-google.golang.org/protobuf/types/known/typepb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _globals['_SYNTAX']._serialized_start=1699 + _globals['_SYNTAX']._serialized_end=1766 + _globals['_TYPE']._serialized_start=113 + _globals['_TYPE']._serialized_end=408 + _globals['_FIELD']._serialized_start=411 + _globals['_FIELD']._serialized_end=1231 + _globals['_FIELD_KIND']._serialized_start=785 + _globals['_FIELD_KIND']._serialized_end=1113 + _globals['_FIELD_CARDINALITY']._serialized_start=1115 + _globals['_FIELD_CARDINALITY']._serialized_end=1231 + _globals['_ENUM']._serialized_start=1234 + _globals['_ENUM']._serialized_end=1515 + _globals['_ENUMVALUE']._serialized_start=1517 + _globals['_ENUMVALUE']._serialized_end=1623 + _globals['_OPTION']._serialized_start=1625 + _globals['_OPTION']._serialized_end=1697 +# @@protoc_insertion_point(module_scope) diff --git a/google/protobuf/unknown_fields.py b/google/protobuf/unknown_fields.py new file mode 100644 index 0000000..9b1e549 --- /dev/null +++ b/google/protobuf/unknown_fields.py @@ -0,0 +1,97 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Contains Unknown Fields APIs. + +Simple usage example: + unknown_field_set = UnknownFieldSet(message) + for unknown_field in unknown_field_set: + wire_type = unknown_field.wire_type + field_number = unknown_field.field_number + data = unknown_field.data +""" + + +from google.protobuf.internal import api_implementation + +if api_implementation._c_module is not None: # pylint: disable=protected-access + UnknownFieldSet = api_implementation._c_module.UnknownFieldSet # pylint: disable=protected-access +else: + from google.protobuf.internal import decoder # pylint: disable=g-import-not-at-top + from google.protobuf.internal import wire_format # pylint: disable=g-import-not-at-top + + class UnknownField: + """A parsed unknown field.""" + + # Disallows assignment to other attributes. + __slots__ = ['_field_number', '_wire_type', '_data'] + + def __init__(self, field_number, wire_type, data): + self._field_number = field_number + self._wire_type = wire_type + self._data = data + return + + @property + def field_number(self): + return self._field_number + + @property + def wire_type(self): + return self._wire_type + + @property + def data(self): + return self._data + + class UnknownFieldSet: + """UnknownField container.""" + + # Disallows assignment to other attributes. + __slots__ = ['_values'] + + def __init__(self, msg): + + def InternalAdd(field_number, wire_type, data): + unknown_field = UnknownField(field_number, wire_type, data) + self._values.append(unknown_field) + + self._values = [] + msg_des = msg.DESCRIPTOR + # pylint: disable=protected-access + unknown_fields = msg._unknown_fields + if (msg_des.has_options and + msg_des.GetOptions().message_set_wire_format): + local_decoder = decoder.UnknownMessageSetItemDecoder() + for _, buffer in unknown_fields: + (field_number, data) = local_decoder(memoryview(buffer)) + InternalAdd(field_number, wire_format.WIRETYPE_LENGTH_DELIMITED, data) + else: + for tag_bytes, buffer in unknown_fields: + # pylint: disable=protected-access + (tag, _) = decoder._DecodeVarint(tag_bytes, 0) + field_number, wire_type = wire_format.UnpackTag(tag) + if field_number == 0: + raise RuntimeError('Field number 0 is illegal.') + (data, _) = decoder._DecodeUnknownField( + memoryview(buffer), 0, wire_type) + InternalAdd(field_number, wire_type, data) + + def __getitem__(self, index): + size = len(self._values) + if index < 0: + index += size + if index < 0 or index >= size: + raise IndexError('index %d out of range'.index) + + return self._values[index] + + def __len__(self): + return len(self._values) + + def __iter__(self): + return iter(self._values) diff --git a/google/protobuf/util/__init__.py b/google/protobuf/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/google/protobuf/wrappers_pb2.py b/google/protobuf/wrappers_pb2.py new file mode 100644 index 0000000..24b6421 --- /dev/null +++ b/google/protobuf/wrappers_pb2.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: google/protobuf/wrappers.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'google/protobuf/wrappers.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1egoogle/protobuf/wrappers.proto\x12\x0fgoogle.protobuf\"#\n\x0b\x44oubleValue\x12\x14\n\x05value\x18\x01 \x01(\x01R\x05value\"\"\n\nFloatValue\x12\x14\n\x05value\x18\x01 \x01(\x02R\x05value\"\"\n\nInt64Value\x12\x14\n\x05value\x18\x01 \x01(\x03R\x05value\"#\n\x0bUInt64Value\x12\x14\n\x05value\x18\x01 \x01(\x04R\x05value\"\"\n\nInt32Value\x12\x14\n\x05value\x18\x01 \x01(\x05R\x05value\"#\n\x0bUInt32Value\x12\x14\n\x05value\x18\x01 \x01(\rR\x05value\"!\n\tBoolValue\x12\x14\n\x05value\x18\x01 \x01(\x08R\x05value\"#\n\x0bStringValue\x12\x14\n\x05value\x18\x01 \x01(\tR\x05value\"\"\n\nBytesValue\x12\x14\n\x05value\x18\x01 \x01(\x0cR\x05valueB\x83\x01\n\x13\x63om.google.protobufB\rWrappersProtoP\x01Z1google.golang.org/protobuf/types/known/wrapperspb\xf8\x01\x01\xa2\x02\x03GPB\xaa\x02\x1eGoogle.Protobuf.WellKnownTypesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google.protobuf.wrappers_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\023com.google.protobufB\rWrappersProtoP\001Z1google.golang.org/protobuf/types/known/wrapperspb\370\001\001\242\002\003GPB\252\002\036Google.Protobuf.WellKnownTypes' + _globals['_DOUBLEVALUE']._serialized_start=51 + _globals['_DOUBLEVALUE']._serialized_end=86 + _globals['_FLOATVALUE']._serialized_start=88 + _globals['_FLOATVALUE']._serialized_end=122 + _globals['_INT64VALUE']._serialized_start=124 + _globals['_INT64VALUE']._serialized_end=158 + _globals['_UINT64VALUE']._serialized_start=160 + _globals['_UINT64VALUE']._serialized_end=195 + _globals['_INT32VALUE']._serialized_start=197 + _globals['_INT32VALUE']._serialized_end=231 + _globals['_UINT32VALUE']._serialized_start=233 + _globals['_UINT32VALUE']._serialized_end=268 + _globals['_BOOLVALUE']._serialized_start=270 + _globals['_BOOLVALUE']._serialized_end=303 + _globals['_STRINGVALUE']._serialized_start=305 + _globals['_STRINGVALUE']._serialized_end=340 + _globals['_BYTESVALUE']._serialized_start=342 + _globals['_BYTESVALUE']._serialized_end=376 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/__init__.py b/hoymiles_wifi/__init__.py new file mode 100644 index 0000000..916e9e7 --- /dev/null +++ b/hoymiles_wifi/__init__.py @@ -0,0 +1,9 @@ +"""Init file for hoymiles_wifi package.""" + +import logging +import os + +LOGLEVEL = os.environ.get("LOGLEVEL", "INFO").upper() +logging.basicConfig(level=LOGLEVEL) + +logger = logging.getLogger(__name__) diff --git a/hoymiles_wifi/__main__.py b/hoymiles_wifi/__main__.py new file mode 100644 index 0000000..73d013e --- /dev/null +++ b/hoymiles_wifi/__main__.py @@ -0,0 +1,398 @@ +"""Contains the main functionality of the hoymiles_wifi package.""" + +from __future__ import annotations + +import argparse +import asyncio +import json +import sys +from dataclasses import asdict, dataclass + +from google.protobuf.json_format import MessageToJson +from google.protobuf.message import Message + +from hoymiles_wifi.const import DTU_FIRMWARE_URL_00_01_11, MAX_POWER_LIMIT +from hoymiles_wifi.dtu import DTU +from hoymiles_wifi.hoymiles import ( + generate_dtu_version_string, + generate_inverter_serial_number, + generate_sw_version_string, + generate_version_string, + get_dtu_model_name, + get_inverter_model_name, +) +from hoymiles_wifi.protobuf import ( + AppGetHistPower_pb2, + APPHeartbeatPB_pb2, + APPInfomationData_pb2, + CommandPB_pb2, + GetConfig_pb2, + InfomationData_pb2, + NetworkInfo_pb2, + RealData_pb2, + RealDataNew_pb2, +) + +RED = "\033[91m" +END = "\033[0m" + + +@dataclass +class VersionInfo: + """Represents version information for the hoymiles_wifi package.""" + + dtu_hw_version: str + dtu_sw_version: str + inverter_hw_version: str + inverter_sw_version: str + + def __str__(self: VersionInfo) -> str: + """Return a string representation of the VersionInfo object.""" + + return ( + f'dtu_hw_version: "{self.dtu_hw_version}"\n' + f'dtu_sw_version: "{self.dtu_sw_version}"\n' + f'inverter_hw_version: "{self.inverter_hw_version}"\n' + f'inverter_sw_version: "{self.inverter_sw_version}"\n' + ) + + def to_dict(self: VersionInfo) -> dict: + """Convert the VersionInfo object to a dictionary.""" + + return asdict(self) + + +# Inverter commands +async def async_get_real_data_new( + dtu: DTU, +) -> RealDataNew_pb2.RealDataNewResDTO | None: + """Get real data from the inverter asynchronously.""" + + return await dtu.async_get_real_data_new() + + +async def async_get_real_data(dtu: DTU) -> RealData_pb2.RealDataResDTO | None: + """Get real data from the inverter asynchronously.""" + + return await dtu.async_get_real_data() + + +async def async_get_config(dtu: DTU) -> GetConfig_pb2.GetConfigResDTO | None: + """Get the config from the inverter asynchronously.""" + + return await dtu.async_get_config() + + +async def async_network_info( + dtu: DTU, +) -> NetworkInfo_pb2.NetworkInfoResDTO | None: + """Get network information from the inverter asynchronously.""" + + return await dtu.async_network_info() + + +async def async_app_information_data( + dtu: DTU, +) -> APPInfomationData_pb2.AppInfomationDataResDTO | None: + """Get application information data from the inverter asynchronously.""" + + return await dtu.async_app_information_data() + + +async def async_app_get_hist_power( + dtu: DTU, +) -> AppGetHistPower_pb2.AppGetHistPowerResDTO: + """Get historical power data from the inverter asynchronously.""" + + return await dtu.async_app_get_hist_power() + + +async def async_set_power_limit( + dtu: DTU, +) -> CommandPB_pb2.CommandResDTO | None: + """Set the power limit of the inverter asynchronously.""" + + print( # noqa: T201 + RED + + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "!!! Danger zone! This will change the power limit of the dtu. !!!\n" + + "!!! Please be careful and make sure you know what you are doing. !!!\n" + + "!!! Only proceed if you know what you are doing. !!!\n" + + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + END, + ) + + cont = input("Do you want to continue? (y/n): ") + if cont != "y": + return None + + power_limit = int(input("Enter the new power limit (0-100): ")) + + if power_limit < 0 or power_limit > MAX_POWER_LIMIT: + print("Error. Invalid power limit!") # noqa: T201 + return None + + print(f"Setting power limit to {power_limit}%") # noqa: T201 + cont = input("Are you sure? (y/n): ") + + if cont != "y": + return None + + return await dtu.async_set_power_limit(power_limit) + + +async def async_set_wifi(dtu: DTU) -> CommandPB_pb2.CommandResDTO | None: + """Set the wifi SSID and password of the inverter asynchronously.""" + + wifi_ssid = input("Enter the new wifi SSID: ").strip() + wifi_password = input("Enter the new wifi password: ").strip() + print(f'Setting wifi to "{wifi_ssid}"') # noqa: T201 + print(f'Setting wifi password to "{wifi_password}"') # noqa: T201 + cont = input("Are you sure? (y/n): ") + if cont != "y": + return None + return await dtu.async_set_wifi(wifi_ssid, wifi_password) + + +async def async_firmware_update(dtu: DTU) -> CommandPB_pb2.CommandResDTO | None: + """Update the firmware of the DTU asynchronously.""" + + print( # noqa: T201 + RED + + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "!!! Danger zone! This will update the firmeware of the DTU. !!!\n" + + "!!! Please be careful and make sure you know what you are doing. !!!\n" + + "!!! Only proceed if you know what you are doing. !!!\n" + + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + END, + ) + + cont = input("Do you want to continue? (y/n): ") + if cont != "y": + return None + + print("Please select a firmware version:") # noqa: T201 + print("1.) V00.01.11") # noqa: T201 + print("2.) Custom URL") # noqa: T201 + + while True: + selection = input("Enter your selection (1 or 2): ") + + if selection == "1": + url = DTU_FIRMWARE_URL_00_01_11 + break + if selection == "2": + url = input("Enter the custom URL: ").strip() + break + + print("Invalid selection. Please enter 1 or 2.") # noqa: T201 + + print() # noqa: T201 + print(f'Firmware update URL: "{url}"') # noqa: T201 + print() # noqa: T201 + + cont = input("Do you want to continue? (y/n): ") + if cont != "y": + return None + + return await dtu.async_update_dtu_firmware() + + +async def async_restart_dtu(dtu: DTU) -> CommandPB_pb2.CommandResDTO | None: + """Restart the DTU asynchronously.""" + + cont = input("Do you want to restart the DTU? (y/n): ") + if cont != "y": + return None + + return await dtu.async_restart_dtu() + + +async def async_turn_on_inverter(dtu: DTU) -> CommandPB_pb2.CommandResDTO | None: + """Turn on the inverter asynchronously.""" + + inverter_serial = input("Enter the inverter serial number to turn *ON*: ") + + cont = input(f"Do you want to turn *ON* the Inverter {inverter_serial}? (y/n): ") + if cont != "y": + return None + + return await dtu.async_turn_on_inverter(inverter_serial) + + +async def async_turn_off_inverter(dtu: DTU) -> CommandPB_pb2.CommandResDTO | None: + """Turn off the inverter asynchronously.""" + + inverter_serial = input("Enter the inverter serial number to turn *OFF*: ") + + cont = input(f"Do you want to turn *OFF* the Inverter {inverter_serial}? (y/n): ") + if cont != "y": + return None + + return await dtu.async_turn_off_inverter(inverter_serial) + + +async def async_get_information_data( + dtu: DTU, +) -> InfomationData_pb2.InfomationDataResDTO: + """Get information data from the dtu asynchronously.""" + + return await dtu.async_get_information_data() + + +async def async_get_version_info(dtu: DTU) -> VersionInfo | None: + """Get version information from the dtu asynchronously.""" + + response = await async_app_information_data(dtu) + + if not response: + return None + + return VersionInfo( + dtu_hw_version="H" + + generate_dtu_version_string(response.dtu_info.dtu_hw_version), + dtu_sw_version="V" + + generate_dtu_version_string(response.dtu_info.dtu_sw_version), + inverter_hw_version="H" + + generate_version_string(response.pv_info[0].pv_hw_version), + inverter_sw_version="V" + + generate_sw_version_string(response.pv_info[0].pv_sw_version), + ) + + +async def async_heatbeat(dtu: DTU) -> APPHeartbeatPB_pb2.APPHeartbeatResDTO | None: + """Request a heartbeat from the dtu asynchronously.""" + + return await dtu.async_heartbeat() + + +async def async_identify_dtu(dtu: DTU) -> str: + """Identify the DTU asynchronously.""" + + real_data = await async_get_real_data_new(dtu) + return get_dtu_model_name(real_data.device_serial_number) + + +async def async_identify_inverters(dtu: DTU) -> list[str]: + """Identify the DTU asynchronously.""" + + inverter_models = [] + real_data = await async_get_real_data_new(dtu) + if real_data: + for sgs_data in real_data.sgs_data: + serial_number = generate_inverter_serial_number(sgs_data.serial_number) + inverter_model = get_inverter_model_name(serial_number) + inverter_models.append(inverter_model) + + for tgs_data in real_data.tgs_data: + serial_number = generate_inverter_serial_number(tgs_data.serial_number) + inverter_model = get_inverter_model_name(serial_number) + inverter_models.append(inverter_model) + + return inverter_models + + +async def async_get_alarm_list(dtu: DTU) -> None: + """Get alarm list from the dtu asynchronously.""" + + return await dtu.async_get_alarm_list() + + +def print_invalid_command(command: str) -> None: + """Print an invalid command message.""" + + print(f"Invalid command: {command}") # noqa: T201 + sys.exit(1) + + +async def main() -> None: + """Execute the main function for the hoymiles_wifi package.""" + + parser = argparse.ArgumentParser(description="Hoymiles DTU Monitoring") + parser.add_argument( + "--host", type=str, required=True, help="IP address or hostname of the DTU" + ) + parser.add_argument( + "--as-json", + action="store_true", + default=False, + help="Format the output as JSON", + ) + parser.add_argument( + "command", + type=str, + choices=[ + "get-real-data-new", + "get-real-data", + "get-config", + "network-info", + "app-information-data", + "app-get-hist-power", + "set-power-limit", + "set-wifi", + "firmware-update", + "restart-dtu", + "turn-on-inverter", + "turn-off-inverter", + "get-information-data", + "get-version-info", + "heartbeat", + "identify-dtu", + "identify-inverters", + "get-alarm-list", + ], + help="Command to execute", + ) + args = parser.parse_args() + + dtu = DTU(args.host) + + # Execute the specified command using a switch case + switch = { + "get-real-data-new": async_get_real_data_new, + "get-real-data": async_get_real_data, + "get-config": async_get_config, + "network-info": async_network_info, + "app-information-data": async_app_information_data, + "app-get-hist-power": async_app_get_hist_power, + "set-power-limit": async_set_power_limit, + "set-wifi": async_set_wifi, + "firmware-update": async_firmware_update, + "restart-dtu": async_restart_dtu, + "turn-on-inverter": async_turn_on_inverter, + "turn-off-inverter": async_turn_off_inverter, + "get-information-data": async_get_information_data, + "get-version-info": async_get_version_info, + "heartbeat": async_heatbeat, + "identify-dtu": async_identify_dtu, + "identify-inverters": async_identify_inverters, + "get-alarm-list": async_get_alarm_list, + } + + command_func = switch.get(args.command, print_invalid_command) + response = await command_func(dtu) + + if response: + if args.as_json: + if isinstance(response, Message): + print(MessageToJson(response)) # noqa: T201 + else: + print(json.dumps(asdict(response), indent=4)) # noqa: T201 + else: + print(f"{args.command.capitalize()} Response: \n{response}") # noqa: T201 + else: + print( # noqa: T201 + f"No response or unable to retrieve response for " + f"{args.command.replace('_', ' ')}", + ) + sys.exit(2) + + +def run_main() -> None: + """Run the main function for the hoymiles_wifi package.""" + + asyncio.run(main()) + + +if __name__ == "__main__": + run_main() diff --git a/hoymiles_wifi/const.py b/hoymiles_wifi/const.py new file mode 100644 index 0000000..c2e14df --- /dev/null +++ b/hoymiles_wifi/const.py @@ -0,0 +1,118 @@ +"""Constants for the Hoymiles WiFi integration.""" + +DTU_PORT = 10081 + +# App -> DTU start with 0xa3, responses start 0xa2 +CMD_HEADER = b"HM" +CMD_APP_INFO_DATA_RES_DTO = b"\xa3\x01" +CMD_HB_RES_DTO = b"\xa3\x02" +CMD_REAL_DATA_RES_DTO = b"\xa3\x03" +CMD_W_INFO_RES_DTO = b"\xa3\x04" +CMD_COMMAND_RES_DTO = b"\xa3\x05" +CMD_COMMAND_STATUS_RES_DTO = b"\xa3\x06" +CMD_DEV_CONFIG_FETCH_RES_DTO = b"\xa3\x07" +CMD_DEV_CONFIG_PUT_RES_DTO = b"\xa3\x08" +CMD_GET_CONFIG = b"\xa3\x09" +CMD_SET_CONFIG = b"\xa3\x10" +CMD_REAL_RES_DTO = b"\xa3\x11" +CMD_GPST_RES_DTO = b"\xa3\x12" +CMD_AUTO_SEARCH = b"\xa3\x13" +CMD_NETWORK_INFO_RES = b"\xa3\x14" +CMD_APP_GET_HIST_POWER_RES = b"\xa3\x15" +CMD_APP_GET_HIST_ED_RES = b"\xa3\x16" +CMD_HB_RES_DTO_ALT = b"\x83\x01" +CMD_REGISTER_RES_DTO = b"\x83\x02" +CMD_STORAGE_DATA_RES = b"\x83\x03" +CMD_COMMAND_RES_DTO_2 = b"\x83\x05" +CMD_COMMAND_STATUS_RES_DTO_2 = b"\x83\x06" +CMD_DEV_CONFIG_FETCH_RES_DTO_2 = b"\x83\x07" +CMD_DEV_CONFIG_PUT_RES_DTO_2 = b"\x83\x08" +CMD_GET_CONFIG_RES = b"\xdb\x08" +CMD_SET_CONFIG_RES = b"\xdb\x07" + +CMD_CLOUD_INFO_DATA_RES_DTO = b"\x23\x01" +CMD_CLOUD_COMMAND_RES_DTO = b"\x23\x05" + +CMD_ACTION_MICRO_DEFAULT = 0 +CMD_ACTION_DTU_REBOOT = 1 +CMD_ACTION_DTU_UPGRADE = 2 +CMD_ACTION_MI_REBOOT = 3 +CMD_ACTION_COLLECT_VERSION = 4 +CMD_ACTION_ANTI_THEFT_SETTING = 5 +CMD_ACTION_MI_START = 6 +CMD_ACTION_MI_SHUTDOWN = 7 +CMD_ACTION_LIMIT_POWER = 8 +CMD_ACTION_REFLUX_CONTROL = 9 +CMD_ACTION_CLEAN_GROUNDING_FAULT = 10 +CMD_ACTION_CT_SET = 11 +CMD_ACTION_MI_LOCK = 12 +CMD_ACTION_MI_UNLOCK = 13 +CMD_ACTION_SET_GRID_FILE = 14 +CMD_ACTION_UPGRADE_MI = 15 +CMD_ACTION_ID_NETWORKING = 16 +CMD_ACTION_REFLUX_NETWORKING = 17 +CMD_ACTION_STOP_CONTROLLER_CMD = 18 +CMD_ACTION_SET_WIFI_PASS = 19 +CMD_ACTION_SET_SVR_DNS_PORT = 20 +CMD_ACTION_SET_GPRS_APN = 21 +CMD_ACTION_ANTI_THEFT_CONTROL = 22 +CMD_ACTION_REPEATER_NETWORKING = 0 +CMD_ACTION_DTU_DEFAULT = 0 +CMD_ACTION_GATEWAY_DEFAULT = 0 +CMD_ACTION_METER_REVERSE = 49 +CMD_ACTION_ALARM_LIST = 50 +CMD_ACTION_GW_REBOOT = 4096 +CMD_ACTION_GW_RESET = 4097 +CMD_ACTION_GW_STOP_RUN = 4098 +CMD_ACTION_GW_COLLECT_REAL_DATA = 4099 +CMD_ACTION_GW_COLLECT_VER = 4100 +CMD_ACTION_GW_AUTO_NETWORKING = 4101 +CMD_ACTION_GW_UPGRADE = 4102 +CMD_ACTION_MICRO_MEMORY_SNAPSHOT = 53 +CMD_ACTION_MICRO_DATA_WAVE = 54 +CMD_ACTION_SET_485_PORT = 36 +CMD_ACTION_THREE_BALANCE_SET = 37 +CMD_ACTION_MI_GRID_PROTECT_SELF = 38 +CMD_ACTION_SUN_SPEC_CONFIG = 39 +CMD_ACTION_POWER_GENERATION_CORRECT = 40 +CMD_ACTION_GRID_FILE_READ = 41 +CMD_ACTION_CLEAN_WARN = 42 +CMD_ACTION_DRM_SETTING = 43 +CMD_ACTION_ES_CONFIG_MANAGER = 0 +CMD_ACTION_ES_USER_SETTING = 0 +CMD_ACTION_READ_MI_HU_WARN = 46 +CMD_ACTION_LIMIT_POWER_PF = 47 +CMD_ACTION_LIMIT_POWER_REACTIVE = 48 +CMD_ACTION_INV_BOOT_UP = 8193 +CMD_ACTION_INV_SHUTDOWN = 8194 +CMD_ACTION_INV_REBOOT = 8195 +CMD_ACTION_INV_RESET = 8196 +CMD_ACTION_INV_CLEAN_WARN = 8197 +CMD_ACTION_INV_CLEAN_HIS_DATA = 8198 +CMD_ACTION_INV_UPLOAD_REAL_DATA = 8199 +CMD_ACTION_INV_FIND_DEV = 8200 +CMD_ACTION_INV_BATTERY_MODE_CONFIG = 0 +CMD_ACTION_BMS_REBOOT = 8224 +CMD_ACTION_BMS_URGENT_CHARGING = 8225 +CMD_ACTION_BMS_BALANCE = 8208 +CMD_ACTION_INV_UPGRADE = 4112 +CMD_ACTION_BMS_UPGRADE = 4112 + + +DEV_DTU = 1 +DEV_REPEATER = 2 +DEV_MICRO = 3 +DEV_MODEL = 4 +DEV_METER = 5 +DEV_INV = 6 +DEV_RSD = 7 +DEV_OP = 8 +DEV_GATEWAY = 9 +DEV_BMS = 10 + +DTU_FIRMWARE_URL_00_01_11 = ( + "http://fwupdate.hoymiles.com/cfs/bin/2311/06/,1488725943932555264.bin" +) + +MAX_POWER_LIMIT = 100 +OFFSET = 28800 diff --git a/hoymiles_wifi/dtu.py b/hoymiles_wifi/dtu.py new file mode 100644 index 0000000..d855698 --- /dev/null +++ b/hoymiles_wifi/dtu.py @@ -0,0 +1,422 @@ +"""DTU communication implementation for Hoymiles WiFi.""" + +from __future__ import annotations + +import asyncio +import struct +import time +from datetime import datetime +from enum import Enum, IntEnum +from typing import Any + +from crcmod import mkCrcFun + +from hoymiles_wifi import logger +from hoymiles_wifi.const import ( + CMD_ACTION_ALARM_LIST, + CMD_ACTION_DTU_REBOOT, + CMD_ACTION_DTU_UPGRADE, + CMD_ACTION_LIMIT_POWER, + CMD_ACTION_MI_SHUTDOWN, + CMD_ACTION_MI_START, + CMD_APP_GET_HIST_POWER_RES, + CMD_APP_INFO_DATA_RES_DTO, + CMD_CLOUD_COMMAND_RES_DTO, + CMD_COMMAND_RES_DTO, + CMD_GET_CONFIG, + CMD_HB_RES_DTO, + CMD_HEADER, + CMD_NETWORK_INFO_RES, + CMD_REAL_DATA_RES_DTO, + CMD_REAL_RES_DTO, + CMD_SET_CONFIG, + DEV_DTU, + DTU_FIRMWARE_URL_00_01_11, + DTU_PORT, + OFFSET, +) +from hoymiles_wifi.hoymiles import convert_inverter_serial_number +from hoymiles_wifi.protobuf import ( + AppGetHistPower_pb2, + APPHeartbeatPB_pb2, + APPInfomationData_pb2, + CommandPB_pb2, + GetConfig_pb2, + InfomationData_pb2, + NetworkInfo_pb2, + RealData_pb2, + RealDataNew_pb2, + SetConfig_pb2, +) +from hoymiles_wifi.utils import initialize_set_config + + +class NetmodeSelect(IntEnum): + """Network mode selection.""" + + WIFI = 1 + SIM = 2 + LAN = 3 + + +class NetworkState(Enum): + """Network state.""" + + Unknown = 0 + Online = 1 + Offline = 2 + + +class DTU: + """DTU class.""" + + def __init__(self, host: str): + """Initialize DTU class.""" + + self.host = host + self.state = NetworkState.Unknown + self.sequence = 0 + self.mutex = asyncio.Lock() + + def get_state(self) -> NetworkState: + """Get DTU state.""" + + return self.state + + def set_state(self, new_state: NetworkState): + """Set DTU state.""" + + if self.state != new_state: + self.state = new_state + logger.debug(f"DTU is {new_state}") + + async def async_get_real_data(self) -> RealData_pb2.RealDataResDTO | None: + """Get real data.""" + + request = RealData_pb2.RealDataResDTO() + request.time_ymd_hms = ( + datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8") + ) + request.time = int(time.time()) + request.offset = OFFSET + request.error_code = 0 + + command = CMD_REAL_DATA_RES_DTO + return await self.async_send_request( + command, request, RealData_pb2.RealDataReqDTO + ) + + async def async_get_real_data_new(self) -> RealDataNew_pb2.RealDataNewResDTO | None: + """Get real data new.""" + + request = RealDataNew_pb2.RealDataNewResDTO() + request.time_ymd_hms = ( + datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8") + ) + request.offset = OFFSET + request.time = int(time.time()) + command = CMD_REAL_RES_DTO + return await self.async_send_request( + command, request, RealDataNew_pb2.RealDataNewReqDTO + ) + + async def async_get_config(self) -> GetConfig_pb2.GetConfigResDTO | None: + """Get config.""" + + request = GetConfig_pb2.GetConfigResDTO() + request.offset = OFFSET + request.time = int(time.time()) - 60 + command = CMD_GET_CONFIG + return await self.async_send_request( + command, + request, + GetConfig_pb2.GetConfigReqDTO, + ) + + async def async_network_info(self) -> NetworkInfo_pb2.NetworkInfoResDTO | None: + """Get network info.""" + + request = NetworkInfo_pb2.NetworkInfoResDTO() + request.offset = OFFSET + request.time = int(time.time()) + command = CMD_NETWORK_INFO_RES + return await self.async_send_request( + command, request, NetworkInfo_pb2.NetworkInfoReqDTO + ) + + async def async_app_information_data( + self, + ) -> APPInfomationData_pb2.APPInfoDataResDTO: + """Get app information data.""" + request = APPInfomationData_pb2.APPInfoDataResDTO() + request.time_ymd_hms = ( + datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8") + ) + request.offset = OFFSET + request.time = int(time.time()) + command = CMD_APP_INFO_DATA_RES_DTO + return await self.async_send_request( + command, request, APPInfomationData_pb2.APPInfoDataReqDTO + ) + + async def async_app_get_hist_power( + self, + ) -> AppGetHistPower_pb2.AppGetHistPowerResDTO | None: + """Get historical power.""" + + request = AppGetHistPower_pb2.AppGetHistPowerResDTO() + request.control_point = 0 + request.offset = OFFSET + request.requested_time = int(time.time()) + request.requested_day = 0 + command = CMD_APP_GET_HIST_POWER_RES + return await self.async_send_request( + command, + request, + AppGetHistPower_pb2.AppGetHistPowerReqDTO, + ) + + async def async_set_power_limit( + self, + power_limit: int, + ) -> CommandPB_pb2.CommandResDTO | None: + """Set power limit.""" + if power_limit < 0 or power_limit > 100: + logger.error("Error. Invalid power limit!") + return + + power_limit = power_limit * 10 + + request = CommandPB_pb2.CommandResDTO() + request.time = int(time.time()) + request.action = CMD_ACTION_LIMIT_POWER + request.package_nub = 1 + request.tid = int(time.time()) + request.data = f"A:{power_limit},B:0,C:0\r".encode() + + command = CMD_COMMAND_RES_DTO + + return await self.async_send_request( + command, request, CommandPB_pb2.CommandReqDTO + ) + + async def async_set_wifi( + self, ssid: str, password: str + ) -> SetConfig_pb2.SetConfigResDTO | None: + """Set wifi.""" + + get_config_req = await self.async_get_config() + + if get_config_req is None: + logger.error("Failed to get config") + return None + + request = initialize_set_config(get_config_req) + + request.time = int(time.time()) + request.offset = OFFSET + request.app_page = 1 + request.netmode_select = NetmodeSelect.WIFI + request.wifi_ssid = ssid.encode("utf-8") + request.wifi_password = password.encode("utf-8") + + command = CMD_SET_CONFIG + return await self.async_send_request( + command, request, SetConfig_pb2.SetConfigReqDTO + ) + + async def async_update_dtu_firmware( + self, + firmware_url: str = DTU_FIRMWARE_URL_00_01_11, + ) -> CommandPB_pb2.CommandResDTO | None: + """Update DTU firmware.""" + + request = CommandPB_pb2.CommandResDTO() + request.action = CMD_ACTION_DTU_UPGRADE + request.package_nub = 1 + request.tid = int(time.time()) + request.data = (firmware_url + "\r").encode("utf-8") + + command = CMD_CLOUD_COMMAND_RES_DTO + return await self.async_send_request( + command, request, CommandPB_pb2.CommandReqDTO + ) + + async def async_restart_dtu(self) -> CommandPB_pb2.CommandResDTO | None: + """Restart DTU.""" + + request = CommandPB_pb2.CommandResDTO() + request.action = CMD_ACTION_DTU_REBOOT + request.package_nub = 1 + request.tid = int(time.time()) + + command = CMD_CLOUD_COMMAND_RES_DTO + return await self.async_send_request( + command, request, CommandPB_pb2.CommandReqDTO + ) + + async def async_turn_on_inverter( + self, inverter_serial: str + ) -> CommandPB_pb2.CommandResDTO | None: + """Turn on Inverter.""" + + inverter_serial_int = convert_inverter_serial_number(inverter_serial) + + request = CommandPB_pb2.CommandResDTO() + request.action = CMD_ACTION_MI_START + request.package_nub = 1 + request.dev_kind = DEV_DTU + request.tid = int(time.time()) + request.mi_to_sn.extend([inverter_serial_int]) + + command = CMD_CLOUD_COMMAND_RES_DTO + + return await self.async_send_request( + command, request, CommandPB_pb2.CommandReqDTO + ) + + async def async_turn_off_inverter( + self, inverter_serial: str + ) -> CommandPB_pb2.CommandResDTO | None: + """Turn off Inverter.""" + + inverter_serial_int = convert_inverter_serial_number(inverter_serial) + + request = CommandPB_pb2.CommandResDTO() + request.action = CMD_ACTION_MI_SHUTDOWN + request.package_nub = 1 + request.dev_kind = DEV_DTU + request.tid = int(time.time()) + request.mi_to_sn.extend([inverter_serial_int]) + + command = CMD_CLOUD_COMMAND_RES_DTO + + return await self.async_send_request( + command, request, CommandPB_pb2.CommandReqDTO + ) + + async def async_get_information_data( + self, + ) -> InfomationData_pb2.InfoDataResDTO | None: + """Get information data.""" + + request = InfomationData_pb2.InfoDataResDTO() + request.time_ymd_hms = ( + datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8") + ) + request.offset = OFFSET + request.time = int(time.time()) + command = CMD_APP_INFO_DATA_RES_DTO + return await self.async_send_request( + command, request, InfomationData_pb2.InfoDataReqDTO + ) + + async def async_heartbeat(self) -> APPHeartbeatPB_pb2.HBReqDTO | None: + """Request heartbeat.""" + + request = APPHeartbeatPB_pb2.HBResDTO() + request.time_ymd_hms = ( + datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8") + ) + request.offset = OFFSET + request.time = int(time.time()) + + command = CMD_HB_RES_DTO + return await self.async_send_request( + command, request, APPHeartbeatPB_pb2.HBReqDTO + ) + + async def async_get_alarm_list(self) -> CommandPB_pb2.CommandResDTO | None: + """Turn off DTU.""" + + request = CommandPB_pb2.CommandResDTO() + request.action = CMD_ACTION_ALARM_LIST + request.package_nub = 1 + request.dev_kind = 0 + request.tid = int(time.time()) + + command = CMD_COMMAND_RES_DTO + return await self.async_send_request( + command, request, CommandPB_pb2.CommandReqDTO + ) + + async def async_send_request( + self, + command: bytes, + request: Any, + response_type: Any, + dtu_port: int = DTU_PORT, + ): + """Send request to DTU.""" + + self.sequence = (self.sequence + 1) & 0xFFFF + + request_as_bytes = request.SerializeToString() + crc16 = mkCrcFun(0x18005, rev=True, initCrc=0xFFFF, xorOut=0x0000)( + request_as_bytes + ) + length = len(request_as_bytes) + 10 + + # compose request message + header = CMD_HEADER + command + message = ( + header + + struct.pack(">HHH", self.sequence, crc16, length) + + request_as_bytes + ) + + address = (self.host, dtu_port) + + async with self.mutex: + try: + reader, writer = await asyncio.open_connection(*address) + + writer.write(message) + await writer.drain() + + buf = await asyncio.wait_for(reader.read(1024), timeout=5) + except (OSError, asyncio.TimeoutError) as e: + logger.debug(f"{e}") + self.set_state(NetworkState.Offline) + return None + finally: + try: + writer.close() + await writer.wait_closed() + except Exception as e: + logger.debug(f"Error closing writer: {e}") + + try: + if len(buf) < 10: + raise ValueError("Buffer is too short for unpacking") + + crc16_target, read_length = struct.unpack(">HH", buf[6:10]) + + logger.debug(f"Read length: {read_length}") + + if len(buf) != read_length: + raise ValueError("Buffer is incomplete") + + response_as_bytes = buf[10:read_length] + + crc16_response = mkCrcFun(0x18005, rev=True, initCrc=0xFFFF, xorOut=0x0000)( + response_as_bytes + ) + + if crc16_response != crc16_target: + logger.debug( + f"CRC16 mismatch: {hex(crc16_response)} != {hex(crc16_target)}" + ) + raise ValueError("CRC16 mismatch") + + parsed = response_type.FromString(response_as_bytes) + + if not parsed: + raise ValueError("Parsing resulted in an empty or falsy value") + except Exception as e: + logger.debug(f"Failed to parse response: {e}") + self.set_state(NetworkState.Unknown) + return None + + self.set_state(NetworkState.Online) + return parsed diff --git a/hoymiles_wifi/hoymiles.py b/hoymiles_wifi/hoymiles.py new file mode 100644 index 0000000..6bbcffa --- /dev/null +++ b/hoymiles_wifi/hoymiles.py @@ -0,0 +1,304 @@ +"""Hoymiles quirks for inverters and DTU.""" + +import struct +from enum import Enum + +from hoymiles_wifi import logger + + +class InverterType(Enum): + """Inverter type.""" + + ONE = "1T" + TWO = "2T" + FOUR = "4T" + SIX = "6T" + + +class InverterSeries(Enum): + """Inverter series.""" + + HM = "HM" + HMS = "HMS" + HMT = "HMT" + + +class InverterPower(Enum): + """Inverter power.""" + + P_100 = "100" + P_250 = "250" + P_300_350_400 = "300/350/400" + P_400 = "400" + P_500 = "500" + P_600_700_800 = "600/700/800" + P_800W = "800W" + P_1000 = "1000" + P_1000W = "1000W" + P_800W_1000W = "800W/1000W" + P_1000_1200_1500 = "1000/1200/1500" + P_1200_1500 = "1200/1500" + P_1600 = "1600" + P_2000 = "2000" + P_2250 = "2250" + + +power_mapping = { + 0x1011: InverterPower.P_100, + 0x1020: InverterPower.P_250, + 0x1021: InverterPower.P_300_350_400, + 0x1121: InverterPower.P_300_350_400, + 0x1125: InverterPower.P_400, + 0x1040: InverterPower.P_500, + 0x1041: InverterPower.P_600_700_800, + 0x1042: InverterPower.P_600_700_800, + 0x1141: InverterPower.P_600_700_800, + 0x1060: InverterPower.P_1000, + 0x1061: InverterPower.P_1200_1500, + 0x1161: InverterPower.P_1000_1200_1500, + 0x1164: InverterPower.P_1600, + 0x1412: InverterPower.P_800W_1000W, + 0x1382: InverterPower.P_2250, +} + + +class DTUType(Enum): + """DTU type.""" + + DTU_G100 = "DTU-G100" + DTU_W100 = "DTU-W100" + DTU_LITE_S = "DTU-Lite-S" + DTU_LITE = "DTU-Lite" + DTU_PRO = "DTU-PRO" + DTU_PRO_S = "DTU-PRO-S" + DTUBI = "DTUBI" + DTU_W100_LITE_S = "DTU-W100/DTU-Lite-S" + DTU_W_LITE = "DTU-WLite" + + +type_mapping = { + 0x10F7: DTUType.DTU_PRO, + 0x10FB: DTUType.DTU_PRO, + 0x4101: DTUType.DTU_PRO, + 0x10FC: DTUType.DTU_PRO, + 0x4120: DTUType.DTU_PRO, + 0x10F8: DTUType.DTU_PRO, + 0x4100: DTUType.DTU_PRO, + 0x10FD: DTUType.DTU_PRO, + 0x4121: DTUType.DTU_PRO, + 0x10D3: DTUType.DTU_W100_LITE_S, + 0x4110: DTUType.DTU_W100_LITE_S, + 0x10D8: DTUType.DTU_W100_LITE_S, + 0x4130: DTUType.DTU_W100_LITE_S, + 0x4132: DTUType.DTU_W100_LITE_S, + 0x4133: DTUType.DTU_W100_LITE_S, + 0x10D9: DTUType.DTU_W100_LITE_S, + 0x4111: DTUType.DTU_W100_LITE_S, + 0x10D2: DTUType.DTU_G100, + 0x10D6: DTUType.DTU_LITE, + 0x10D7: DTUType.DTU_LITE, + 0x4131: DTUType.DTU_LITE, + 0x1124: DTUType.DTUBI, + 0x1125: DTUType.DTUBI, + 0x1403: DTUType.DTUBI, + 0x1144: DTUType.DTUBI, + 0x1143: DTUType.DTUBI, + 0x1145: DTUType.DTUBI, + 0x1412: DTUType.DTUBI, + 0x1164: DTUType.DTUBI, + 0x1165: DTUType.DTUBI, + 0x1166: DTUType.DTUBI, + 0x1167: DTUType.DTUBI, + 0x1222: DTUType.DTUBI, + 0x1422: DTUType.DTUBI, + 0x1423: DTUType.DTUBI, + 0x1361: DTUType.DTUBI, + 0x1362: DTUType.DTUBI, + 0x1381: DTUType.DTUBI, + 0x1382: DTUType.DTUBI, + 0x4143: DTUType.DTUBI, +} + + +def format_number(number: int) -> str: + """Format number to two digits.""" + + return f"{number:02d}" + + +def generate_version_string(version_number: int) -> str: + """Generate version string.""" + + version_string = ( + format_number(version_number // 2048) + + "." + + format_number((version_number // 64) % 32) + + "." + + format_number(version_number % 64) + ) + return version_string + + +def generate_sw_version_string(version_number: int) -> str: + """Generate software version string.""" + + version_number2 = version_number // 10000 + version_number3 = (version_number - (version_number2 * 10000)) // 100 + version_number4 = (version_number - (version_number2 * 10000)) - ( + version_number3 * 100 + ) + + version_string = ( + format_number(version_number2) + + "." + + format_number(version_number3) + + "." + + format_number(version_number4) + ) + return version_string + + +def generate_dtu_version_string(version_number: int, type: str = "") -> str: + """Generate DTU version string.""" + + version_string = "" + version_number2 = version_number % 256 + version_number3 = (version_number // 256) % 16 + + if type == "SRF": + version_string += f"{format_number(version_number // 1048576)}.{format_number((version_number % 65536) // 4096)}.{format_number(version_number3)}.{format_number(version_number2)}" + elif type == "HRF": + version_string += f"{format_number(version_number // 65536)}.{format_number((version_number % 65536) // 4096)}.{format_number(version_number3)}.{format_number(version_number2)}" + else: + version_string += f"{format_number(version_number // 4096)}.{format_number(version_number3)}.{format_number(version_number2)}" + + return version_string + + +def generate_inverter_serial_number(serial_number: int) -> str: + """Generate inverter serial number.""" + + return hex(serial_number)[2:] + + +def convert_inverter_serial_number(serial_number_str: str) -> int: + """Get inverter serial number from string.""" + + return int(serial_number_str, 16) + + +def get_inverter_type(serial_bytes: bytes) -> InverterType: + """Get inverter type.""" + + inverter_type = None + # Access individual bytes + if serial_bytes[0] == 0x11: + if serial_bytes[1] in [0x25, 0x24, 0x22, 0x21]: + inverter_type = InverterType.ONE + elif serial_bytes[1] in [0x44, 0x42, 0x41]: + inverter_type = InverterType.TWO + elif serial_bytes[1] in [0x64, 0x62, 0x61]: + inverter_type = InverterType.FOUR + elif serial_bytes[0] == 0x13: + inverter_type = InverterType.SIX + elif serial_bytes[0] == 0x14: + if serial_bytes[1] in [0x12]: + inverter_type = InverterType.TWO + + if inverter_type is None: + raise ValueError( + f"Unknown inverter type: {hex(serial_bytes[0])} {hex(serial_bytes[1])}" + ) + + return inverter_type + + +def get_inverter_series(serial_bytes: bytes) -> InverterSeries: + """Get inverter series.""" + + series = None + if serial_bytes[0] == 0x11: + if (serial_bytes[1] & 0x0F) == 0x04: + series = InverterSeries.HMS + else: + series = InverterSeries.HM + elif serial_bytes[0] == 0x10: + if serial_bytes[1] & 0x03 == 0x02: + series = InverterSeries.HM + else: + series = InverterSeries.HMS + elif serial_bytes[0] == 0x13: + series = InverterSeries.HMT + elif serial_bytes[0] == 0x14: + series = InverterSeries.HMS + + if series is None: + raise ValueError( + f"Unknown series: {hex(serial_bytes[0])} {hex(serial_bytes[1])}!" + ) + + return series + + +def get_inverter_power(serial_bytes: bytes) -> InverterPower: + """Get inverter power.""" + + inverter_type_bytes = struct.unpack(">H", serial_bytes[:2])[0] + power = power_mapping.get(inverter_type_bytes) + + if power is None: + raise ValueError( + f"Unknown power: {hex(serial_bytes[0])} {hex(serial_bytes[1])}!" + ) + + return power + + +def get_inverter_model_name(serial_number: str) -> str: + """Get hardware model name.""" + + serial_bytes = bytes.fromhex(serial_number) + + try: + inverter_type = get_inverter_type(serial_bytes) + inverter_series = get_inverter_series(serial_bytes) + inverter_power = get_inverter_power(serial_bytes) + except Exception as e: + logger.error(e) + return "Unknown" + else: + inverter_model_name = ( + inverter_series.value + + "-" + + inverter_power.value + + "-" + + inverter_type.value + ) + return inverter_model_name + + +def get_dtu_model_type(serial_bytes: bytes) -> DTUType: + """Get DTU model type.""" + + dtu_type_bytes = struct.unpack(">H", serial_bytes[:2])[0] + + dtu_type = type_mapping.get(dtu_type_bytes) + + if dtu_type is None: + raise ValueError(f"Unknown DTU: {serial_bytes[:2]}!") + + return dtu_type + + +def get_dtu_model_name(serial_number: str) -> str: + """Get DTU model name.""" + + serial_bytes = bytes.fromhex(serial_number) + + try: + dtu_type = get_dtu_model_type(serial_bytes) + except Exception as e: + logger.error(e) + return "Unknown" + else: + return dtu_type.value diff --git a/hoymiles_wifi/protobuf/APPHeartbeatPB.proto b/hoymiles_wifi/protobuf/APPHeartbeatPB.proto new file mode 100644 index 0000000..486af76 --- /dev/null +++ b/hoymiles_wifi/protobuf/APPHeartbeatPB.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +message HBReqDTO { + int32 offset = 1; // Offset value + int32 time = 2; // Timestamp of the request + int32 csq = 3; // Carrier Signal Quality (CSQ) + string dtu_serial_number = 4; // DTU serial number + string device_serial_number = 5; // Device serial number +} + +message HBResDTO { + int32 offset = 1; // Offset value + int32 time = 2; // Timestamp of the response + string time_ymd_hms = 3; // Timestamp in the format YMD_HMS +} diff --git a/hoymiles_wifi/protobuf/APPHeartbeatPB_pb2.py b/hoymiles_wifi/protobuf/APPHeartbeatPB_pb2.py new file mode 100644 index 0000000..92257ff --- /dev/null +++ b/hoymiles_wifi/protobuf/APPHeartbeatPB_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: APPHeartbeatPB.proto +# Protobuf Python Version: 4.25.0 +"""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\x14\x41PPHeartbeatPB.proto\"n\n\x08HBReqDTO\x12\x0e\n\x06offset\x18\x01 \x01(\x05\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x0b\n\x03\x63sq\x18\x03 \x01(\x05\x12\x19\n\x11\x64tu_serial_number\x18\x04 \x01(\t\x12\x1c\n\x14\x64\x65vice_serial_number\x18\x05 \x01(\t\">\n\x08HBResDTO\x12\x0e\n\x06offset\x18\x01 \x01(\x05\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x14\n\x0ctime_ymd_hms\x18\x03 \x01(\tb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'APPHeartbeatPB_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_HBREQDTO']._serialized_start=24 + _globals['_HBREQDTO']._serialized_end=134 + _globals['_HBRESDTO']._serialized_start=136 + _globals['_HBRESDTO']._serialized_end=198 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/APPInfomationData.proto b/hoymiles_wifi/protobuf/APPInfomationData.proto new file mode 100644 index 0000000..f6038bb --- /dev/null +++ b/hoymiles_wifi/protobuf/APPInfomationData.proto @@ -0,0 +1,89 @@ +syntax = "proto3"; + +message APPDtuInfoMO { + int32 device_kind = 1; // Type of the device + int32 dtu_sw_version = 2; // DTU software version + int32 dtu_hw_version = 3; // DTU hardware version + int32 dtu_step_time = 4; // DTU step time + int32 dtu_rf_hw_version = 5; // DTU RF hardware version + int32 dtu_rf_sw_version = 6; // DTU RF software version + int32 access_model = 7; // Access model + int32 communication_time = 8; // Communication time + int32 signal_strength = 9; // Signal strength + string gprs_version = 10; // GPRS version + string wifi_version = 11; // Wifi version + string ka_number = 12; // KA number + int32 dtu_rule_id = 13; // DTU rule ID + int32 dtu_error_code = 14; // DTU error code + int32 dtu485_mode = 15; // DTU485 mode + int32 dtu485_address = 16; // DTU485 address + int32 sub1g_frequency_band = 17; // Sub1G frequency band + int32 sub1g_channel_total_number = 18; // Sub1G channel total number + int32 sub1g_channel_number = 19; // Sub1G channel number + int32 sub1g_rp = 20; // Sub1G RP + int32 sub1g_channel_total = 21; // Sub1G channel total + string gprs_imei = 22; // GPRS IMEI +} + +message APPMeterInfoMO { + int32 device_kind = 1; // Type of the device + int64 meter_serial_number = 2; // Meter serial number + int32 meter_model = 3; // Meter model + int32 meter_ct = 4; // Meter current transformer + int32 communication_way = 5; // Communication way + int32 access_mode = 6; // Access mode + int32 sw_version = 7; // Software version + string meter_value = 8; // Meter value +} + +message APPRpInfoMO { + int32 device_kind = 1; // Type of the device + int64 rp_serial_number = 2; // RP serial number + int32 rp_sw_version = 3; // RP software version + int32 rp_hw_version = 4; // RP hardware version + int32 rp_rule_id = 5; // RP rule ID +} + +message APPPvInfoMO { + int32 device_kind = 1; // Type of the device + int64 pv_serial_number = 2; // PV serial number + int32 pv_usfw = 3; // PV US firmware + int32 pv_sw_version = 4; // PV software version + int32 pv_hw_part_number = 5; // PV hardware part number + int32 pv_hw_version = 6; // PV hardware version + int32 pv_grid_profile_code = 7; // PV grid profile code + int32 pv_grid_profile = 8; // PV grid profile + int32 pv_rf_hw_version = 9; // PV RF hardware version + int32 pv_rf_sw_version = 10; // PV RF software version + int32 mi_rule_id = 11; // MI rule ID +} + +message APPFeatureMO { + int32 key = 1; // Feature key + string value = 2; // Feature value +} + +message APPInfoDataReqDTO { + string dtu_serial_number = 1; // DTU serial number + uint32 timestamp = 2; // Timestamp + int32 device_number = 3; // Device number + int32 pv_number = 4; // PV number + int32 package_number = 5; // Package number + int32 current_package = 6; // Current package + int32 channel = 7; // Channel + APPDtuInfoMO dtu_info = 8; // DTU information + repeated APPMeterInfoMO meter_info = 9; // Meter information + repeated APPRpInfoMO rp_info = 10; // RP information + repeated APPPvInfoMO pv_info = 11; // PV information + uint32 unknown_field = 12; // Unknown field + repeated APPFeatureMO app_features = 13; // Application features +} + +message APPInfoDataResDTO { + string time_ymd_hms = 1; // Date and time + int32 offset = 2; // Offset + int32 current_package = 3; // Current package + int32 error_code = 4; // Error code + uint32 time = 5; // Timestamp +} + diff --git a/hoymiles_wifi/protobuf/APPInfomationData_pb2.py b/hoymiles_wifi/protobuf/APPInfomationData_pb2.py new file mode 100644 index 0000000..e546927 --- /dev/null +++ b/hoymiles_wifi/protobuf/APPInfomationData_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: APPInfomationData.proto +# Protobuf Python Version: 4.25.0 +"""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\x17\x41PPInfomationData.proto\"\xa6\x04\n\x0c\x41PPDtuInfoMO\x12\x13\n\x0b\x64\x65vice_kind\x18\x01 \x01(\x05\x12\x16\n\x0e\x64tu_sw_version\x18\x02 \x01(\x05\x12\x16\n\x0e\x64tu_hw_version\x18\x03 \x01(\x05\x12\x15\n\rdtu_step_time\x18\x04 \x01(\x05\x12\x19\n\x11\x64tu_rf_hw_version\x18\x05 \x01(\x05\x12\x19\n\x11\x64tu_rf_sw_version\x18\x06 \x01(\x05\x12\x14\n\x0c\x61\x63\x63\x65ss_model\x18\x07 \x01(\x05\x12\x1a\n\x12\x63ommunication_time\x18\x08 \x01(\x05\x12\x17\n\x0fsignal_strength\x18\t \x01(\x05\x12\x14\n\x0cgprs_version\x18\n \x01(\t\x12\x14\n\x0cwifi_version\x18\x0b \x01(\t\x12\x11\n\tka_number\x18\x0c \x01(\t\x12\x13\n\x0b\x64tu_rule_id\x18\r \x01(\x05\x12\x16\n\x0e\x64tu_error_code\x18\x0e \x01(\x05\x12\x13\n\x0b\x64tu485_mode\x18\x0f \x01(\x05\x12\x16\n\x0e\x64tu485_address\x18\x10 \x01(\x05\x12\x1c\n\x14sub1g_frequency_band\x18\x11 \x01(\x05\x12\"\n\x1asub1g_channel_total_number\x18\x12 \x01(\x05\x12\x1c\n\x14sub1g_channel_number\x18\x13 \x01(\x05\x12\x10\n\x08sub1g_rp\x18\x14 \x01(\x05\x12\x1b\n\x13sub1g_channel_total\x18\x15 \x01(\x05\x12\x11\n\tgprs_imei\x18\x16 \x01(\t\"\xc2\x01\n\x0e\x41PPMeterInfoMO\x12\x13\n\x0b\x64\x65vice_kind\x18\x01 \x01(\x05\x12\x1b\n\x13meter_serial_number\x18\x02 \x01(\x03\x12\x13\n\x0bmeter_model\x18\x03 \x01(\x05\x12\x10\n\x08meter_ct\x18\x04 \x01(\x05\x12\x19\n\x11\x63ommunication_way\x18\x05 \x01(\x05\x12\x13\n\x0b\x61\x63\x63\x65ss_mode\x18\x06 \x01(\x05\x12\x12\n\nsw_version\x18\x07 \x01(\x05\x12\x13\n\x0bmeter_value\x18\x08 \x01(\t\"~\n\x0b\x41PPRpInfoMO\x12\x13\n\x0b\x64\x65vice_kind\x18\x01 \x01(\x05\x12\x18\n\x10rp_serial_number\x18\x02 \x01(\x03\x12\x15\n\rrp_sw_version\x18\x03 \x01(\x05\x12\x15\n\rrp_hw_version\x18\x04 \x01(\x05\x12\x12\n\nrp_rule_id\x18\x05 \x01(\x05\"\x95\x02\n\x0b\x41PPPvInfoMO\x12\x13\n\x0b\x64\x65vice_kind\x18\x01 \x01(\x05\x12\x18\n\x10pv_serial_number\x18\x02 \x01(\x03\x12\x0f\n\x07pv_usfw\x18\x03 \x01(\x05\x12\x15\n\rpv_sw_version\x18\x04 \x01(\x05\x12\x19\n\x11pv_hw_part_number\x18\x05 \x01(\x05\x12\x15\n\rpv_hw_version\x18\x06 \x01(\x05\x12\x1c\n\x14pv_grid_profile_code\x18\x07 \x01(\x05\x12\x17\n\x0fpv_grid_profile\x18\x08 \x01(\x05\x12\x18\n\x10pv_rf_hw_version\x18\t \x01(\x05\x12\x18\n\x10pv_rf_sw_version\x18\n \x01(\x05\x12\x12\n\nmi_rule_id\x18\x0b \x01(\x05\"*\n\x0c\x41PPFeatureMO\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\t\"\xed\x02\n\x11\x41PPInfoDataReqDTO\x12\x19\n\x11\x64tu_serial_number\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\r\x12\x15\n\rdevice_number\x18\x03 \x01(\x05\x12\x11\n\tpv_number\x18\x04 \x01(\x05\x12\x16\n\x0epackage_number\x18\x05 \x01(\x05\x12\x17\n\x0f\x63urrent_package\x18\x06 \x01(\x05\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\x05\x12\x1f\n\x08\x64tu_info\x18\x08 \x01(\x0b\x32\r.APPDtuInfoMO\x12#\n\nmeter_info\x18\t \x03(\x0b\x32\x0f.APPMeterInfoMO\x12\x1d\n\x07rp_info\x18\n \x03(\x0b\x32\x0c.APPRpInfoMO\x12\x1d\n\x07pv_info\x18\x0b \x03(\x0b\x32\x0c.APPPvInfoMO\x12\x15\n\runknown_field\x18\x0c \x01(\r\x12#\n\x0c\x61pp_features\x18\r \x03(\x0b\x32\r.APPFeatureMO\"t\n\x11\x41PPInfoDataResDTO\x12\x14\n\x0ctime_ymd_hms\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x02 \x01(\x05\x12\x17\n\x0f\x63urrent_package\x18\x03 \x01(\x05\x12\x12\n\nerror_code\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\x05 \x01(\rb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'APPInfomationData_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_APPDTUINFOMO']._serialized_start=28 + _globals['_APPDTUINFOMO']._serialized_end=578 + _globals['_APPMETERINFOMO']._serialized_start=581 + _globals['_APPMETERINFOMO']._serialized_end=775 + _globals['_APPRPINFOMO']._serialized_start=777 + _globals['_APPRPINFOMO']._serialized_end=903 + _globals['_APPPVINFOMO']._serialized_start=906 + _globals['_APPPVINFOMO']._serialized_end=1183 + _globals['_APPFEATUREMO']._serialized_start=1185 + _globals['_APPFEATUREMO']._serialized_end=1227 + _globals['_APPINFODATAREQDTO']._serialized_start=1230 + _globals['_APPINFODATAREQDTO']._serialized_end=1595 + _globals['_APPINFODATARESDTO']._serialized_start=1597 + _globals['_APPINFODATARESDTO']._serialized_end=1713 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/AlarmData.proto b/hoymiles_wifi/protobuf/AlarmData.proto new file mode 100644 index 0000000..68d92f5 --- /dev/null +++ b/hoymiles_wifi/protobuf/AlarmData.proto @@ -0,0 +1,46 @@ +syntax = "proto3"; + +message WInfoMO { + int64 pv_sn = 1; // PV serial number + int32 WCode = 2; // WCode identifier + int32 WNum = 3; // WNum value + int32 WTime1 = 4; // First timestamp value + int32 WTime2 = 5; // Second timestamp value + int32 WData1 = 6; // First data value + int32 WData2 = 7; // Second data value +} + +message WInfoReqDTO { + string dtu_sn = 1; // DTU serial number + int32 time = 2; // Timestamp of the request + repeated WInfoMO mWInfo = 3; // WInfoMO data array +} + +message WInfoResDTO { + string time_ymd_hms = 1; // Timestamp in the format YMD_HMS + int32 error_code = 2; // Error code indicator + int32 offset = 3; // Offset value + int32 time = 4; // Timestamp value +} + +message WWVDataReqDTO { + string dtu_sn = 1; // DTU serial number + int32 time = 2; // Timestamp of the request + int32 package_nub = 3; // Package number + int32 package_now = 4; // Current package number + int64 pv_sn = 5; // PV serial number + int32 WCode = 6; // WCode identifier + int32 WNum = 7; // WNum value + int32 WTime1 = 8; // First timestamp value + int32 WVDataL = 9; // Length of WWVData + int32 WPos = 10; // WPos value + string mWVData = 11; // WWVData string +} + +message WWVDataResDTO { + string time_ymd_hms = 1; // Timestamp in the format YMD_HMS + int32 package_now = 2; // Current package number + int32 error_code = 3; // Error code indicator + int32 offset = 4; // Offset value + int32 time = 5; // Timestamp value +} diff --git a/hoymiles_wifi/protobuf/AlarmData_pb2.py b/hoymiles_wifi/protobuf/AlarmData_pb2.py new file mode 100644 index 0000000..77f5f4e --- /dev/null +++ b/hoymiles_wifi/protobuf/AlarmData_pb2.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: AlarmData.proto +# Protobuf Python Version: 4.25.0 +"""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\x0f\x41larmData.proto\"u\n\x07WInfoMO\x12\r\n\x05pv_sn\x18\x01 \x01(\x03\x12\r\n\x05WCode\x18\x02 \x01(\x05\x12\x0c\n\x04WNum\x18\x03 \x01(\x05\x12\x0e\n\x06WTime1\x18\x04 \x01(\x05\x12\x0e\n\x06WTime2\x18\x05 \x01(\x05\x12\x0e\n\x06WData1\x18\x06 \x01(\x05\x12\x0e\n\x06WData2\x18\x07 \x01(\x05\"E\n\x0bWInfoReqDTO\x12\x0e\n\x06\x64tu_sn\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x18\n\x06mWInfo\x18\x03 \x03(\x0b\x32\x08.WInfoMO\"U\n\x0bWInfoResDTO\x12\x14\n\x0ctime_ymd_hms\x18\x01 \x01(\t\x12\x12\n\nerror_code\x18\x02 \x01(\x05\x12\x0e\n\x06offset\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x05\"\xc3\x01\n\rWWVDataReqDTO\x12\x0e\n\x06\x64tu_sn\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x13\n\x0bpackage_nub\x18\x03 \x01(\x05\x12\x13\n\x0bpackage_now\x18\x04 \x01(\x05\x12\r\n\x05pv_sn\x18\x05 \x01(\x03\x12\r\n\x05WCode\x18\x06 \x01(\x05\x12\x0c\n\x04WNum\x18\x07 \x01(\x05\x12\x0e\n\x06WTime1\x18\x08 \x01(\x05\x12\x0f\n\x07WVDataL\x18\t \x01(\x05\x12\x0c\n\x04WPos\x18\n \x01(\x05\x12\x0f\n\x07mWVData\x18\x0b \x01(\t\"l\n\rWWVDataResDTO\x12\x14\n\x0ctime_ymd_hms\x18\x01 \x01(\t\x12\x13\n\x0bpackage_now\x18\x02 \x01(\x05\x12\x12\n\nerror_code\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\x05 \x01(\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'AlarmData_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_WINFOMO']._serialized_start=19 + _globals['_WINFOMO']._serialized_end=136 + _globals['_WINFOREQDTO']._serialized_start=138 + _globals['_WINFOREQDTO']._serialized_end=207 + _globals['_WINFORESDTO']._serialized_start=209 + _globals['_WINFORESDTO']._serialized_end=294 + _globals['_WWVDATAREQDTO']._serialized_start=297 + _globals['_WWVDATAREQDTO']._serialized_end=492 + _globals['_WWVDATARESDTO']._serialized_start=494 + _globals['_WWVDATARESDTO']._serialized_end=602 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/AppGetHistED.proto b/hoymiles_wifi/protobuf/AppGetHistED.proto new file mode 100644 index 0000000..e6205af --- /dev/null +++ b/hoymiles_wifi/protobuf/AppGetHistED.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +message AppGetHistEDResDTO { + int32 cp = 1; // Control point identifier + int32 oft = 2; // Offset value + uint32 time = 3; // Timestamp of the response +} + +message APPEnergyInfoMO { + uint32 ed = 1; // Energy data + uint32 r_time = 2; // Relative timestamp +} + +message AppGetHistEDReqDTO { + int64 sn = 1; // Serial number + int32 oft = 2; // Offset value + uint32 time = 3; // Timestamp of the request + repeated APPEnergyInfoMO energ = 4; // APPEnergyInfoMO data array +} diff --git a/hoymiles_wifi/protobuf/AppGetHistED_pb2.py b/hoymiles_wifi/protobuf/AppGetHistED_pb2.py new file mode 100644 index 0000000..dde1d57 --- /dev/null +++ b/hoymiles_wifi/protobuf/AppGetHistED_pb2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: AppGetHistED.proto +# Protobuf Python Version: 4.25.0 +"""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\x12\x41ppGetHistED.proto\";\n\x12\x41ppGetHistEDResDTO\x12\n\n\x02\x63p\x18\x01 \x01(\x05\x12\x0b\n\x03oft\x18\x02 \x01(\x05\x12\x0c\n\x04time\x18\x03 \x01(\r\"-\n\x0f\x41PPEnergyInfoMO\x12\n\n\x02\x65\x64\x18\x01 \x01(\r\x12\x0e\n\x06r_time\x18\x02 \x01(\r\"\\\n\x12\x41ppGetHistEDReqDTO\x12\n\n\x02sn\x18\x01 \x01(\x03\x12\x0b\n\x03oft\x18\x02 \x01(\x05\x12\x0c\n\x04time\x18\x03 \x01(\r\x12\x1f\n\x05\x65nerg\x18\x04 \x03(\x0b\x32\x10.APPEnergyInfoMOb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'AppGetHistED_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_APPGETHISTEDRESDTO']._serialized_start=22 + _globals['_APPGETHISTEDRESDTO']._serialized_end=81 + _globals['_APPENERGYINFOMO']._serialized_start=83 + _globals['_APPENERGYINFOMO']._serialized_end=128 + _globals['_APPGETHISTEDREQDTO']._serialized_start=130 + _globals['_APPGETHISTEDREQDTO']._serialized_end=222 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/AppGetHistPower.proto b/hoymiles_wifi/protobuf/AppGetHistPower.proto new file mode 100644 index 0000000..b41dc9c --- /dev/null +++ b/hoymiles_wifi/protobuf/AppGetHistPower.proto @@ -0,0 +1,25 @@ +syntax = "proto3"; + +message AppGetHistPowerResDTO { + int32 control_point = 1; // Control point identifier + int32 offset = 2; // Offset value + uint32 requested_time = 3; // Time requested + uint32 requested_day = 4; // Day requested +} + +message AppGetHistPowerReqDTO { + int64 serial_number = 1; // Device serial number + int32 access_point = 2; // Access point identifier + int32 control_point = 3; // Control point identifier + int32 offset = 4; // Offset value + uint32 request_time = 5; // Timestamp of the request + uint32 start_time = 6; // Start timestamp + uint32 long_term_start = 7; // Long-term start timestamp + uint32 absolute_start = 8; // Absolute start timestamp + uint32 step_time = 9; // Step time + uint32 relative_power = 10; // Relative power + uint32 total_energy = 11; // Total energy + uint32 daily_energy = 12; // Daily energy + repeated int32 power_array = 13; // Array of power values + uint32 warning_number = 14; // Warning number +} diff --git a/hoymiles_wifi/protobuf/AppGetHistPower_pb2.py b/hoymiles_wifi/protobuf/AppGetHistPower_pb2.py new file mode 100644 index 0000000..99fb412 --- /dev/null +++ b/hoymiles_wifi/protobuf/AppGetHistPower_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: AppGetHistPower.proto +# Protobuf Python Version: 4.25.0 +"""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\x15\x41ppGetHistPower.proto\"m\n\x15\x41ppGetHistPowerResDTO\x12\x15\n\rcontrol_point\x18\x01 \x01(\x05\x12\x0e\n\x06offset\x18\x02 \x01(\x05\x12\x16\n\x0erequested_time\x18\x03 \x01(\r\x12\x15\n\rrequested_day\x18\x04 \x01(\r\"\xca\x02\n\x15\x41ppGetHistPowerReqDTO\x12\x15\n\rserial_number\x18\x01 \x01(\x03\x12\x14\n\x0c\x61\x63\x63\x65ss_point\x18\x02 \x01(\x05\x12\x15\n\rcontrol_point\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\x12\x14\n\x0crequest_time\x18\x05 \x01(\r\x12\x12\n\nstart_time\x18\x06 \x01(\r\x12\x17\n\x0flong_term_start\x18\x07 \x01(\r\x12\x16\n\x0e\x61\x62solute_start\x18\x08 \x01(\r\x12\x11\n\tstep_time\x18\t \x01(\r\x12\x16\n\x0erelative_power\x18\n \x01(\r\x12\x14\n\x0ctotal_energy\x18\x0b \x01(\r\x12\x14\n\x0c\x64\x61ily_energy\x18\x0c \x01(\r\x12\x13\n\x0bpower_array\x18\r \x03(\x05\x12\x16\n\x0ewarning_number\x18\x0e \x01(\rb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'AppGetHistPower_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_APPGETHISTPOWERRESDTO']._serialized_start=25 + _globals['_APPGETHISTPOWERRESDTO']._serialized_end=134 + _globals['_APPGETHISTPOWERREQDTO']._serialized_start=137 + _globals['_APPGETHISTPOWERREQDTO']._serialized_end=467 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/AutoSearch.proto b/hoymiles_wifi/protobuf/AutoSearch.proto new file mode 100644 index 0000000..b06cb29 --- /dev/null +++ b/hoymiles_wifi/protobuf/AutoSearch.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +message AutoSearchReqDTO { + string dtu_serial_number = 1; // DTU serial number + int32 time = 2; // Timestamp of the request + int32 package_number = 3; // Total number of packages + int32 current_package = 4; // Current package number + repeated int64 mi_serial_numbers = 5; // Array of MI (Meter Interface) serial numbers +} + +message AutoSearchResDTO { + string ymd_hms = 1; // Timestamp in the format YMD_HMS + int32 offset = 2; // Offset value + int32 current_package = 3; // Current package number + int32 error_code = 4; // Error code indicator + int32 time = 5; // Timestamp of the response +} diff --git a/hoymiles_wifi/protobuf/AutoSearch_pb2.py b/hoymiles_wifi/protobuf/AutoSearch_pb2.py new file mode 100644 index 0000000..fc37994 --- /dev/null +++ b/hoymiles_wifi/protobuf/AutoSearch_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: AutoSearch.proto +# Protobuf Python Version: 4.25.0 +"""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\x10\x41utoSearch.proto\"\x87\x01\n\x10\x41utoSearchReqDTO\x12\x19\n\x11\x64tu_serial_number\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x16\n\x0epackage_number\x18\x03 \x01(\x05\x12\x17\n\x0f\x63urrent_package\x18\x04 \x01(\x05\x12\x19\n\x11mi_serial_numbers\x18\x05 \x03(\x03\"n\n\x10\x41utoSearchResDTO\x12\x0f\n\x07ymd_hms\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x02 \x01(\x05\x12\x17\n\x0f\x63urrent_package\x18\x03 \x01(\x05\x12\x12\n\nerror_code\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\x05 \x01(\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'AutoSearch_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_AUTOSEARCHREQDTO']._serialized_start=21 + _globals['_AUTOSEARCHREQDTO']._serialized_end=156 + _globals['_AUTOSEARCHRESDTO']._serialized_start=158 + _globals['_AUTOSEARCHRESDTO']._serialized_end=268 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/CommandPB.proto b/hoymiles_wifi/protobuf/CommandPB.proto new file mode 100644 index 0000000..4b77aa1 --- /dev/null +++ b/hoymiles_wifi/protobuf/CommandPB.proto @@ -0,0 +1,78 @@ +syntax = "proto3"; + +message CommandResDTO { + int32 time = 1; // Timestamp of the response + int32 action = 2; // Action code indicating the type of command + int32 dev_kind = 3; // Device kind information + int32 package_nub = 4; // Total number of packages expected + int32 package_now = 5; // Current package number + int64 tid = 6; // Transaction ID? + string data = 7; // Additional data payload + repeated string es_to_sn = 8; // List of ES serial numbers + repeated int64 mi_to_sn = 9; // List of MI serial numbers + int32 system_total_a = 10; // System total for category A + int32 system_total_b = 11; // System total for category B + int32 system_total_c = 12; // System total for category C + repeated int64 mi_sn_item_a = 13; // List of MI serial numbers for category A + repeated int64 mi_sn_item_b = 14; // List of MI serial numbers for category B + repeated int64 mi_sn_item_c = 15; // List of MI serial numbers for category C +} + +message CommandReqDTO { + string dtu_sn = 1; // Data Terminal Unit (DTU) serial number + int32 time = 2; // Timestamp of the request + int32 action = 3; // Action code indicating the type of command + int32 package_now = 4; // Current package number + int32 err_code = 5; // Error code (if any) + int64 tid = 6; // Transaction ID +} + +message ESOperatingStatusMO { + string es_sn = 1; // ES serial number + int32 progress_rate = 2; // Progress rate of the operation +} + +message MIOperatingStatusMO { + int64 mi_sn = 1; // MI serial number + int32 progress_rate = 2; // Progress rate of the operation +} + +message MIErrorStatusMO { + int64 mi_sn = 1; // MI serial number + int64 error_code = 2; // Error code associated with the MI +} + +message ESSucStatusMO { + string es_sn = 1; // ES serial number +} + +message ESErrorStatusMO { + string es_sn = 1; // ES serial number + int64 error_code = 2; // Error code associated with the ES +} + +message CommandStatusReqDTO { + string dtu_sn = 1; // Data Terminal Unit (DTU) serial number + int32 time = 2; // Timestamp of the request + int32 action = 3; // Action code indicating the type of command + int32 package_nub = 4; // Total number of packages expected + int32 package_now = 5; // Current package number + int64 tid = 6; // Transaction ID + repeated string es_sns_sucs = 7; // List of ES serial numbers with successful execution + repeated int64 mi_sns_sucs = 8; // List of MI serial numbers with successful execution + repeated string es_sns_failds = 9; // List of ES serial numbers with failed execution + repeated int64 mi_sns_failds = 10; // List of MI serial numbers with failed execution + repeated ESOperatingStatusMO es_mOperatingStatus = 11; // List of ES operating statuses + repeated MIOperatingStatusMO mi_mOperatingStatus = 12; // List of MI operating statuses + repeated MIErrorStatusMO mi_mErrorStatus = 13; // List of MI error statuses + repeated ESSucStatusMO es_mSucStatus = 14; // List of successful ES statuses + repeated ESErrorStatusMO es_mErrorStatus = 15; // List of error ES statuses +} + +message CommandStatusResDTO { + int32 time = 1; // Timestamp of the response + int32 action = 2; // Action code indicating the type of command + int32 package_now = 3; // Current package number + int64 tid = 4; // Transaction ID + int32 err_code = 5; // Error code (if any) +} diff --git a/hoymiles_wifi/protobuf/CommandPB_pb2.py b/hoymiles_wifi/protobuf/CommandPB_pb2.py new file mode 100644 index 0000000..3424881 --- /dev/null +++ b/hoymiles_wifi/protobuf/CommandPB_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: CommandPB.proto +# Protobuf Python Version: 4.25.0 +"""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\x0f\x43ommandPB.proto\"\xb2\x02\n\rCommandResDTO\x12\x0c\n\x04time\x18\x01 \x01(\x05\x12\x0e\n\x06\x61\x63tion\x18\x02 \x01(\x05\x12\x10\n\x08\x64\x65v_kind\x18\x03 \x01(\x05\x12\x13\n\x0bpackage_nub\x18\x04 \x01(\x05\x12\x13\n\x0bpackage_now\x18\x05 \x01(\x05\x12\x0b\n\x03tid\x18\x06 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x07 \x01(\t\x12\x10\n\x08\x65s_to_sn\x18\x08 \x03(\t\x12\x10\n\x08mi_to_sn\x18\t \x03(\x03\x12\x16\n\x0esystem_total_a\x18\n \x01(\x05\x12\x16\n\x0esystem_total_b\x18\x0b \x01(\x05\x12\x16\n\x0esystem_total_c\x18\x0c \x01(\x05\x12\x14\n\x0cmi_sn_item_a\x18\r \x03(\x03\x12\x14\n\x0cmi_sn_item_b\x18\x0e \x03(\x03\x12\x14\n\x0cmi_sn_item_c\x18\x0f \x03(\x03\"q\n\rCommandReqDTO\x12\x0e\n\x06\x64tu_sn\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x0e\n\x06\x61\x63tion\x18\x03 \x01(\x05\x12\x13\n\x0bpackage_now\x18\x04 \x01(\x05\x12\x10\n\x08\x65rr_code\x18\x05 \x01(\x05\x12\x0b\n\x03tid\x18\x06 \x01(\x03\";\n\x13\x45SOperatingStatusMO\x12\r\n\x05\x65s_sn\x18\x01 \x01(\t\x12\x15\n\rprogress_rate\x18\x02 \x01(\x05\";\n\x13MIOperatingStatusMO\x12\r\n\x05mi_sn\x18\x01 \x01(\x03\x12\x15\n\rprogress_rate\x18\x02 \x01(\x05\"4\n\x0fMIErrorStatusMO\x12\r\n\x05mi_sn\x18\x01 \x01(\x03\x12\x12\n\nerror_code\x18\x02 \x01(\x03\"\x1e\n\rESSucStatusMO\x12\r\n\x05\x65s_sn\x18\x01 \x01(\t\"4\n\x0f\x45SErrorStatusMO\x12\r\n\x05\x65s_sn\x18\x01 \x01(\t\x12\x12\n\nerror_code\x18\x02 \x01(\x03\"\xb5\x03\n\x13\x43ommandStatusReqDTO\x12\x0e\n\x06\x64tu_sn\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x0e\n\x06\x61\x63tion\x18\x03 \x01(\x05\x12\x13\n\x0bpackage_nub\x18\x04 \x01(\x05\x12\x13\n\x0bpackage_now\x18\x05 \x01(\x05\x12\x0b\n\x03tid\x18\x06 \x01(\x03\x12\x13\n\x0b\x65s_sns_sucs\x18\x07 \x03(\t\x12\x13\n\x0bmi_sns_sucs\x18\x08 \x03(\x03\x12\x15\n\res_sns_failds\x18\t \x03(\t\x12\x15\n\rmi_sns_failds\x18\n \x03(\x03\x12\x31\n\x13\x65s_mOperatingStatus\x18\x0b \x03(\x0b\x32\x14.ESOperatingStatusMO\x12\x31\n\x13mi_mOperatingStatus\x18\x0c \x03(\x0b\x32\x14.MIOperatingStatusMO\x12)\n\x0fmi_mErrorStatus\x18\r \x03(\x0b\x32\x10.MIErrorStatusMO\x12%\n\res_mSucStatus\x18\x0e \x03(\x0b\x32\x0e.ESSucStatusMO\x12)\n\x0f\x65s_mErrorStatus\x18\x0f \x03(\x0b\x32\x10.ESErrorStatusMO\"g\n\x13\x43ommandStatusResDTO\x12\x0c\n\x04time\x18\x01 \x01(\x05\x12\x0e\n\x06\x61\x63tion\x18\x02 \x01(\x05\x12\x13\n\x0bpackage_now\x18\x03 \x01(\x05\x12\x0b\n\x03tid\x18\x04 \x01(\x03\x12\x10\n\x08\x65rr_code\x18\x05 \x01(\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'CommandPB_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_COMMANDRESDTO']._serialized_start=20 + _globals['_COMMANDRESDTO']._serialized_end=326 + _globals['_COMMANDREQDTO']._serialized_start=328 + _globals['_COMMANDREQDTO']._serialized_end=441 + _globals['_ESOPERATINGSTATUSMO']._serialized_start=443 + _globals['_ESOPERATINGSTATUSMO']._serialized_end=502 + _globals['_MIOPERATINGSTATUSMO']._serialized_start=504 + _globals['_MIOPERATINGSTATUSMO']._serialized_end=563 + _globals['_MIERRORSTATUSMO']._serialized_start=565 + _globals['_MIERRORSTATUSMO']._serialized_end=617 + _globals['_ESSUCSTATUSMO']._serialized_start=619 + _globals['_ESSUCSTATUSMO']._serialized_end=649 + _globals['_ESERRORSTATUSMO']._serialized_start=651 + _globals['_ESERRORSTATUSMO']._serialized_end=703 + _globals['_COMMANDSTATUSREQDTO']._serialized_start=706 + _globals['_COMMANDSTATUSREQDTO']._serialized_end=1143 + _globals['_COMMANDSTATUSRESDTO']._serialized_start=1145 + _globals['_COMMANDSTATUSRESDTO']._serialized_end=1248 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/DevConfig.proto b/hoymiles_wifi/protobuf/DevConfig.proto new file mode 100644 index 0000000..1efe666 --- /dev/null +++ b/hoymiles_wifi/protobuf/DevConfig.proto @@ -0,0 +1,76 @@ +syntax = "proto3"; + +message DevConfigFetchResDTO { + int32 response_time = 1; // Timestamp of the response + int64 transaction_id = 2; // Transaction ID + string dtu_sn = 3; // DTU serial number + string dev_sn = 4; // Device serial number + int32 current_package = 5; // Current package number + int32 rule_type = 6; // Rule type +} + +message DevConfigFetchReqDTO { + int32 request_time = 1; // Timestamp of the request + int64 transaction_id = 2; // Transaction ID + int32 rule_id = 3; // Rule ID + string data = 4; // Data + int32 crc = 5; // CRC value + string dtu_sn = 6; // DTU serial number + string dev_sn = 7; // Device serial number + string cfg_data = 8; // Configuration data + int32 cfg_crc = 9; // Configuration CRC value + int32 total_packages = 10; // Total number of packages + int32 current_package = 11; // Current package number + int32 rule_type = 12; // Rule type +} + +message DevConfigPutResDTO { + int32 response_time = 1; // Timestamp of the response + int64 transaction_id = 2; // Transaction ID + int32 rule_id = 3; // Rule ID + string data = 4; // Data + int32 crc = 5; // CRC value + string dtu_sn = 6; // DTU serial number + string dev_sn = 7; // Device serial number + string cfg_data = 8; // Configuration data + int32 cfg_crc = 9; // Configuration CRC value + int32 total_packages = 10; // Total number of packages + int32 current_package = 11; // Current package number + repeated int64 mi_to_sn = 12; // List of MI (Meter Interface) serial numbers + int32 rule_type = 13; // Rule type +} + +message DevConfigPutReqDTO { + int32 request_time = 1; // Timestamp of the request + int64 transaction_id = 2; // Transaction ID + string dtu_sn = 3; // DTU serial number + string dev_sn = 4; // Device serial number + int32 status = 5; // Status indicator + int32 current_package = 6; // Current package number + repeated int64 mi_to_sn = 7; // List of MI (Meter Interface) serial numbers + int32 rule_type = 8; // Rule type +} + +message DevConfigReportReqDTO { + int32 request_time = 1; // Timestamp of the request + int64 transaction_id = 2; // Transaction ID + int32 rule_id = 3; // Rule ID + string data = 4; // Data + int32 crc = 5; // CRC value + string dtu_sn = 6; // DTU serial number + string dev_sn = 7; // Device serial number + string cfg_data = 8; // Configuration data + int32 cfg_crc = 9; // Configuration CRC value + int32 total_packages = 10; // Total number of packages + int32 current_package = 11; // Current package number + int32 rule_type = 12; // Rule type +} + +message DevConfigReportResDTO { + int32 response_time = 1; // Timestamp of the response + int64 transaction_id = 2; // Transaction ID + string dtu_sn = 3; // DTU serial number + string dev_sn = 4; // Device serial number + int32 current_package = 5; // Current package number + int32 rule_type = 6; // Rule type +} diff --git a/hoymiles_wifi/protobuf/DevConfig_pb2.py b/hoymiles_wifi/protobuf/DevConfig_pb2.py new file mode 100644 index 0000000..e8f82b4 --- /dev/null +++ b/hoymiles_wifi/protobuf/DevConfig_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: DevConfig.proto +# Protobuf Python Version: 4.25.0 +"""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\x0f\x44\x65vConfig.proto\"\x91\x01\n\x14\x44\x65vConfigFetchResDTO\x12\x15\n\rresponse_time\x18\x01 \x01(\x05\x12\x16\n\x0etransaction_id\x18\x02 \x01(\x03\x12\x0e\n\x06\x64tu_sn\x18\x03 \x01(\t\x12\x0e\n\x06\x64\x65v_sn\x18\x04 \x01(\t\x12\x17\n\x0f\x63urrent_package\x18\x05 \x01(\x05\x12\x11\n\trule_type\x18\x06 \x01(\x05\"\xf7\x01\n\x14\x44\x65vConfigFetchReqDTO\x12\x14\n\x0crequest_time\x18\x01 \x01(\x05\x12\x16\n\x0etransaction_id\x18\x02 \x01(\x03\x12\x0f\n\x07rule_id\x18\x03 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x0b\n\x03\x63rc\x18\x05 \x01(\x05\x12\x0e\n\x06\x64tu_sn\x18\x06 \x01(\t\x12\x0e\n\x06\x64\x65v_sn\x18\x07 \x01(\t\x12\x10\n\x08\x63\x66g_data\x18\x08 \x01(\t\x12\x0f\n\x07\x63\x66g_crc\x18\t \x01(\x05\x12\x16\n\x0etotal_packages\x18\n \x01(\x05\x12\x17\n\x0f\x63urrent_package\x18\x0b \x01(\x05\x12\x11\n\trule_type\x18\x0c \x01(\x05\"\x88\x02\n\x12\x44\x65vConfigPutResDTO\x12\x15\n\rresponse_time\x18\x01 \x01(\x05\x12\x16\n\x0etransaction_id\x18\x02 \x01(\x03\x12\x0f\n\x07rule_id\x18\x03 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x0b\n\x03\x63rc\x18\x05 \x01(\x05\x12\x0e\n\x06\x64tu_sn\x18\x06 \x01(\t\x12\x0e\n\x06\x64\x65v_sn\x18\x07 \x01(\t\x12\x10\n\x08\x63\x66g_data\x18\x08 \x01(\t\x12\x0f\n\x07\x63\x66g_crc\x18\t \x01(\x05\x12\x16\n\x0etotal_packages\x18\n \x01(\x05\x12\x17\n\x0f\x63urrent_package\x18\x0b \x01(\x05\x12\x10\n\x08mi_to_sn\x18\x0c \x03(\x03\x12\x11\n\trule_type\x18\r \x01(\x05\"\xb0\x01\n\x12\x44\x65vConfigPutReqDTO\x12\x14\n\x0crequest_time\x18\x01 \x01(\x05\x12\x16\n\x0etransaction_id\x18\x02 \x01(\x03\x12\x0e\n\x06\x64tu_sn\x18\x03 \x01(\t\x12\x0e\n\x06\x64\x65v_sn\x18\x04 \x01(\t\x12\x0e\n\x06status\x18\x05 \x01(\x05\x12\x17\n\x0f\x63urrent_package\x18\x06 \x01(\x05\x12\x10\n\x08mi_to_sn\x18\x07 \x03(\x03\x12\x11\n\trule_type\x18\x08 \x01(\x05\"\xf8\x01\n\x15\x44\x65vConfigReportReqDTO\x12\x14\n\x0crequest_time\x18\x01 \x01(\x05\x12\x16\n\x0etransaction_id\x18\x02 \x01(\x03\x12\x0f\n\x07rule_id\x18\x03 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x0b\n\x03\x63rc\x18\x05 \x01(\x05\x12\x0e\n\x06\x64tu_sn\x18\x06 \x01(\t\x12\x0e\n\x06\x64\x65v_sn\x18\x07 \x01(\t\x12\x10\n\x08\x63\x66g_data\x18\x08 \x01(\t\x12\x0f\n\x07\x63\x66g_crc\x18\t \x01(\x05\x12\x16\n\x0etotal_packages\x18\n \x01(\x05\x12\x17\n\x0f\x63urrent_package\x18\x0b \x01(\x05\x12\x11\n\trule_type\x18\x0c \x01(\x05\"\x92\x01\n\x15\x44\x65vConfigReportResDTO\x12\x15\n\rresponse_time\x18\x01 \x01(\x05\x12\x16\n\x0etransaction_id\x18\x02 \x01(\x03\x12\x0e\n\x06\x64tu_sn\x18\x03 \x01(\t\x12\x0e\n\x06\x64\x65v_sn\x18\x04 \x01(\t\x12\x17\n\x0f\x63urrent_package\x18\x05 \x01(\x05\x12\x11\n\trule_type\x18\x06 \x01(\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'DevConfig_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_DEVCONFIGFETCHRESDTO']._serialized_start=20 + _globals['_DEVCONFIGFETCHRESDTO']._serialized_end=165 + _globals['_DEVCONFIGFETCHREQDTO']._serialized_start=168 + _globals['_DEVCONFIGFETCHREQDTO']._serialized_end=415 + _globals['_DEVCONFIGPUTRESDTO']._serialized_start=418 + _globals['_DEVCONFIGPUTRESDTO']._serialized_end=682 + _globals['_DEVCONFIGPUTREQDTO']._serialized_start=685 + _globals['_DEVCONFIGPUTREQDTO']._serialized_end=861 + _globals['_DEVCONFIGREPORTREQDTO']._serialized_start=864 + _globals['_DEVCONFIGREPORTREQDTO']._serialized_end=1112 + _globals['_DEVCONFIGREPORTRESDTO']._serialized_start=1115 + _globals['_DEVCONFIGREPORTRESDTO']._serialized_end=1261 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/EventData.proto b/hoymiles_wifi/protobuf/EventData.proto new file mode 100644 index 0000000..38ee4c7 --- /dev/null +++ b/hoymiles_wifi/protobuf/EventData.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +message EventDataResDTO { + int32 offset = 1; // Offset in the response + int32 time = 2; // Timestamp of the response + string time_ymd_hms = 3; // Start timestamp in YYYY-MM-DD HH:MM:SS format +} + +message MIEvent { + int32 event_code = 1; // Event code + int32 event_status = 2; // Event status + int32 event_count = 3; // Event count + int32 pv_voltage = 4; // PV voltage + int32 grid_voltage = 5; // Grid voltage + int32 grid_frequency = 6; // Grid frequency + int32 grid_power = 7; // Grid power + int32 temperature = 8; // Temperature + int64 mi_id = 9; // Meter Interface (MI) ID + int32 start_timestamp = 10; // Start timestamp +} + +message EventDataReqDTO { + int32 offset = 1; // Offset in the request + int32 time = 2; // Timestamp of the request + repeated MIEvent mi_events = 3; // List of MI (Meter Interface) events +} diff --git a/hoymiles_wifi/protobuf/EventData_pb2.py b/hoymiles_wifi/protobuf/EventData_pb2.py new file mode 100644 index 0000000..70c0fd8 --- /dev/null +++ b/hoymiles_wifi/protobuf/EventData_pb2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: EventData.proto +# Protobuf Python Version: 4.25.0 +"""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\x0f\x45ventData.proto\"E\n\x0f\x45ventDataResDTO\x12\x0e\n\x06offset\x18\x01 \x01(\x05\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x14\n\x0ctime_ymd_hms\x18\x03 \x01(\t\"\xdb\x01\n\x07MIEvent\x12\x12\n\nevent_code\x18\x01 \x01(\x05\x12\x14\n\x0c\x65vent_status\x18\x02 \x01(\x05\x12\x13\n\x0b\x65vent_count\x18\x03 \x01(\x05\x12\x12\n\npv_voltage\x18\x04 \x01(\x05\x12\x14\n\x0cgrid_voltage\x18\x05 \x01(\x05\x12\x16\n\x0egrid_frequency\x18\x06 \x01(\x05\x12\x12\n\ngrid_power\x18\x07 \x01(\x05\x12\x13\n\x0btemperature\x18\x08 \x01(\x05\x12\r\n\x05mi_id\x18\t \x01(\x03\x12\x17\n\x0fstart_timestamp\x18\n \x01(\x05\"L\n\x0f\x45ventDataReqDTO\x12\x0e\n\x06offset\x18\x01 \x01(\x05\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x1b\n\tmi_events\x18\x03 \x03(\x0b\x32\x08.MIEventb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'EventData_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_EVENTDATARESDTO']._serialized_start=19 + _globals['_EVENTDATARESDTO']._serialized_end=88 + _globals['_MIEVENT']._serialized_start=91 + _globals['_MIEVENT']._serialized_end=310 + _globals['_EVENTDATAREQDTO']._serialized_start=312 + _globals['_EVENTDATAREQDTO']._serialized_end=388 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/GPSTData.proto b/hoymiles_wifi/protobuf/GPSTData.proto new file mode 100644 index 0000000..fccdb8f --- /dev/null +++ b/hoymiles_wifi/protobuf/GPSTData.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +message STValMO { + int32 code = 1; + int32 dflt_val = 2; + int32 dflt_tim = 3; + int32 rslt_val = 4; + int32 rslt_tim = 5; +} + +message GPSTValMO { + int64 pv_sn = 1; + int32 ver = 2; + int32 st = 3; + int32 gpf = 4; + int32 gpf_ver = 5; + STValMO hv1_stval = 6; + STValMO lv1_stval = 7; + STValMO hv2_stval = 8; + STValMO lv2_stval = 9; + STValMO hf1_stval = 10; + STValMO lf1_stval = 11; + STValMO hf2_stval = 12; + STValMO lf2_stval = 13; +} + +message GPSTReqDTO { + string dtu_sn = 1; + int32 time = 2; + int32 package_nub = 3; + int32 package_now = 4; + repeated GPSTValMO mGPSTInfo = 5; +} + +message GPSTResDTO { + string ymd_hms = 1; + int32 offset = 2; + int32 package_now = 3; + int32 err_code = 4; + int32 time = 5; +} diff --git a/hoymiles_wifi/protobuf/GPSTData_pb2.py b/hoymiles_wifi/protobuf/GPSTData_pb2.py new file mode 100644 index 0000000..1d690e9 --- /dev/null +++ b/hoymiles_wifi/protobuf/GPSTData_pb2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: GPSTData.proto +# Protobuf Python Version: 4.25.0 +"""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\x0eGPSTData.proto\"_\n\x07STValMO\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x10\n\x08\x64\x66lt_val\x18\x02 \x01(\x05\x12\x10\n\x08\x64\x66lt_tim\x18\x03 \x01(\x05\x12\x10\n\x08rslt_val\x18\x04 \x01(\x05\x12\x10\n\x08rslt_tim\x18\x05 \x01(\x05\"\xb9\x02\n\tGPSTValMO\x12\r\n\x05pv_sn\x18\x01 \x01(\x03\x12\x0b\n\x03ver\x18\x02 \x01(\x05\x12\n\n\x02st\x18\x03 \x01(\x05\x12\x0b\n\x03gpf\x18\x04 \x01(\x05\x12\x0f\n\x07gpf_ver\x18\x05 \x01(\x05\x12\x1b\n\thv1_stval\x18\x06 \x01(\x0b\x32\x08.STValMO\x12\x1b\n\tlv1_stval\x18\x07 \x01(\x0b\x32\x08.STValMO\x12\x1b\n\thv2_stval\x18\x08 \x01(\x0b\x32\x08.STValMO\x12\x1b\n\tlv2_stval\x18\t \x01(\x0b\x32\x08.STValMO\x12\x1b\n\thf1_stval\x18\n \x01(\x0b\x32\x08.STValMO\x12\x1b\n\tlf1_stval\x18\x0b \x01(\x0b\x32\x08.STValMO\x12\x1b\n\thf2_stval\x18\x0c \x01(\x0b\x32\x08.STValMO\x12\x1b\n\tlf2_stval\x18\r \x01(\x0b\x32\x08.STValMO\"s\n\nGPSTReqDTO\x12\x0e\n\x06\x64tu_sn\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x13\n\x0bpackage_nub\x18\x03 \x01(\x05\x12\x13\n\x0bpackage_now\x18\x04 \x01(\x05\x12\x1d\n\tmGPSTInfo\x18\x05 \x03(\x0b\x32\n.GPSTValMO\"b\n\nGPSTResDTO\x12\x0f\n\x07ymd_hms\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x02 \x01(\x05\x12\x13\n\x0bpackage_now\x18\x03 \x01(\x05\x12\x10\n\x08\x65rr_code\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\x05 \x01(\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'GPSTData_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_STVALMO']._serialized_start=18 + _globals['_STVALMO']._serialized_end=113 + _globals['_GPSTVALMO']._serialized_start=116 + _globals['_GPSTVALMO']._serialized_end=429 + _globals['_GPSTREQDTO']._serialized_start=431 + _globals['_GPSTREQDTO']._serialized_end=546 + _globals['_GPSTRESDTO']._serialized_start=548 + _globals['_GPSTRESDTO']._serialized_end=646 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/GetConfig.proto b/hoymiles_wifi/protobuf/GetConfig.proto new file mode 100644 index 0000000..295febe --- /dev/null +++ b/hoymiles_wifi/protobuf/GetConfig.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +message GetConfigResDTO { + int32 offset = 1; // Offset in the response + uint32 time = 2; // Timestamp of the response +} + +message GetConfigReqDTO { + int32 request_offset = 1; // Offset in the request + uint32 request_time = 2; // Timestamp of the request + int32 lock_password = 3; // Lock password + int32 lock_time = 4; // Lock time + int32 limit_power_mypower = 5; // Limit power mypower + int32 zero_export_433_addr = 6; // Zero export 433 address + int32 zero_export_enable = 7; // Zero export enable + int32 netmode_select = 8; // Netmode select + int32 channel_select = 9; // Channel select + int32 server_send_time = 10; // Server send time + int32 wifi_rssi = 11; // Wifi RSSI + int32 serverport = 12; // Server port + string apn_set = 13; // Access Point Name (APN) set + string meter_kind = 14; // Meter kind + string meter_interface = 15; // Meter interface + string wifi_ssid = 16; // Wifi SSID + string wifi_password = 17; // Wifi password + string server_domain_name = 18; // Server domain name + int32 inv_type = 19; // Inverter type + string dtu_sn = 20; // DTU serial number + int32 access_model = 21; // Access model + int32 mac_0 = 22; // MAC address byte 0 + int32 mac_1 = 23; // MAC address byte 1 + int32 mac_2 = 24; // MAC address byte 2 + int32 mac_3 = 25; // MAC address byte 3 + int32 dhcp_switch = 26; // DHCP switch + int32 ip_addr_0 = 27; // IP address byte 0 + int32 ip_addr_1 = 28; // IP address byte 1 + int32 ip_addr_2 = 29; // IP address byte 2 + int32 ip_addr_3 = 30; // IP address byte 3 + int32 subnet_mask_0 = 31; // Subnet mask byte 0 + int32 subnet_mask_1 = 32; // Subnet mask byte 1 + int32 subnet_mask_2 = 33; // Subnet mask byte 2 + int32 subnet_mask_3 = 34; // Subnet mask byte 3 + int32 default_gateway_0 = 35; // Default gateway byte 0 + int32 default_gateway_1 = 36; // Default gateway byte 1 + int32 default_gateway_2 = 37; // Default gateway byte 2 + int32 default_gateway_3 = 38; // Default gateway byte 3 + string ka_nub = 39; // KA number + string apn_name = 40; // APN name + string apn_password = 41; // APN password + int32 sub1g_sweep_switch = 42; // Sub1G sweep switch + int32 sub1g_work_channel = 43; // Sub1G work channel + int32 cable_dns_0 = 44; // Cable DNS byte 0 + int32 cable_dns_1 = 45; // Cable DNS byte 1 + int32 cable_dns_2 = 46; // Cable DNS byte 2 + int32 cable_dns_3 = 47; // Cable DNS byte 3 + int32 wifi_ip_addr_0 = 48; // Wifi IP address byte 0 + int32 wifi_ip_addr_1 = 49; // Wifi IP address byte 1 + int32 wifi_ip_addr_2 = 50; // Wifi IP address byte 2 + int32 wifi_ip_addr_3 = 51; // Wifi IP address byte 3 + int32 mac_4 = 52; // MAC address byte 4 + int32 mac_5 = 53; // MAC address byte 5 + int32 wifi_mac_0 = 54; // Wifi MAC address byte 0 + int32 wifi_mac_1 = 55; // Wifi MAC address byte 1 + int32 wifi_mac_2 = 56; // Wifi MAC address byte 2 + int32 wifi_mac_3 = 57; // Wifi MAC address byte 3 + int32 wifi_mac_4 = 58; // Wifi MAC address byte 4 + int32 wifi_mac_5 = 59; // Wifi MAC address byte 5 + string gprs_imei = 60; // GPRS IMEI + string dtu_ap_ssid = 61; // DTU Access Point (AP) SSID + string dtu_ap_pass = 62; // DTU AP password +} diff --git a/hoymiles_wifi/protobuf/GetConfig_pb2.py b/hoymiles_wifi/protobuf/GetConfig_pb2.py new file mode 100644 index 0000000..6da7965 --- /dev/null +++ b/hoymiles_wifi/protobuf/GetConfig_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: GetConfig.proto +# Protobuf Python Version: 4.25.0 +"""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\x0fGetConfig.proto\"/\n\x0fGetConfigResDTO\x12\x0e\n\x06offset\x18\x01 \x01(\x05\x12\x0c\n\x04time\x18\x02 \x01(\r\"\xc9\n\n\x0fGetConfigReqDTO\x12\x16\n\x0erequest_offset\x18\x01 \x01(\x05\x12\x14\n\x0crequest_time\x18\x02 \x01(\r\x12\x15\n\rlock_password\x18\x03 \x01(\x05\x12\x11\n\tlock_time\x18\x04 \x01(\x05\x12\x1b\n\x13limit_power_mypower\x18\x05 \x01(\x05\x12\x1c\n\x14zero_export_433_addr\x18\x06 \x01(\x05\x12\x1a\n\x12zero_export_enable\x18\x07 \x01(\x05\x12\x16\n\x0enetmode_select\x18\x08 \x01(\x05\x12\x16\n\x0e\x63hannel_select\x18\t \x01(\x05\x12\x18\n\x10server_send_time\x18\n \x01(\x05\x12\x11\n\twifi_rssi\x18\x0b \x01(\x05\x12\x12\n\nserverport\x18\x0c \x01(\x05\x12\x0f\n\x07\x61pn_set\x18\r \x01(\t\x12\x12\n\nmeter_kind\x18\x0e \x01(\t\x12\x17\n\x0fmeter_interface\x18\x0f \x01(\t\x12\x11\n\twifi_ssid\x18\x10 \x01(\t\x12\x15\n\rwifi_password\x18\x11 \x01(\t\x12\x1a\n\x12server_domain_name\x18\x12 \x01(\t\x12\x10\n\x08inv_type\x18\x13 \x01(\x05\x12\x0e\n\x06\x64tu_sn\x18\x14 \x01(\t\x12\x14\n\x0c\x61\x63\x63\x65ss_model\x18\x15 \x01(\x05\x12\r\n\x05mac_0\x18\x16 \x01(\x05\x12\r\n\x05mac_1\x18\x17 \x01(\x05\x12\r\n\x05mac_2\x18\x18 \x01(\x05\x12\r\n\x05mac_3\x18\x19 \x01(\x05\x12\x13\n\x0b\x64hcp_switch\x18\x1a \x01(\x05\x12\x11\n\tip_addr_0\x18\x1b \x01(\x05\x12\x11\n\tip_addr_1\x18\x1c \x01(\x05\x12\x11\n\tip_addr_2\x18\x1d \x01(\x05\x12\x11\n\tip_addr_3\x18\x1e \x01(\x05\x12\x15\n\rsubnet_mask_0\x18\x1f \x01(\x05\x12\x15\n\rsubnet_mask_1\x18 \x01(\x05\x12\x15\n\rsubnet_mask_2\x18! \x01(\x05\x12\x15\n\rsubnet_mask_3\x18\" \x01(\x05\x12\x19\n\x11\x64\x65\x66\x61ult_gateway_0\x18# \x01(\x05\x12\x19\n\x11\x64\x65\x66\x61ult_gateway_1\x18$ \x01(\x05\x12\x19\n\x11\x64\x65\x66\x61ult_gateway_2\x18% \x01(\x05\x12\x19\n\x11\x64\x65\x66\x61ult_gateway_3\x18& \x01(\x05\x12\x0e\n\x06ka_nub\x18\' \x01(\t\x12\x10\n\x08\x61pn_name\x18( \x01(\t\x12\x14\n\x0c\x61pn_password\x18) \x01(\t\x12\x1a\n\x12sub1g_sweep_switch\x18* \x01(\x05\x12\x1a\n\x12sub1g_work_channel\x18+ \x01(\x05\x12\x13\n\x0b\x63\x61\x62le_dns_0\x18, \x01(\x05\x12\x13\n\x0b\x63\x61\x62le_dns_1\x18- \x01(\x05\x12\x13\n\x0b\x63\x61\x62le_dns_2\x18. \x01(\x05\x12\x13\n\x0b\x63\x61\x62le_dns_3\x18/ \x01(\x05\x12\x16\n\x0ewifi_ip_addr_0\x18\x30 \x01(\x05\x12\x16\n\x0ewifi_ip_addr_1\x18\x31 \x01(\x05\x12\x16\n\x0ewifi_ip_addr_2\x18\x32 \x01(\x05\x12\x16\n\x0ewifi_ip_addr_3\x18\x33 \x01(\x05\x12\r\n\x05mac_4\x18\x34 \x01(\x05\x12\r\n\x05mac_5\x18\x35 \x01(\x05\x12\x12\n\nwifi_mac_0\x18\x36 \x01(\x05\x12\x12\n\nwifi_mac_1\x18\x37 \x01(\x05\x12\x12\n\nwifi_mac_2\x18\x38 \x01(\x05\x12\x12\n\nwifi_mac_3\x18\x39 \x01(\x05\x12\x12\n\nwifi_mac_4\x18: \x01(\x05\x12\x12\n\nwifi_mac_5\x18; \x01(\x05\x12\x11\n\tgprs_imei\x18< \x01(\t\x12\x13\n\x0b\x64tu_ap_ssid\x18= \x01(\t\x12\x13\n\x0b\x64tu_ap_pass\x18> \x01(\tb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'GetConfig_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_GETCONFIGRESDTO']._serialized_start=19 + _globals['_GETCONFIGRESDTO']._serialized_end=66 + _globals['_GETCONFIGREQDTO']._serialized_start=69 + _globals['_GETCONFIGREQDTO']._serialized_end=1422 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/InfomationData.proto b/hoymiles_wifi/protobuf/InfomationData.proto new file mode 100644 index 0000000..e99a8f2 --- /dev/null +++ b/hoymiles_wifi/protobuf/InfomationData.proto @@ -0,0 +1,83 @@ +syntax = "proto3"; + +message DtuInfoMO { + int32 device_kind = 1; + int32 dtu_sw = 2; + int32 dtu_hw = 3; + int32 dtu_step_time = 4; + int32 dtu_rf_hw = 5; + int32 dtu_rf_sw = 6; + int32 access_model = 7; + string gprs_vsn = 8; + string wifi_vsn = 9; + string ka_nub = 10; + int32 dtu_rule_id = 11; + int32 dtu_error_code = 12; + int32 grid_type = 13; + int32 zero_export_switch= 14; + int32 surplus_power_a = 15; + int32 surplus_power_b = 16; + int32 surplus_power_c = 17; + int32 zero_export_control= 18; + int32 phase_balance_switch = 19; + int32 tolerance_between_phases = 20; +} + +message MeterInfoMO { + int32 device_kind = 1; + int64 meter_sn = 2; + int32 meter_model = 3; + int32 meter_ct = 4; + int32 com_way = 5; + int32 access_mode = 6; +} + +message RpInfoMO { + int32 device_kind = 1; + int64 rp_sn = 2; + int32 rp_sw = 3; + int32 rp_hw = 4; + int32 rp_rule_id = 5; +} + +message PvInfoMO { + int32 device_kind = 1; + int64 pv_sn = 2; + int32 pv_usfw = 3; + int32 pv_sw = 4; + int32 pv_hw_pn = 5; + int32 pv_hw = 6; + int32 pv_gpf_code = 7; + int32 pv_gpf = 8; + int32 pv_rf_hw = 9; + int32 pv_rf_sw = 10; + int32 mi_rule_id = 11; +} + +message FeatureMO { + int32 key = 1; + string value = 2; +} + +message InfoDataReqDTO { + string dtu_sn = 1; + int32 time = 2; + int32 device_nub = 3; + int32 pv_nub = 4; + int32 package_nub = 5; + int32 package_now = 6; + int32 channel = 7; + DtuInfoMO mDtuInfo = 8; + repeated MeterInfoMO mMeterInfo = 9; + repeated RpInfoMO mRpInfo = 10; + repeated PvInfoMO mpvInfo = 11; + repeated FeatureMO m_feature = 12; +} + +message InfoDataResDTO { + string time_ymd_hms = 1; + int32 offset = 2; + int32 package_now = 3; + int32 error_code = 4; + int32 time = 5; +} diff --git a/hoymiles_wifi/protobuf/InfomationData_pb2.py b/hoymiles_wifi/protobuf/InfomationData_pb2.py new file mode 100644 index 0000000..4ce1775 --- /dev/null +++ b/hoymiles_wifi/protobuf/InfomationData_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: InfomationData.proto +# Protobuf Python Version: 4.25.0 +"""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\x14InfomationData.proto\"\xcb\x03\n\tDtuInfoMO\x12\x13\n\x0b\x64\x65vice_kind\x18\x01 \x01(\x05\x12\x0e\n\x06\x64tu_sw\x18\x02 \x01(\x05\x12\x0e\n\x06\x64tu_hw\x18\x03 \x01(\x05\x12\x15\n\rdtu_step_time\x18\x04 \x01(\x05\x12\x11\n\tdtu_rf_hw\x18\x05 \x01(\x05\x12\x11\n\tdtu_rf_sw\x18\x06 \x01(\x05\x12\x14\n\x0c\x61\x63\x63\x65ss_model\x18\x07 \x01(\x05\x12\x10\n\x08gprs_vsn\x18\x08 \x01(\t\x12\x10\n\x08wifi_vsn\x18\t \x01(\t\x12\x0e\n\x06ka_nub\x18\n \x01(\t\x12\x13\n\x0b\x64tu_rule_id\x18\x0b \x01(\x05\x12\x16\n\x0e\x64tu_error_code\x18\x0c \x01(\x05\x12\x11\n\tgrid_type\x18\r \x01(\x05\x12\x1a\n\x12zero_export_switch\x18\x0e \x01(\x05\x12\x17\n\x0fsurplus_power_a\x18\x0f \x01(\x05\x12\x17\n\x0fsurplus_power_b\x18\x10 \x01(\x05\x12\x17\n\x0fsurplus_power_c\x18\x11 \x01(\x05\x12\x1b\n\x13zero_export_control\x18\x12 \x01(\x05\x12\x1c\n\x14phase_balance_switch\x18\x13 \x01(\x05\x12 \n\x18tolerance_between_phases\x18\x14 \x01(\x05\"\x81\x01\n\x0bMeterInfoMO\x12\x13\n\x0b\x64\x65vice_kind\x18\x01 \x01(\x05\x12\x10\n\x08meter_sn\x18\x02 \x01(\x03\x12\x13\n\x0bmeter_model\x18\x03 \x01(\x05\x12\x10\n\x08meter_ct\x18\x04 \x01(\x05\x12\x0f\n\x07\x63om_way\x18\x05 \x01(\x05\x12\x13\n\x0b\x61\x63\x63\x65ss_mode\x18\x06 \x01(\x05\"`\n\x08RpInfoMO\x12\x13\n\x0b\x64\x65vice_kind\x18\x01 \x01(\x05\x12\r\n\x05rp_sn\x18\x02 \x01(\x03\x12\r\n\x05rp_sw\x18\x03 \x01(\x05\x12\r\n\x05rp_hw\x18\x04 \x01(\x05\x12\x12\n\nrp_rule_id\x18\x05 \x01(\x05\"\xcc\x01\n\x08PvInfoMO\x12\x13\n\x0b\x64\x65vice_kind\x18\x01 \x01(\x05\x12\r\n\x05pv_sn\x18\x02 \x01(\x03\x12\x0f\n\x07pv_usfw\x18\x03 \x01(\x05\x12\r\n\x05pv_sw\x18\x04 \x01(\x05\x12\x10\n\x08pv_hw_pn\x18\x05 \x01(\x05\x12\r\n\x05pv_hw\x18\x06 \x01(\x05\x12\x13\n\x0bpv_gpf_code\x18\x07 \x01(\x05\x12\x0e\n\x06pv_gpf\x18\x08 \x01(\x05\x12\x10\n\x08pv_rf_hw\x18\t \x01(\x05\x12\x10\n\x08pv_rf_sw\x18\n \x01(\x05\x12\x12\n\nmi_rule_id\x18\x0b \x01(\x05\"\'\n\tFeatureMO\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\t\"\xa4\x02\n\x0eInfoDataReqDTO\x12\x0e\n\x06\x64tu_sn\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x12\n\ndevice_nub\x18\x03 \x01(\x05\x12\x0e\n\x06pv_nub\x18\x04 \x01(\x05\x12\x13\n\x0bpackage_nub\x18\x05 \x01(\x05\x12\x13\n\x0bpackage_now\x18\x06 \x01(\x05\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\x05\x12\x1c\n\x08mDtuInfo\x18\x08 \x01(\x0b\x32\n.DtuInfoMO\x12 \n\nmMeterInfo\x18\t \x03(\x0b\x32\x0c.MeterInfoMO\x12\x1a\n\x07mRpInfo\x18\n \x03(\x0b\x32\t.RpInfoMO\x12\x1a\n\x07mpvInfo\x18\x0b \x03(\x0b\x32\t.PvInfoMO\x12\x1d\n\tm_feature\x18\x0c \x03(\x0b\x32\n.FeatureMO\"m\n\x0eInfoDataResDTO\x12\x14\n\x0ctime_ymd_hms\x18\x01 \x01(\t\x12\x0e\n\x06offset\x18\x02 \x01(\x05\x12\x13\n\x0bpackage_now\x18\x03 \x01(\x05\x12\x12\n\nerror_code\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\x05 \x01(\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'InfomationData_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_DTUINFOMO']._serialized_start=25 + _globals['_DTUINFOMO']._serialized_end=484 + _globals['_METERINFOMO']._serialized_start=487 + _globals['_METERINFOMO']._serialized_end=616 + _globals['_RPINFOMO']._serialized_start=618 + _globals['_RPINFOMO']._serialized_end=714 + _globals['_PVINFOMO']._serialized_start=717 + _globals['_PVINFOMO']._serialized_end=921 + _globals['_FEATUREMO']._serialized_start=923 + _globals['_FEATUREMO']._serialized_end=962 + _globals['_INFODATAREQDTO']._serialized_start=965 + _globals['_INFODATAREQDTO']._serialized_end=1257 + _globals['_INFODATARESDTO']._serialized_start=1259 + _globals['_INFODATARESDTO']._serialized_end=1368 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/NetworkInfo.proto b/hoymiles_wifi/protobuf/NetworkInfo.proto new file mode 100644 index 0000000..bba6980 --- /dev/null +++ b/hoymiles_wifi/protobuf/NetworkInfo.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +message NetworkInfoReqDTO { + string dtu_sn = 1; // DTU serial number + uint32 time = 2; // Timestamp of the request + int32 net_set_mod = 3; // Network setting mode + int32 net_set_time = 4; // Network setting time + int32 net_set_state = 5; // Network setting state + int32 net_work_mod = 6; // Network working mode + int32 net_work_time = 7; // Network working time + int32 csq = 8; // Carrier Signal Quality (CSQ) + int32 net_work_state = 9; // Network working state + int32 ap_set_state = 10; // Access Point (AP) setting state +} + +message NetworkInfoResDTO { + int32 offset = 1; // Offset value for response + uint32 time = 2; // Timestamp of the response +} diff --git a/hoymiles_wifi/protobuf/NetworkInfo_pb2.py b/hoymiles_wifi/protobuf/NetworkInfo_pb2.py new file mode 100644 index 0000000..5b3a1e0 --- /dev/null +++ b/hoymiles_wifi/protobuf/NetworkInfo_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: NetworkInfo.proto +# Protobuf Python Version: 4.25.0 +"""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\x11NetworkInfo.proto\"\xdb\x01\n\x11NetworkInfoReqDTO\x12\x0e\n\x06\x64tu_sn\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\r\x12\x13\n\x0bnet_set_mod\x18\x03 \x01(\x05\x12\x14\n\x0cnet_set_time\x18\x04 \x01(\x05\x12\x15\n\rnet_set_state\x18\x05 \x01(\x05\x12\x14\n\x0cnet_work_mod\x18\x06 \x01(\x05\x12\x15\n\rnet_work_time\x18\x07 \x01(\x05\x12\x0b\n\x03\x63sq\x18\x08 \x01(\x05\x12\x16\n\x0enet_work_state\x18\t \x01(\x05\x12\x14\n\x0c\x61p_set_state\x18\n \x01(\x05\"1\n\x11NetworkInfoResDTO\x12\x0e\n\x06offset\x18\x01 \x01(\x05\x12\x0c\n\x04time\x18\x02 \x01(\rb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'NetworkInfo_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_NETWORKINFOREQDTO']._serialized_start=22 + _globals['_NETWORKINFOREQDTO']._serialized_end=241 + _globals['_NETWORKINFORESDTO']._serialized_start=243 + _globals['_NETWORKINFORESDTO']._serialized_end=292 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/RealData.proto b/hoymiles_wifi/protobuf/RealData.proto new file mode 100644 index 0000000..89c41eb --- /dev/null +++ b/hoymiles_wifi/protobuf/RealData.proto @@ -0,0 +1,78 @@ +syntax = "proto3"; + +message MeterDataMO { + int32 device_kind = 1; // Device kind identifier + int64 meter_sn = 2; // Meter serial number + int32 meter_total_power = 3; // Total power by the meter (Watts) + int32 meter_phase_A_power = 4; // Power in phase A (Watts) + int32 meter_phase_B_power = 5; // Power in phase B (Watts) + int32 meter_phase_C_power = 6; // Power in phase C (Watts) + int32 meter_factor = 7; // Meter factor + int32 meter_total_energy = + 8; // Total energy produced by the meter (Watt-hours) + int32 meter_phase_A_energy = 9; // Energy produced in phase A (Watt-hours) + int32 meter_phase_B_energy = 10; // Energy produced in phase B (Watt-hours) + int32 meter_phase_C_energy = 11; // Energy produced in phase C (Watt-hours) + int32 meter_total_consumed = 12; // Total energy consumed (Watt-hours) + int32 meter_phase_A_consumed = 13; // Energy consumed in phase A (Watt-hours) + int32 meter_phase_B_consumed = 14; // Energy consumed in phase B (Watt-hours) + int32 meter_phase_C_consumed = 15; // Energy consumed in phase C (Watt-hours) + int32 meter_fault = 16; // Meter fault code +} + +message RpDataMO { + int64 rp_sn = 1; // RP serial number + int32 rp_signal = 2; // RP signal strength + int32 rp_channel = 3; // RP channel number + int32 rp_link_nub = 4; // RP link number + int32 rp_link_status = 5; // RP link status +} + +message PvDataMO { + int64 pv_sn = 1; // PV serial number + int32 pv_port = 2; // PV port number + int32 pv_vol = 3; // PV voltage (Volts) + int32 pv_cur = 4; // PV current (Amperes) + int32 pv_power = 5; // PV power (Watts) + int32 pv_energy_total = 6; // Total energy generated by PV (Watt-hours) + int32 grid_vol = 7; // Grid voltage (Volts) + int32 grid_vol_max = 8; // Maximum grid voltage (Volts) + int32 grid_freq = 9; // Grid frequency (Hertz) + int32 grid_p = 10; // Grid active power (Watts) + int32 grid_q = 11; // Grid reactive power (VAR) + int32 grid_i = 12; // Grid current (Amperes) + int32 grid_pf = 13; // Grid power factor + int32 pv_temp = 14; // PV temperature + int32 pv_run_status = 15; // PV running status + int32 pv_fault_num = 16; // PV fault number + int32 pv_fault_cnt = 17; // PV fault count + int32 pv_warning_cnt = 18; // PV warning count + int32 pv_link_status = 19; // PV link status + int32 pv_send_power = 20; // PV send power (Watts) + int32 pv_receive_power = 21; // PV received power (Watts) + int32 pv_time = 22; // PV time + int32 pv_energy = 23; // PV energy (Watt-hours) + int32 mi_signal = 24; // Modulation index signal +} + +message RealDataReqDTO { + string dtu_sn = 1; // DTU serial number + int32 timestamp = 2; // Timestamp of the data + int32 device_number = 3; // Device number + int32 pv_number = 4; // PV number + int32 package_number = 5; // Package number + int32 current_package = 6; // Current package number + int32 csq = 7; // Carrier Signal Quality (CSQ) + repeated MeterDataMO meter_data = 8; // Meter data array + repeated RpDataMO rp_data = 9; // RpDataMO data array + repeated PvDataMO pv_data = 10; // PvDataMO data array + int32 version = 11; // Version number +} + +message RealDataResDTO { + string time_ymd_hms = 1; // Timestamp in the format YMD_HMS + int32 package_now = 2; // Package now + int32 error_code = 3; // Error code indicator + int32 offset = 4; // Offset value + int32 time = 5; // Timestamp value +} diff --git a/hoymiles_wifi/protobuf/RealDataNew.proto b/hoymiles_wifi/protobuf/RealDataNew.proto new file mode 100644 index 0000000..69523e1 --- /dev/null +++ b/hoymiles_wifi/protobuf/RealDataNew.proto @@ -0,0 +1,123 @@ +syntax = "proto3"; + +message MeterMO { + int32 device_type = 1; // Device type identifier + int64 serial_number = 2; // Serial number of the meter + int32 phase_total_power = 3; // Total power across all phases (Watts) + int32 phase_A_power = 4; // Power in phase A (Watts) + int32 phase_B_power = 5; // Power in phase B (Watts) + int32 phase_C_power = 6; // Power in phase C (Watts) + int32 power_factor_total = 7; // Total power factor + int32 energy_total_power = 8; // Total energy generated (Watt-hours) + int32 energy_phase_A = 9; // Energy generated in phase A (Watt-hours) + int32 energy_phase_B = 10; // Energy generated in phase B (Watt-hours) + int32 energy_phase_C = 11; // Energy generated in phase C (Watt-hours) + int32 energy_total_consumed = 12; // Total energy consumed (Watt-hours) + int32 energy_phase_A_consumed = 13; // Energy consumed in phase A (Watt-hours) + int32 energy_phase_B_consumed = 14; // Energy consumed in phase B (Watt-hours) + int32 energy_phase_C_consumed = 15; // Energy consumed in phase C (Watt-hours) + int32 fault_code = 16; // Fault code indicator + int32 voltage_phase_A = 17; // Voltage in phase A (Volts) + int32 voltage_phase_B = 18; // Voltage in phase B (Volts) + int32 voltage_phase_C = 19; // Voltage in phase C (Volts) + int32 current_phase_A = 20; // Current in phase A (Amperes) + int32 current_phase_B = 21; // Current in phase B (Amperes) + int32 current_phase_C = 22; // Current in phase C (Amperes) + int32 power_factor_phase_A = 23; // Power factor in phase A + int32 power_factor_phase_B = 24; // Power factor in phase B + int32 power_factor_phase_C = 25; // Power factor in phase C +} + +message RpMO { + int64 serial_number = 1; // Serial number of the device + int32 signature = 2; // Signature value + int32 channel = 3; // Channel number + int32 pv_number = 4; // Photovoltaic (PV) number + int32 link_status = 5; // Link status indicator +} + +message RSDMO { + int64 serial_number = 1; // Serial number of the device + int32 firmware_version = 2; // Firmware version + int32 voltage = 3; // Voltage value + int32 power = 4; // Power value + int32 temperature = 5; // Temperature value + int32 warning_number = 6; // Warning number + int32 crc_checksum = 7; // CRC checksum + int32 link_status = 8; // Link status indicator +} + +message SGSMO { + int64 serial_number = 1; // Serial number of the device + int32 firmware_version = 2; // Firmware version + int32 voltage = 3; // Grid voltage (Volts) + int32 frequency = 4; // Grid frequency (Hertz) + int32 active_power = 5; // Active power (Watts) + int32 reactive_power = 6; // Reactive power (VAR) + int32 current = 7; // Current (Amperes) + int32 power_factor = 8; // Power factor + int32 temperature = 9; // Temperature value + int32 warning_number = 10; // Warning number + int32 crc_checksum = 11; // CRC checksum + int32 link_status = 12; // Link status indicator + int32 power_limit = 13; // Power limit (Watts) + int32 modulation_index_signal = 20; // Modulation index signal +} + +message TGSMO { + int64 serial_number = 1; // Serial number of the device + int32 firmware_version = 2; // Firmware version + int32 voltage_phase_A = 3; // Voltage in phase A (Volts) + int32 voltage_phase_B = 4; // Voltage in phase B (Volts) + int32 voltage_phase_C = 5; // Voltage in phase C (Volts) + int32 voltage_line_AB = 6; // Voltage between lines A and B (Volts) + int32 voltage_line_BC = 7; // Voltage between lines B and C (Volts) + int32 voltage_line_CA = 8; // Voltage between lines C and A (Volts) + int32 frequency = 9; // Frequency (Hertz) + int32 active_power = 10; // Active power (Watts) + int32 reactive_power = 11; // Reactive power (VAR) + int32 current_phase_A = 12; // Current in phase A (Amperes) + int32 current_phase_B = 13; // Current in phase B (Amperes) + int32 current_phase_C = 14; // Current in phase C (Amperes) + int32 power_factor = 15; // Power factor + int32 temperature = 16; // Temperature value + int32 warning_number = 17; // Warning number + int32 crc_checksum = 18; // CRC checksum + int32 link_status = 19; // Link status indicator + int32 modulation_index_signal = 20; // Modulation index signal +} + +message PvMO { + int64 serial_number = 1; // Serial number of the device + int32 port_number = 2; // Port number + int32 voltage = 3; // Voltage (Volts) + int32 current = 4; // Current (Amperes) + int32 power = 5; // Power (Watts) + int32 energy_total = 6; // Total energy generated (Watt-hours) + int32 energy_daily = 7; // Daily energy generated (Watt-hours) + int32 error_code = 8; // Error code indicator +} + +message RealDataNewReqDTO { + string device_serial_number = 1; // Serial number of the device + int32 timestamp = 2; // Timestamp of the data + int32 active_power = 3; // Active power (Watts) + int32 cp = 4; // Control parameter? + int32 firmware_version = 5; // Firmware version + repeated MeterMO meter_data = 6; // Meter data array + repeated RpMO rp_data = 7; // RpMO data array + repeated RSDMO rsd_data = 8; // RSDMO data array + repeated SGSMO sgs_data = 9; // SGSMO data array + repeated TGSMO tgs_data = 10; // TGSMO data array + repeated PvMO pv_data = 11; // PvMO data array + uint64 dtu_power = 12; // Power of the DTU (Watts) + uint64 dtu_daily_energy = 13; // Daily energy of the DTU (Watt-hours) +} + +message RealDataNewResDTO { + string time_ymd_hms = 1; // Timestamp in the format YMD_HMS + int32 cp = 2; // Control parameter? + int32 error_code = 3; // Error code indicator + int32 offset = 4; // Offset value + int32 time = 5; // Timestamp value +} diff --git a/hoymiles_wifi/protobuf/RealDataNew_pb2.py b/hoymiles_wifi/protobuf/RealDataNew_pb2.py new file mode 100644 index 0000000..b491c3e --- /dev/null +++ b/hoymiles_wifi/protobuf/RealDataNew_pb2.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: RealDataNew.proto +# Protobuf Python Version: 4.25.0 +"""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\x11RealDataNew.proto\"\x9b\x05\n\x07MeterMO\x12\x13\n\x0b\x64\x65vice_type\x18\x01 \x01(\x05\x12\x15\n\rserial_number\x18\x02 \x01(\x03\x12\x19\n\x11phase_total_power\x18\x03 \x01(\x05\x12\x15\n\rphase_A_power\x18\x04 \x01(\x05\x12\x15\n\rphase_B_power\x18\x05 \x01(\x05\x12\x15\n\rphase_C_power\x18\x06 \x01(\x05\x12\x1a\n\x12power_factor_total\x18\x07 \x01(\x05\x12\x1a\n\x12\x65nergy_total_power\x18\x08 \x01(\x05\x12\x16\n\x0e\x65nergy_phase_A\x18\t \x01(\x05\x12\x16\n\x0e\x65nergy_phase_B\x18\n \x01(\x05\x12\x16\n\x0e\x65nergy_phase_C\x18\x0b \x01(\x05\x12\x1d\n\x15\x65nergy_total_consumed\x18\x0c \x01(\x05\x12\x1f\n\x17\x65nergy_phase_A_consumed\x18\r \x01(\x05\x12\x1f\n\x17\x65nergy_phase_B_consumed\x18\x0e \x01(\x05\x12\x1f\n\x17\x65nergy_phase_C_consumed\x18\x0f \x01(\x05\x12\x12\n\nfault_code\x18\x10 \x01(\x05\x12\x17\n\x0fvoltage_phase_A\x18\x11 \x01(\x05\x12\x17\n\x0fvoltage_phase_B\x18\x12 \x01(\x05\x12\x17\n\x0fvoltage_phase_C\x18\x13 \x01(\x05\x12\x17\n\x0f\x63urrent_phase_A\x18\x14 \x01(\x05\x12\x17\n\x0f\x63urrent_phase_B\x18\x15 \x01(\x05\x12\x17\n\x0f\x63urrent_phase_C\x18\x16 \x01(\x05\x12\x1c\n\x14power_factor_phase_A\x18\x17 \x01(\x05\x12\x1c\n\x14power_factor_phase_B\x18\x18 \x01(\x05\x12\x1c\n\x14power_factor_phase_C\x18\x19 \x01(\x05\"i\n\x04RpMO\x12\x15\n\rserial_number\x18\x01 \x01(\x03\x12\x11\n\tsignature\x18\x02 \x01(\x05\x12\x0f\n\x07\x63hannel\x18\x03 \x01(\x05\x12\x11\n\tpv_number\x18\x04 \x01(\x05\x12\x13\n\x0blink_status\x18\x05 \x01(\x05\"\xb0\x01\n\x05RSDMO\x12\x15\n\rserial_number\x18\x01 \x01(\x03\x12\x18\n\x10\x66irmware_version\x18\x02 \x01(\x05\x12\x0f\n\x07voltage\x18\x03 \x01(\x05\x12\r\n\x05power\x18\x04 \x01(\x05\x12\x13\n\x0btemperature\x18\x05 \x01(\x05\x12\x16\n\x0ewarning_number\x18\x06 \x01(\x05\x12\x14\n\x0c\x63rc_checksum\x18\x07 \x01(\x05\x12\x13\n\x0blink_status\x18\x08 \x01(\x05\"\xbf\x02\n\x05SGSMO\x12\x15\n\rserial_number\x18\x01 \x01(\x03\x12\x18\n\x10\x66irmware_version\x18\x02 \x01(\x05\x12\x0f\n\x07voltage\x18\x03 \x01(\x05\x12\x11\n\tfrequency\x18\x04 \x01(\x05\x12\x14\n\x0c\x61\x63tive_power\x18\x05 \x01(\x05\x12\x16\n\x0ereactive_power\x18\x06 \x01(\x05\x12\x0f\n\x07\x63urrent\x18\x07 \x01(\x05\x12\x14\n\x0cpower_factor\x18\x08 \x01(\x05\x12\x13\n\x0btemperature\x18\t \x01(\x05\x12\x16\n\x0ewarning_number\x18\n \x01(\x05\x12\x14\n\x0c\x63rc_checksum\x18\x0b \x01(\x05\x12\x13\n\x0blink_status\x18\x0c \x01(\x05\x12\x13\n\x0bpower_limit\x18\r \x01(\x05\x12\x1f\n\x17modulation_index_signal\x18\x14 \x01(\x05\"\xe9\x03\n\x05TGSMO\x12\x15\n\rserial_number\x18\x01 \x01(\x03\x12\x18\n\x10\x66irmware_version\x18\x02 \x01(\x05\x12\x17\n\x0fvoltage_phase_A\x18\x03 \x01(\x05\x12\x17\n\x0fvoltage_phase_B\x18\x04 \x01(\x05\x12\x17\n\x0fvoltage_phase_C\x18\x05 \x01(\x05\x12\x17\n\x0fvoltage_line_AB\x18\x06 \x01(\x05\x12\x17\n\x0fvoltage_line_BC\x18\x07 \x01(\x05\x12\x17\n\x0fvoltage_line_CA\x18\x08 \x01(\x05\x12\x11\n\tfrequency\x18\t \x01(\x05\x12\x14\n\x0c\x61\x63tive_power\x18\n \x01(\x05\x12\x16\n\x0ereactive_power\x18\x0b \x01(\x05\x12\x17\n\x0f\x63urrent_phase_A\x18\x0c \x01(\x05\x12\x17\n\x0f\x63urrent_phase_B\x18\r \x01(\x05\x12\x17\n\x0f\x63urrent_phase_C\x18\x0e \x01(\x05\x12\x14\n\x0cpower_factor\x18\x0f \x01(\x05\x12\x13\n\x0btemperature\x18\x10 \x01(\x05\x12\x16\n\x0ewarning_number\x18\x11 \x01(\x05\x12\x14\n\x0c\x63rc_checksum\x18\x12 \x01(\x05\x12\x13\n\x0blink_status\x18\x13 \x01(\x05\x12\x1f\n\x17modulation_index_signal\x18\x14 \x01(\x05\"\xa3\x01\n\x04PvMO\x12\x15\n\rserial_number\x18\x01 \x01(\x03\x12\x13\n\x0bport_number\x18\x02 \x01(\x05\x12\x0f\n\x07voltage\x18\x03 \x01(\x05\x12\x0f\n\x07\x63urrent\x18\x04 \x01(\x05\x12\r\n\x05power\x18\x05 \x01(\x05\x12\x14\n\x0c\x65nergy_total\x18\x06 \x01(\x05\x12\x14\n\x0c\x65nergy_daily\x18\x07 \x01(\x05\x12\x12\n\nerror_code\x18\x08 \x01(\x05\"\xc9\x02\n\x11RealDataNewReqDTO\x12\x1c\n\x14\x64\x65vice_serial_number\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x05\x12\x14\n\x0c\x61\x63tive_power\x18\x03 \x01(\x05\x12\n\n\x02\x63p\x18\x04 \x01(\x05\x12\x18\n\x10\x66irmware_version\x18\x05 \x01(\x05\x12\x1c\n\nmeter_data\x18\x06 \x03(\x0b\x32\x08.MeterMO\x12\x16\n\x07rp_data\x18\x07 \x03(\x0b\x32\x05.RpMO\x12\x18\n\x08rsd_data\x18\x08 \x03(\x0b\x32\x06.RSDMO\x12\x18\n\x08sgs_data\x18\t \x03(\x0b\x32\x06.SGSMO\x12\x18\n\x08tgs_data\x18\n \x03(\x0b\x32\x06.TGSMO\x12\x16\n\x07pv_data\x18\x0b \x03(\x0b\x32\x05.PvMO\x12\x11\n\tdtu_power\x18\x0c \x01(\x04\x12\x18\n\x10\x64tu_daily_energy\x18\r \x01(\x04\"g\n\x11RealDataNewResDTO\x12\x14\n\x0ctime_ymd_hms\x18\x01 \x01(\t\x12\n\n\x02\x63p\x18\x02 \x01(\x05\x12\x12\n\nerror_code\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\x05 \x01(\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'RealDataNew_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_METERMO']._serialized_start=22 + _globals['_METERMO']._serialized_end=689 + _globals['_RPMO']._serialized_start=691 + _globals['_RPMO']._serialized_end=796 + _globals['_RSDMO']._serialized_start=799 + _globals['_RSDMO']._serialized_end=975 + _globals['_SGSMO']._serialized_start=978 + _globals['_SGSMO']._serialized_end=1297 + _globals['_TGSMO']._serialized_start=1300 + _globals['_TGSMO']._serialized_end=1789 + _globals['_PVMO']._serialized_start=1792 + _globals['_PVMO']._serialized_end=1955 + _globals['_REALDATANEWREQDTO']._serialized_start=1958 + _globals['_REALDATANEWREQDTO']._serialized_end=2287 + _globals['_REALDATANEWRESDTO']._serialized_start=2289 + _globals['_REALDATANEWRESDTO']._serialized_end=2392 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/RealData_pb2.py b/hoymiles_wifi/protobuf/RealData_pb2.py new file mode 100644 index 0000000..cdbff51 --- /dev/null +++ b/hoymiles_wifi/protobuf/RealData_pb2.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: RealData.proto +# Protobuf Python Version: 4.25.0 +"""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\x0eRealData.proto\"\xc5\x03\n\x0bMeterDataMO\x12\x13\n\x0b\x64\x65vice_kind\x18\x01 \x01(\x05\x12\x10\n\x08meter_sn\x18\x02 \x01(\x03\x12\x19\n\x11meter_total_power\x18\x03 \x01(\x05\x12\x1b\n\x13meter_phase_A_power\x18\x04 \x01(\x05\x12\x1b\n\x13meter_phase_B_power\x18\x05 \x01(\x05\x12\x1b\n\x13meter_phase_C_power\x18\x06 \x01(\x05\x12\x14\n\x0cmeter_factor\x18\x07 \x01(\x05\x12\x1a\n\x12meter_total_energy\x18\x08 \x01(\x05\x12\x1c\n\x14meter_phase_A_energy\x18\t \x01(\x05\x12\x1c\n\x14meter_phase_B_energy\x18\n \x01(\x05\x12\x1c\n\x14meter_phase_C_energy\x18\x0b \x01(\x05\x12\x1c\n\x14meter_total_consumed\x18\x0c \x01(\x05\x12\x1e\n\x16meter_phase_A_consumed\x18\r \x01(\x05\x12\x1e\n\x16meter_phase_B_consumed\x18\x0e \x01(\x05\x12\x1e\n\x16meter_phase_C_consumed\x18\x0f \x01(\x05\x12\x13\n\x0bmeter_fault\x18\x10 \x01(\x05\"m\n\x08RpDataMO\x12\r\n\x05rp_sn\x18\x01 \x01(\x03\x12\x11\n\trp_signal\x18\x02 \x01(\x05\x12\x12\n\nrp_channel\x18\x03 \x01(\x05\x12\x13\n\x0brp_link_nub\x18\x04 \x01(\x05\x12\x16\n\x0erp_link_status\x18\x05 \x01(\x05\"\xdd\x03\n\x08PvDataMO\x12\r\n\x05pv_sn\x18\x01 \x01(\x03\x12\x0f\n\x07pv_port\x18\x02 \x01(\x05\x12\x0e\n\x06pv_vol\x18\x03 \x01(\x05\x12\x0e\n\x06pv_cur\x18\x04 \x01(\x05\x12\x10\n\x08pv_power\x18\x05 \x01(\x05\x12\x17\n\x0fpv_energy_total\x18\x06 \x01(\x05\x12\x10\n\x08grid_vol\x18\x07 \x01(\x05\x12\x14\n\x0cgrid_vol_max\x18\x08 \x01(\x05\x12\x11\n\tgrid_freq\x18\t \x01(\x05\x12\x0e\n\x06grid_p\x18\n \x01(\x05\x12\x0e\n\x06grid_q\x18\x0b \x01(\x05\x12\x0e\n\x06grid_i\x18\x0c \x01(\x05\x12\x0f\n\x07grid_pf\x18\r \x01(\x05\x12\x0f\n\x07pv_temp\x18\x0e \x01(\x05\x12\x15\n\rpv_run_status\x18\x0f \x01(\x05\x12\x14\n\x0cpv_fault_num\x18\x10 \x01(\x05\x12\x14\n\x0cpv_fault_cnt\x18\x11 \x01(\x05\x12\x16\n\x0epv_warning_cnt\x18\x12 \x01(\x05\x12\x16\n\x0epv_link_status\x18\x13 \x01(\x05\x12\x15\n\rpv_send_power\x18\x14 \x01(\x05\x12\x18\n\x10pv_receive_power\x18\x15 \x01(\x05\x12\x0f\n\x07pv_time\x18\x16 \x01(\x05\x12\x11\n\tpv_energy\x18\x17 \x01(\x05\x12\x11\n\tmi_signal\x18\x18 \x01(\x05\"\x86\x02\n\x0eRealDataReqDTO\x12\x0e\n\x06\x64tu_sn\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x05\x12\x15\n\rdevice_number\x18\x03 \x01(\x05\x12\x11\n\tpv_number\x18\x04 \x01(\x05\x12\x16\n\x0epackage_number\x18\x05 \x01(\x05\x12\x17\n\x0f\x63urrent_package\x18\x06 \x01(\x05\x12\x0b\n\x03\x63sq\x18\x07 \x01(\x05\x12 \n\nmeter_data\x18\x08 \x03(\x0b\x32\x0c.MeterDataMO\x12\x1a\n\x07rp_data\x18\t \x03(\x0b\x32\t.RpDataMO\x12\x1a\n\x07pv_data\x18\n \x03(\x0b\x32\t.PvDataMO\x12\x0f\n\x07version\x18\x0b \x01(\x05\"d\n\x0eRealDataResDTO\x12\x14\n\x0ctime_ymd_hms\x18\x01 \x01(\t\x12\n\n\x02\x63p\x18\x02 \x01(\x05\x12\x12\n\nerror_code\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\x05 \x01(\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'RealData_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_METERDATAMO']._serialized_start=19 + _globals['_METERDATAMO']._serialized_end=472 + _globals['_RPDATAMO']._serialized_start=474 + _globals['_RPDATAMO']._serialized_end=583 + _globals['_PVDATAMO']._serialized_start=586 + _globals['_PVDATAMO']._serialized_end=1063 + _globals['_REALDATAREQDTO']._serialized_start=1066 + _globals['_REALDATAREQDTO']._serialized_end=1328 + _globals['_REALDATARESDTO']._serialized_start=1330 + _globals['_REALDATARESDTO']._serialized_end=1430 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/SetConfig.proto b/hoymiles_wifi/protobuf/SetConfig.proto new file mode 100644 index 0000000..bc79133 --- /dev/null +++ b/hoymiles_wifi/protobuf/SetConfig.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +// Definition of the response message for setting configuration. +message SetConfigResDTO { + int32 offset = 1; // Offset value + uint32 time = 2; // Time value + int32 lock_password = 3; // Lock password + int32 lock_time = 4; // Lock time + int32 limit_power_mypower = 5; // Limit power for mypower + int32 zero_export_433_addr = 6; // Zero export 433 address + int32 zero_export_enable = 7; // Zero export enable flag + int32 netmode_select = 8; // Network mode selection + int32 channel_select = 9; // Channel selection + int32 server_send_time = 10; // Server send time + int32 serverport = 11; // Server port + string apn_set = 12; // Access Point Name (APN) setting + string meter_kind = 13; // Meter kind + string meter_interface = 14; // Meter interface + string wifi_ssid = 15; // Wi-Fi SSID + string wifi_password = 16; // Wi-Fi password + string server_domain_name = 17; // Server domain name + int32 inv_type = 18; // Inverter type + string dtu_sn = 19; // Data Terminal Unit (DTU) serial number + int32 access_model = 20; // Access model + int32 mac_0 = 21; // MAC address octet 0 + int32 mac_1 = 22; // MAC address octet 1 + int32 mac_2 = 23; // MAC address octet 2 + int32 mac_3 = 24; // MAC address octet 3 + int32 dhcp_switch = 25; // DHCP switch + int32 ip_addr_0 = 26; // IP address octet 0 + int32 ip_addr_1 = 27; // IP address octet 1 + int32 ip_addr_2 = 28; // IP address octet 2 + int32 ip_addr_3 = 29; // IP address octet 3 + int32 subnet_mask_0 = 30; // Subnet mask octet 0 + int32 subnet_mask_1 = 31; // Subnet mask octet 1 + int32 subnet_mask_2 = 32; // Subnet mask octet 2 + int32 subnet_mask_3 = 33; // Subnet mask octet 3 + int32 default_gateway_0 = 34; // Default gateway octet 0 + int32 default_gateway_1 = 35; // Default gateway octet 1 + int32 default_gateway_2 = 36; // Default gateway octet 2 + int32 default_gateway_3 = 37; // Default gateway octet 3 + string apn_name = 38; // APN name + string apn_password = 39; // APN password + int32 sub1g_sweep_switch = 40; // Sub-1GHz sweep switch + int32 sub1g_work_channel = 41; // Sub-1GHz work channel + int32 cable_dns_0 = 42; // Cable DNS octet 0 + int32 cable_dns_1 = 43; // Cable DNS octet 1 + int32 cable_dns_2 = 44; // Cable DNS octet 2 + int32 cable_dns_3 = 45; // Cable DNS octet 3 + int32 mac_4 = 46; // MAC address octet 4 + int32 mac_5 = 47; // MAC address octet 5 + string dtu_ap_ssid = 48; // DTU AP SSID + string dtu_ap_pass = 49; // DTU AP password + int32 app_page = 50; // App page +} + +// Definition of the request message for setting configuration. +message SetConfigReqDTO { + int32 offset = 1; // Offset value + uint32 time = 2; // Time value + int32 error_code = 3; // Error code +} diff --git a/hoymiles_wifi/protobuf/SetConfig_pb2.py b/hoymiles_wifi/protobuf/SetConfig_pb2.py new file mode 100644 index 0000000..ef892dd --- /dev/null +++ b/hoymiles_wifi/protobuf/SetConfig_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: SetConfig.proto +# Protobuf Python Version: 4.25.0 +"""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\x0fSetConfig.proto\"\xbd\x08\n\x0fSetConfigResDTO\x12\x0e\n\x06offset\x18\x01 \x01(\x05\x12\x0c\n\x04time\x18\x02 \x01(\r\x12\x15\n\rlock_password\x18\x03 \x01(\x05\x12\x11\n\tlock_time\x18\x04 \x01(\x05\x12\x1b\n\x13limit_power_mypower\x18\x05 \x01(\x05\x12\x1c\n\x14zero_export_433_addr\x18\x06 \x01(\x05\x12\x1a\n\x12zero_export_enable\x18\x07 \x01(\x05\x12\x16\n\x0enetmode_select\x18\x08 \x01(\x05\x12\x16\n\x0e\x63hannel_select\x18\t \x01(\x05\x12\x18\n\x10server_send_time\x18\n \x01(\x05\x12\x12\n\nserverport\x18\x0b \x01(\x05\x12\x0f\n\x07\x61pn_set\x18\x0c \x01(\t\x12\x12\n\nmeter_kind\x18\r \x01(\t\x12\x17\n\x0fmeter_interface\x18\x0e \x01(\t\x12\x11\n\twifi_ssid\x18\x0f \x01(\t\x12\x15\n\rwifi_password\x18\x10 \x01(\t\x12\x1a\n\x12server_domain_name\x18\x11 \x01(\t\x12\x10\n\x08inv_type\x18\x12 \x01(\x05\x12\x0e\n\x06\x64tu_sn\x18\x13 \x01(\t\x12\x14\n\x0c\x61\x63\x63\x65ss_model\x18\x14 \x01(\x05\x12\r\n\x05mac_0\x18\x15 \x01(\x05\x12\r\n\x05mac_1\x18\x16 \x01(\x05\x12\r\n\x05mac_2\x18\x17 \x01(\x05\x12\r\n\x05mac_3\x18\x18 \x01(\x05\x12\x13\n\x0b\x64hcp_switch\x18\x19 \x01(\x05\x12\x11\n\tip_addr_0\x18\x1a \x01(\x05\x12\x11\n\tip_addr_1\x18\x1b \x01(\x05\x12\x11\n\tip_addr_2\x18\x1c \x01(\x05\x12\x11\n\tip_addr_3\x18\x1d \x01(\x05\x12\x15\n\rsubnet_mask_0\x18\x1e \x01(\x05\x12\x15\n\rsubnet_mask_1\x18\x1f \x01(\x05\x12\x15\n\rsubnet_mask_2\x18 \x01(\x05\x12\x15\n\rsubnet_mask_3\x18! \x01(\x05\x12\x19\n\x11\x64\x65\x66\x61ult_gateway_0\x18\" \x01(\x05\x12\x19\n\x11\x64\x65\x66\x61ult_gateway_1\x18# \x01(\x05\x12\x19\n\x11\x64\x65\x66\x61ult_gateway_2\x18$ \x01(\x05\x12\x19\n\x11\x64\x65\x66\x61ult_gateway_3\x18% \x01(\x05\x12\x10\n\x08\x61pn_name\x18& \x01(\t\x12\x14\n\x0c\x61pn_password\x18\' \x01(\t\x12\x1a\n\x12sub1g_sweep_switch\x18( \x01(\x05\x12\x1a\n\x12sub1g_work_channel\x18) \x01(\x05\x12\x13\n\x0b\x63\x61\x62le_dns_0\x18* \x01(\x05\x12\x13\n\x0b\x63\x61\x62le_dns_1\x18+ \x01(\x05\x12\x13\n\x0b\x63\x61\x62le_dns_2\x18, \x01(\x05\x12\x13\n\x0b\x63\x61\x62le_dns_3\x18- \x01(\x05\x12\r\n\x05mac_4\x18. \x01(\x05\x12\r\n\x05mac_5\x18/ \x01(\x05\x12\x13\n\x0b\x64tu_ap_ssid\x18\x30 \x01(\t\x12\x13\n\x0b\x64tu_ap_pass\x18\x31 \x01(\t\x12\x10\n\x08\x61pp_page\x18\x32 \x01(\x05\"C\n\x0fSetConfigReqDTO\x12\x0e\n\x06offset\x18\x01 \x01(\x05\x12\x0c\n\x04time\x18\x02 \x01(\r\x12\x12\n\nerror_code\x18\x03 \x01(\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'SetConfig_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_SETCONFIGRESDTO']._serialized_start=20 + _globals['_SETCONFIGRESDTO']._serialized_end=1105 + _globals['_SETCONFIGREQDTO']._serialized_start=1107 + _globals['_SETCONFIGREQDTO']._serialized_end=1174 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/WarnData.proto b/hoymiles_wifi/protobuf/WarnData.proto new file mode 100644 index 0000000..a02b8f6 --- /dev/null +++ b/hoymiles_wifi/protobuf/WarnData.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; + +message WarnReqDTO { + string dtu_sn = 1; + int32 time = 2; + int32 package_nub = 3; + int32 package_now = 4; + int32 warn_device = 5; + repeated WarnMO warns = 6; +} + +message WarnMO { + int64 pv_sn = 1; + int32 code = 2; + int32 num = 3; + int32 s_time = 4; + int32 e_time = 5; + int32 w_data1 = 6; + int32 w_data2 = 7; +} + +message WarnResDTO { + string ymd_hms = 1; + int32 package_now = 2; + int32 err_code = 3; + int32 offset = 4; + int32 time = 5; +} + +message WaveReqDTO { + string dtu_sn = 1; + int32 time = 2; + int32 package_nub = 3; + int32 package_now = 4; + int64 pv_sn = 5; + int32 code = 6; + int32 num = 7; + int32 warn_time = 8; + int32 data_len = 9; + int32 pos = 10; + string warn_data = 11; +} + +message WaveResDTO { + string ymd_hms = 1; + int32 package_now = 2; + int32 err_code = 3; + int32 offset = 4; + int32 time = 5; +} diff --git a/hoymiles_wifi/protobuf/WarnData_pb2.py b/hoymiles_wifi/protobuf/WarnData_pb2.py new file mode 100644 index 0000000..98afc0b --- /dev/null +++ b/hoymiles_wifi/protobuf/WarnData_pb2.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: WarnData.proto +# Protobuf Python Version: 4.25.0 +"""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\x0eWarnData.proto\"\x81\x01\n\nWarnReqDTO\x12\x0e\n\x06\x64tu_sn\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x13\n\x0bpackage_nub\x18\x03 \x01(\x05\x12\x13\n\x0bpackage_now\x18\x04 \x01(\x05\x12\x13\n\x0bwarn_device\x18\x05 \x01(\x05\x12\x16\n\x05warns\x18\x06 \x03(\x0b\x32\x07.WarnMO\"t\n\x06WarnMO\x12\r\n\x05pv_sn\x18\x01 \x01(\x03\x12\x0c\n\x04\x63ode\x18\x02 \x01(\x05\x12\x0b\n\x03num\x18\x03 \x01(\x05\x12\x0e\n\x06s_time\x18\x04 \x01(\x05\x12\x0e\n\x06\x65_time\x18\x05 \x01(\x05\x12\x0f\n\x07w_data1\x18\x06 \x01(\x05\x12\x0f\n\x07w_data2\x18\x07 \x01(\x05\"b\n\nWarnResDTO\x12\x0f\n\x07ymd_hms\x18\x01 \x01(\t\x12\x13\n\x0bpackage_now\x18\x02 \x01(\x05\x12\x10\n\x08\x65rr_code\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\x05 \x01(\x05\"\xc3\x01\n\nWaveReqDTO\x12\x0e\n\x06\x64tu_sn\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x05\x12\x13\n\x0bpackage_nub\x18\x03 \x01(\x05\x12\x13\n\x0bpackage_now\x18\x04 \x01(\x05\x12\r\n\x05pv_sn\x18\x05 \x01(\x03\x12\x0c\n\x04\x63ode\x18\x06 \x01(\x05\x12\x0b\n\x03num\x18\x07 \x01(\x05\x12\x11\n\twarn_time\x18\x08 \x01(\x05\x12\x10\n\x08\x64\x61ta_len\x18\t \x01(\x05\x12\x0b\n\x03pos\x18\n \x01(\x05\x12\x11\n\twarn_data\x18\x0b \x01(\t\"b\n\nWaveResDTO\x12\x0f\n\x07ymd_hms\x18\x01 \x01(\t\x12\x13\n\x0bpackage_now\x18\x02 \x01(\x05\x12\x10\n\x08\x65rr_code\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\x12\x0c\n\x04time\x18\x05 \x01(\x05\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'WarnData_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_WARNREQDTO']._serialized_start=19 + _globals['_WARNREQDTO']._serialized_end=148 + _globals['_WARNMO']._serialized_start=150 + _globals['_WARNMO']._serialized_end=266 + _globals['_WARNRESDTO']._serialized_start=268 + _globals['_WARNRESDTO']._serialized_end=366 + _globals['_WAVEREQDTO']._serialized_start=369 + _globals['_WAVEREQDTO']._serialized_end=564 + _globals['_WAVERESDTO']._serialized_start=566 + _globals['_WAVERESDTO']._serialized_end=664 +# @@protoc_insertion_point(module_scope) diff --git a/hoymiles_wifi/protobuf/__init__.py b/hoymiles_wifi/protobuf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hoymiles_wifi/protobuf/compile_proto.sh b/hoymiles_wifi/protobuf/compile_proto.sh new file mode 100644 index 0000000..fc92142 --- /dev/null +++ b/hoymiles_wifi/protobuf/compile_proto.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +for file in $(ls *.proto) +do + #protoc --pyi_out=. $file + protoc --python_out=. $file +done diff --git a/hoymiles_wifi/utils.py b/hoymiles_wifi/utils.py new file mode 100644 index 0000000..52c3dd3 --- /dev/null +++ b/hoymiles_wifi/utils.py @@ -0,0 +1,61 @@ +"""Utils for interacting with Hoymiles WiFi API.""" + +from hoymiles_wifi.protobuf import ( + GetConfig_pb2, + SetConfig_pb2, +) + + +def initialize_set_config(get_config_req: GetConfig_pb2.GetConfigReqDTO): + """Initialize set config response with get config request.""" + + set_config_res = SetConfig_pb2.SetConfigResDTO() + set_config_res.lock_password = get_config_req.lock_password + set_config_res.lock_time = get_config_req.lock_time + set_config_res.limit_power_mypower = get_config_req.limit_power_mypower + set_config_res.zero_export_433_addr = get_config_req.zero_export_433_addr + set_config_res.zero_export_enable = get_config_req.zero_export_enable + set_config_res.netmode_select = get_config_req.netmode_select + set_config_res.channel_select = get_config_req.channel_select + set_config_res.server_send_time = get_config_req.server_send_time + set_config_res.serverport = get_config_req.serverport + set_config_res.apn_set = get_config_req.apn_set + set_config_res.meter_kind = get_config_req.meter_kind + set_config_res.meter_interface = get_config_req.meter_interface + set_config_res.wifi_ssid = get_config_req.wifi_ssid + set_config_res.wifi_password = get_config_req.wifi_password + set_config_res.server_domain_name = get_config_req.server_domain_name + set_config_res.inv_type = get_config_req.inv_type + set_config_res.dtu_sn = get_config_req.dtu_sn + set_config_res.access_model = get_config_req.access_model + set_config_res.mac_0 = get_config_req.mac_0 + set_config_res.mac_1 = get_config_req.mac_1 + set_config_res.mac_2 = get_config_req.mac_2 + set_config_res.mac_3 = get_config_req.mac_3 + set_config_res.mac_4 = get_config_req.mac_4 + set_config_res.mac_5 = get_config_req.mac_5 + set_config_res.dhcp_switch = get_config_req.dhcp_switch + set_config_res.ip_addr_0 = get_config_req.ip_addr_0 + set_config_res.ip_addr_1 = get_config_req.ip_addr_1 + set_config_res.ip_addr_2 = get_config_req.ip_addr_2 + set_config_res.ip_addr_3 = get_config_req.ip_addr_3 + set_config_res.subnet_mask_0 = get_config_req.subnet_mask_0 + set_config_res.subnet_mask_1 = get_config_req.subnet_mask_1 + set_config_res.subnet_mask_2 = get_config_req.subnet_mask_2 + set_config_res.subnet_mask_3 = get_config_req.subnet_mask_3 + set_config_res.default_gateway_0 = get_config_req.default_gateway_0 + set_config_res.default_gateway_1 = get_config_req.default_gateway_1 + set_config_res.default_gateway_2 = get_config_req.default_gateway_2 + set_config_res.default_gateway_3 = get_config_req.default_gateway_3 + set_config_res.apn_name = get_config_req.apn_name + set_config_res.apn_password = get_config_req.apn_password + set_config_res.sub1g_sweep_switch = get_config_req.sub1g_sweep_switch + set_config_res.sub1g_work_channel = get_config_req.sub1g_work_channel + set_config_res.cable_dns_0 = get_config_req.cable_dns_0 + set_config_res.cable_dns_1 = get_config_req.cable_dns_1 + set_config_res.cable_dns_2 = get_config_req.cable_dns_2 + set_config_res.cable_dns_3 = get_config_req.cable_dns_3 + set_config_res.dtu_ap_ssid = get_config_req.dtu_ap_ssid + set_config_res.dtu_ap_pass = get_config_req.dtu_ap_pass + + return set_config_res diff --git a/idna/__init__.py b/idna/__init__.py new file mode 100644 index 0000000..a40eeaf --- /dev/null +++ b/idna/__init__.py @@ -0,0 +1,44 @@ +from .package_data import __version__ +from .core import ( + IDNABidiError, + IDNAError, + InvalidCodepoint, + InvalidCodepointContext, + alabel, + check_bidi, + check_hyphen_ok, + check_initial_combiner, + check_label, + check_nfc, + decode, + encode, + ulabel, + uts46_remap, + valid_contextj, + valid_contexto, + valid_label_length, + valid_string_length, +) +from .intranges import intranges_contain + +__all__ = [ + "IDNABidiError", + "IDNAError", + "InvalidCodepoint", + "InvalidCodepointContext", + "alabel", + "check_bidi", + "check_hyphen_ok", + "check_initial_combiner", + "check_label", + "check_nfc", + "decode", + "encode", + "intranges_contain", + "ulabel", + "uts46_remap", + "valid_contextj", + "valid_contexto", + "valid_label_length", + "valid_string_length", +] diff --git a/idna/codec.py b/idna/codec.py new file mode 100644 index 0000000..eaeada5 --- /dev/null +++ b/idna/codec.py @@ -0,0 +1,118 @@ +from .core import encode, decode, alabel, ulabel, IDNAError +import codecs +import re +from typing import Any, Tuple, Optional + +_unicode_dots_re = re.compile('[\u002e\u3002\uff0e\uff61]') + +class Codec(codecs.Codec): + + def encode(self, data: str, errors: str = 'strict') -> Tuple[bytes, int]: + if errors != 'strict': + raise IDNAError('Unsupported error handling \"{}\"'.format(errors)) + + if not data: + return b"", 0 + + return encode(data), len(data) + + def decode(self, data: bytes, errors: str = 'strict') -> Tuple[str, int]: + if errors != 'strict': + raise IDNAError('Unsupported error handling \"{}\"'.format(errors)) + + if not data: + return '', 0 + + return decode(data), len(data) + +class IncrementalEncoder(codecs.BufferedIncrementalEncoder): + def _buffer_encode(self, data: str, errors: str, final: bool) -> Tuple[bytes, int]: + if errors != 'strict': + raise IDNAError('Unsupported error handling \"{}\"'.format(errors)) + + if not data: + return b'', 0 + + labels = _unicode_dots_re.split(data) + trailing_dot = b'' + if labels: + if not labels[-1]: + trailing_dot = b'.' + del labels[-1] + elif not final: + # Keep potentially unfinished label until the next call + del labels[-1] + if labels: + trailing_dot = b'.' + + result = [] + size = 0 + for label in labels: + result.append(alabel(label)) + if size: + size += 1 + size += len(label) + + # Join with U+002E + result_bytes = b'.'.join(result) + trailing_dot + size += len(trailing_dot) + return result_bytes, size + +class IncrementalDecoder(codecs.BufferedIncrementalDecoder): + def _buffer_decode(self, data: Any, errors: str, final: bool) -> Tuple[str, int]: + if errors != 'strict': + raise IDNAError('Unsupported error handling \"{}\"'.format(errors)) + + if not data: + return ('', 0) + + if not isinstance(data, str): + data = str(data, 'ascii') + + labels = _unicode_dots_re.split(data) + trailing_dot = '' + if labels: + if not labels[-1]: + trailing_dot = '.' + del labels[-1] + elif not final: + # Keep potentially unfinished label until the next call + del labels[-1] + if labels: + trailing_dot = '.' + + result = [] + size = 0 + for label in labels: + result.append(ulabel(label)) + if size: + size += 1 + size += len(label) + + result_str = '.'.join(result) + trailing_dot + size += len(trailing_dot) + return (result_str, size) + + +class StreamWriter(Codec, codecs.StreamWriter): + pass + + +class StreamReader(Codec, codecs.StreamReader): + pass + + +def search_function(name: str) -> Optional[codecs.CodecInfo]: + if name != 'idna2008': + return None + return codecs.CodecInfo( + name=name, + encode=Codec().encode, # type: ignore + decode=Codec().decode, # type: ignore + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamwriter=StreamWriter, + streamreader=StreamReader, + ) + +codecs.register(search_function) diff --git a/idna/compat.py b/idna/compat.py new file mode 100644 index 0000000..786e6bd --- /dev/null +++ b/idna/compat.py @@ -0,0 +1,13 @@ +from .core import * +from .codec import * +from typing import Any, Union + +def ToASCII(label: str) -> bytes: + return encode(label) + +def ToUnicode(label: Union[bytes, bytearray]) -> str: + return decode(label) + +def nameprep(s: Any) -> None: + raise NotImplementedError('IDNA 2008 does not utilise nameprep protocol') + diff --git a/idna/core.py b/idna/core.py new file mode 100644 index 0000000..0bd89a3 --- /dev/null +++ b/idna/core.py @@ -0,0 +1,400 @@ +from . import idnadata +import bisect +import unicodedata +import re +from typing import Union, Optional +from .intranges import intranges_contain + +_virama_combining_class = 9 +_alabel_prefix = b'xn--' +_unicode_dots_re = re.compile('[\u002e\u3002\uff0e\uff61]') + +class IDNAError(UnicodeError): + """ Base exception for all IDNA-encoding related problems """ + pass + + +class IDNABidiError(IDNAError): + """ Exception when bidirectional requirements are not satisfied """ + pass + + +class InvalidCodepoint(IDNAError): + """ Exception when a disallowed or unallocated codepoint is used """ + pass + + +class InvalidCodepointContext(IDNAError): + """ Exception when the codepoint is not valid in the context it is used """ + pass + + +def _combining_class(cp: int) -> int: + v = unicodedata.combining(chr(cp)) + if v == 0: + if not unicodedata.name(chr(cp)): + raise ValueError('Unknown character in unicodedata') + return v + +def _is_script(cp: str, script: str) -> bool: + return intranges_contain(ord(cp), idnadata.scripts[script]) + +def _punycode(s: str) -> bytes: + return s.encode('punycode') + +def _unot(s: int) -> str: + return 'U+{:04X}'.format(s) + + +def valid_label_length(label: Union[bytes, str]) -> bool: + if len(label) > 63: + return False + return True + + +def valid_string_length(label: Union[bytes, str], trailing_dot: bool) -> bool: + if len(label) > (254 if trailing_dot else 253): + return False + return True + + +def check_bidi(label: str, check_ltr: bool = False) -> bool: + # Bidi rules should only be applied if string contains RTL characters + bidi_label = False + for (idx, cp) in enumerate(label, 1): + direction = unicodedata.bidirectional(cp) + if direction == '': + # String likely comes from a newer version of Unicode + raise IDNABidiError('Unknown directionality in label {} at position {}'.format(repr(label), idx)) + if direction in ['R', 'AL', 'AN']: + bidi_label = True + if not bidi_label and not check_ltr: + return True + + # Bidi rule 1 + direction = unicodedata.bidirectional(label[0]) + if direction in ['R', 'AL']: + rtl = True + elif direction == 'L': + rtl = False + else: + raise IDNABidiError('First codepoint in label {} must be directionality L, R or AL'.format(repr(label))) + + valid_ending = False + number_type = None # type: Optional[str] + for (idx, cp) in enumerate(label, 1): + direction = unicodedata.bidirectional(cp) + + if rtl: + # Bidi rule 2 + if not direction in ['R', 'AL', 'AN', 'EN', 'ES', 'CS', 'ET', 'ON', 'BN', 'NSM']: + raise IDNABidiError('Invalid direction for codepoint at position {} in a right-to-left label'.format(idx)) + # Bidi rule 3 + if direction in ['R', 'AL', 'EN', 'AN']: + valid_ending = True + elif direction != 'NSM': + valid_ending = False + # Bidi rule 4 + if direction in ['AN', 'EN']: + if not number_type: + number_type = direction + else: + if number_type != direction: + raise IDNABidiError('Can not mix numeral types in a right-to-left label') + else: + # Bidi rule 5 + if not direction in ['L', 'EN', 'ES', 'CS', 'ET', 'ON', 'BN', 'NSM']: + raise IDNABidiError('Invalid direction for codepoint at position {} in a left-to-right label'.format(idx)) + # Bidi rule 6 + if direction in ['L', 'EN']: + valid_ending = True + elif direction != 'NSM': + valid_ending = False + + if not valid_ending: + raise IDNABidiError('Label ends with illegal codepoint directionality') + + return True + + +def check_initial_combiner(label: str) -> bool: + if unicodedata.category(label[0])[0] == 'M': + raise IDNAError('Label begins with an illegal combining character') + return True + + +def check_hyphen_ok(label: str) -> bool: + if label[2:4] == '--': + raise IDNAError('Label has disallowed hyphens in 3rd and 4th position') + if label[0] == '-' or label[-1] == '-': + raise IDNAError('Label must not start or end with a hyphen') + return True + + +def check_nfc(label: str) -> None: + if unicodedata.normalize('NFC', label) != label: + raise IDNAError('Label must be in Normalization Form C') + + +def valid_contextj(label: str, pos: int) -> bool: + cp_value = ord(label[pos]) + + if cp_value == 0x200c: + + if pos > 0: + if _combining_class(ord(label[pos - 1])) == _virama_combining_class: + return True + + ok = False + for i in range(pos-1, -1, -1): + joining_type = idnadata.joining_types.get(ord(label[i])) + if joining_type == ord('T'): + continue + if joining_type in [ord('L'), ord('D')]: + ok = True + break + + if not ok: + return False + + ok = False + for i in range(pos+1, len(label)): + joining_type = idnadata.joining_types.get(ord(label[i])) + if joining_type == ord('T'): + continue + if joining_type in [ord('R'), ord('D')]: + ok = True + break + return ok + + if cp_value == 0x200d: + + if pos > 0: + if _combining_class(ord(label[pos - 1])) == _virama_combining_class: + return True + return False + + else: + + return False + + +def valid_contexto(label: str, pos: int, exception: bool = False) -> bool: + cp_value = ord(label[pos]) + + if cp_value == 0x00b7: + if 0 < pos < len(label)-1: + if ord(label[pos - 1]) == 0x006c and ord(label[pos + 1]) == 0x006c: + return True + return False + + elif cp_value == 0x0375: + if pos < len(label)-1 and len(label) > 1: + return _is_script(label[pos + 1], 'Greek') + return False + + elif cp_value == 0x05f3 or cp_value == 0x05f4: + if pos > 0: + return _is_script(label[pos - 1], 'Hebrew') + return False + + elif cp_value == 0x30fb: + for cp in label: + if cp == '\u30fb': + continue + if _is_script(cp, 'Hiragana') or _is_script(cp, 'Katakana') or _is_script(cp, 'Han'): + return True + return False + + elif 0x660 <= cp_value <= 0x669: + for cp in label: + if 0x6f0 <= ord(cp) <= 0x06f9: + return False + return True + + elif 0x6f0 <= cp_value <= 0x6f9: + for cp in label: + if 0x660 <= ord(cp) <= 0x0669: + return False + return True + + return False + + +def check_label(label: Union[str, bytes, bytearray]) -> None: + if isinstance(label, (bytes, bytearray)): + label = label.decode('utf-8') + if len(label) == 0: + raise IDNAError('Empty Label') + + check_nfc(label) + check_hyphen_ok(label) + check_initial_combiner(label) + + for (pos, cp) in enumerate(label): + cp_value = ord(cp) + if intranges_contain(cp_value, idnadata.codepoint_classes['PVALID']): + continue + elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTJ']): + try: + if not valid_contextj(label, pos): + raise InvalidCodepointContext('Joiner {} not allowed at position {} in {}'.format( + _unot(cp_value), pos+1, repr(label))) + except ValueError: + raise IDNAError('Unknown codepoint adjacent to joiner {} at position {} in {}'.format( + _unot(cp_value), pos+1, repr(label))) + elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTO']): + if not valid_contexto(label, pos): + raise InvalidCodepointContext('Codepoint {} not allowed at position {} in {}'.format(_unot(cp_value), pos+1, repr(label))) + else: + raise InvalidCodepoint('Codepoint {} at position {} of {} not allowed'.format(_unot(cp_value), pos+1, repr(label))) + + check_bidi(label) + + +def alabel(label: str) -> bytes: + try: + label_bytes = label.encode('ascii') + ulabel(label_bytes) + if not valid_label_length(label_bytes): + raise IDNAError('Label too long') + return label_bytes + except UnicodeEncodeError: + pass + + if not label: + raise IDNAError('No Input') + + label = str(label) + check_label(label) + label_bytes = _punycode(label) + label_bytes = _alabel_prefix + label_bytes + + if not valid_label_length(label_bytes): + raise IDNAError('Label too long') + + return label_bytes + + +def ulabel(label: Union[str, bytes, bytearray]) -> str: + if not isinstance(label, (bytes, bytearray)): + try: + label_bytes = label.encode('ascii') + except UnicodeEncodeError: + check_label(label) + return label + else: + label_bytes = label + + label_bytes = label_bytes.lower() + if label_bytes.startswith(_alabel_prefix): + label_bytes = label_bytes[len(_alabel_prefix):] + if not label_bytes: + raise IDNAError('Malformed A-label, no Punycode eligible content found') + if label_bytes.decode('ascii')[-1] == '-': + raise IDNAError('A-label must not end with a hyphen') + else: + check_label(label_bytes) + return label_bytes.decode('ascii') + + try: + label = label_bytes.decode('punycode') + except UnicodeError: + raise IDNAError('Invalid A-label') + check_label(label) + return label + + +def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False) -> str: + """Re-map the characters in the string according to UTS46 processing.""" + from .uts46data import uts46data + output = '' + + for pos, char in enumerate(domain): + code_point = ord(char) + try: + uts46row = uts46data[code_point if code_point < 256 else + bisect.bisect_left(uts46data, (code_point, 'Z')) - 1] + status = uts46row[1] + replacement = None # type: Optional[str] + if len(uts46row) == 3: + replacement = uts46row[2] # type: ignore + if (status == 'V' or + (status == 'D' and not transitional) or + (status == '3' and not std3_rules and replacement is None)): + output += char + elif replacement is not None and (status == 'M' or + (status == '3' and not std3_rules) or + (status == 'D' and transitional)): + output += replacement + elif status != 'I': + raise IndexError() + except IndexError: + raise InvalidCodepoint( + 'Codepoint {} not allowed at position {} in {}'.format( + _unot(code_point), pos + 1, repr(domain))) + + return unicodedata.normalize('NFC', output) + + +def encode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False, transitional: bool = False) -> bytes: + if not isinstance(s, str): + try: + s = str(s, 'ascii') + except UnicodeDecodeError: + raise IDNAError('should pass a unicode string to the function rather than a byte string.') + if uts46: + s = uts46_remap(s, std3_rules, transitional) + trailing_dot = False + result = [] + if strict: + labels = s.split('.') + else: + labels = _unicode_dots_re.split(s) + if not labels or labels == ['']: + raise IDNAError('Empty domain') + if labels[-1] == '': + del labels[-1] + trailing_dot = True + for label in labels: + s = alabel(label) + if s: + result.append(s) + else: + raise IDNAError('Empty label') + if trailing_dot: + result.append(b'') + s = b'.'.join(result) + if not valid_string_length(s, trailing_dot): + raise IDNAError('Domain too long') + return s + + +def decode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False) -> str: + try: + if not isinstance(s, str): + s = str(s, 'ascii') + except UnicodeDecodeError: + raise IDNAError('Invalid ASCII in A-label') + if uts46: + s = uts46_remap(s, std3_rules, False) + trailing_dot = False + result = [] + if not strict: + labels = _unicode_dots_re.split(s) + else: + labels = s.split('.') + if not labels or labels == ['']: + raise IDNAError('Empty domain') + if not labels[-1]: + del labels[-1] + trailing_dot = True + for label in labels: + s = ulabel(label) + if s: + result.append(s) + else: + raise IDNAError('Empty label') + if trailing_dot: + result.append('') + return '.'.join(result) diff --git a/idna/idnadata.py b/idna/idnadata.py new file mode 100644 index 0000000..f9bc0d8 --- /dev/null +++ b/idna/idnadata.py @@ -0,0 +1,2148 @@ +# This file is automatically generated by tools/idna-data + +__version__ = '15.0.0' +scripts = { + 'Greek': ( + 0x37000000374, + 0x37500000378, + 0x37a0000037e, + 0x37f00000380, + 0x38400000385, + 0x38600000387, + 0x3880000038b, + 0x38c0000038d, + 0x38e000003a2, + 0x3a3000003e2, + 0x3f000000400, + 0x1d2600001d2b, + 0x1d5d00001d62, + 0x1d6600001d6b, + 0x1dbf00001dc0, + 0x1f0000001f16, + 0x1f1800001f1e, + 0x1f2000001f46, + 0x1f4800001f4e, + 0x1f5000001f58, + 0x1f5900001f5a, + 0x1f5b00001f5c, + 0x1f5d00001f5e, + 0x1f5f00001f7e, + 0x1f8000001fb5, + 0x1fb600001fc5, + 0x1fc600001fd4, + 0x1fd600001fdc, + 0x1fdd00001ff0, + 0x1ff200001ff5, + 0x1ff600001fff, + 0x212600002127, + 0xab650000ab66, + 0x101400001018f, + 0x101a0000101a1, + 0x1d2000001d246, + ), + 'Han': ( + 0x2e8000002e9a, + 0x2e9b00002ef4, + 0x2f0000002fd6, + 0x300500003006, + 0x300700003008, + 0x30210000302a, + 0x30380000303c, + 0x340000004dc0, + 0x4e000000a000, + 0xf9000000fa6e, + 0xfa700000fada, + 0x16fe200016fe4, + 0x16ff000016ff2, + 0x200000002a6e0, + 0x2a7000002b73a, + 0x2b7400002b81e, + 0x2b8200002cea2, + 0x2ceb00002ebe1, + 0x2f8000002fa1e, + 0x300000003134b, + 0x31350000323b0, + ), + 'Hebrew': ( + 0x591000005c8, + 0x5d0000005eb, + 0x5ef000005f5, + 0xfb1d0000fb37, + 0xfb380000fb3d, + 0xfb3e0000fb3f, + 0xfb400000fb42, + 0xfb430000fb45, + 0xfb460000fb50, + ), + 'Hiragana': ( + 0x304100003097, + 0x309d000030a0, + 0x1b0010001b120, + 0x1b1320001b133, + 0x1b1500001b153, + 0x1f2000001f201, + ), + 'Katakana': ( + 0x30a1000030fb, + 0x30fd00003100, + 0x31f000003200, + 0x32d0000032ff, + 0x330000003358, + 0xff660000ff70, + 0xff710000ff9e, + 0x1aff00001aff4, + 0x1aff50001affc, + 0x1affd0001afff, + 0x1b0000001b001, + 0x1b1200001b123, + 0x1b1550001b156, + 0x1b1640001b168, + ), +} +joining_types = { + 0x600: 85, + 0x601: 85, + 0x602: 85, + 0x603: 85, + 0x604: 85, + 0x605: 85, + 0x608: 85, + 0x60b: 85, + 0x620: 68, + 0x621: 85, + 0x622: 82, + 0x623: 82, + 0x624: 82, + 0x625: 82, + 0x626: 68, + 0x627: 82, + 0x628: 68, + 0x629: 82, + 0x62a: 68, + 0x62b: 68, + 0x62c: 68, + 0x62d: 68, + 0x62e: 68, + 0x62f: 82, + 0x630: 82, + 0x631: 82, + 0x632: 82, + 0x633: 68, + 0x634: 68, + 0x635: 68, + 0x636: 68, + 0x637: 68, + 0x638: 68, + 0x639: 68, + 0x63a: 68, + 0x63b: 68, + 0x63c: 68, + 0x63d: 68, + 0x63e: 68, + 0x63f: 68, + 0x640: 67, + 0x641: 68, + 0x642: 68, + 0x643: 68, + 0x644: 68, + 0x645: 68, + 0x646: 68, + 0x647: 68, + 0x648: 82, + 0x649: 68, + 0x64a: 68, + 0x66e: 68, + 0x66f: 68, + 0x671: 82, + 0x672: 82, + 0x673: 82, + 0x674: 85, + 0x675: 82, + 0x676: 82, + 0x677: 82, + 0x678: 68, + 0x679: 68, + 0x67a: 68, + 0x67b: 68, + 0x67c: 68, + 0x67d: 68, + 0x67e: 68, + 0x67f: 68, + 0x680: 68, + 0x681: 68, + 0x682: 68, + 0x683: 68, + 0x684: 68, + 0x685: 68, + 0x686: 68, + 0x687: 68, + 0x688: 82, + 0x689: 82, + 0x68a: 82, + 0x68b: 82, + 0x68c: 82, + 0x68d: 82, + 0x68e: 82, + 0x68f: 82, + 0x690: 82, + 0x691: 82, + 0x692: 82, + 0x693: 82, + 0x694: 82, + 0x695: 82, + 0x696: 82, + 0x697: 82, + 0x698: 82, + 0x699: 82, + 0x69a: 68, + 0x69b: 68, + 0x69c: 68, + 0x69d: 68, + 0x69e: 68, + 0x69f: 68, + 0x6a0: 68, + 0x6a1: 68, + 0x6a2: 68, + 0x6a3: 68, + 0x6a4: 68, + 0x6a5: 68, + 0x6a6: 68, + 0x6a7: 68, + 0x6a8: 68, + 0x6a9: 68, + 0x6aa: 68, + 0x6ab: 68, + 0x6ac: 68, + 0x6ad: 68, + 0x6ae: 68, + 0x6af: 68, + 0x6b0: 68, + 0x6b1: 68, + 0x6b2: 68, + 0x6b3: 68, + 0x6b4: 68, + 0x6b5: 68, + 0x6b6: 68, + 0x6b7: 68, + 0x6b8: 68, + 0x6b9: 68, + 0x6ba: 68, + 0x6bb: 68, + 0x6bc: 68, + 0x6bd: 68, + 0x6be: 68, + 0x6bf: 68, + 0x6c0: 82, + 0x6c1: 68, + 0x6c2: 68, + 0x6c3: 82, + 0x6c4: 82, + 0x6c5: 82, + 0x6c6: 82, + 0x6c7: 82, + 0x6c8: 82, + 0x6c9: 82, + 0x6ca: 82, + 0x6cb: 82, + 0x6cc: 68, + 0x6cd: 82, + 0x6ce: 68, + 0x6cf: 82, + 0x6d0: 68, + 0x6d1: 68, + 0x6d2: 82, + 0x6d3: 82, + 0x6d5: 82, + 0x6dd: 85, + 0x6ee: 82, + 0x6ef: 82, + 0x6fa: 68, + 0x6fb: 68, + 0x6fc: 68, + 0x6ff: 68, + 0x70f: 84, + 0x710: 82, + 0x712: 68, + 0x713: 68, + 0x714: 68, + 0x715: 82, + 0x716: 82, + 0x717: 82, + 0x718: 82, + 0x719: 82, + 0x71a: 68, + 0x71b: 68, + 0x71c: 68, + 0x71d: 68, + 0x71e: 82, + 0x71f: 68, + 0x720: 68, + 0x721: 68, + 0x722: 68, + 0x723: 68, + 0x724: 68, + 0x725: 68, + 0x726: 68, + 0x727: 68, + 0x728: 82, + 0x729: 68, + 0x72a: 82, + 0x72b: 68, + 0x72c: 82, + 0x72d: 68, + 0x72e: 68, + 0x72f: 82, + 0x74d: 82, + 0x74e: 68, + 0x74f: 68, + 0x750: 68, + 0x751: 68, + 0x752: 68, + 0x753: 68, + 0x754: 68, + 0x755: 68, + 0x756: 68, + 0x757: 68, + 0x758: 68, + 0x759: 82, + 0x75a: 82, + 0x75b: 82, + 0x75c: 68, + 0x75d: 68, + 0x75e: 68, + 0x75f: 68, + 0x760: 68, + 0x761: 68, + 0x762: 68, + 0x763: 68, + 0x764: 68, + 0x765: 68, + 0x766: 68, + 0x767: 68, + 0x768: 68, + 0x769: 68, + 0x76a: 68, + 0x76b: 82, + 0x76c: 82, + 0x76d: 68, + 0x76e: 68, + 0x76f: 68, + 0x770: 68, + 0x771: 82, + 0x772: 68, + 0x773: 82, + 0x774: 82, + 0x775: 68, + 0x776: 68, + 0x777: 68, + 0x778: 82, + 0x779: 82, + 0x77a: 68, + 0x77b: 68, + 0x77c: 68, + 0x77d: 68, + 0x77e: 68, + 0x77f: 68, + 0x7ca: 68, + 0x7cb: 68, + 0x7cc: 68, + 0x7cd: 68, + 0x7ce: 68, + 0x7cf: 68, + 0x7d0: 68, + 0x7d1: 68, + 0x7d2: 68, + 0x7d3: 68, + 0x7d4: 68, + 0x7d5: 68, + 0x7d6: 68, + 0x7d7: 68, + 0x7d8: 68, + 0x7d9: 68, + 0x7da: 68, + 0x7db: 68, + 0x7dc: 68, + 0x7dd: 68, + 0x7de: 68, + 0x7df: 68, + 0x7e0: 68, + 0x7e1: 68, + 0x7e2: 68, + 0x7e3: 68, + 0x7e4: 68, + 0x7e5: 68, + 0x7e6: 68, + 0x7e7: 68, + 0x7e8: 68, + 0x7e9: 68, + 0x7ea: 68, + 0x7fa: 67, + 0x840: 82, + 0x841: 68, + 0x842: 68, + 0x843: 68, + 0x844: 68, + 0x845: 68, + 0x846: 82, + 0x847: 82, + 0x848: 68, + 0x849: 82, + 0x84a: 68, + 0x84b: 68, + 0x84c: 68, + 0x84d: 68, + 0x84e: 68, + 0x84f: 68, + 0x850: 68, + 0x851: 68, + 0x852: 68, + 0x853: 68, + 0x854: 82, + 0x855: 68, + 0x856: 82, + 0x857: 82, + 0x858: 82, + 0x860: 68, + 0x861: 85, + 0x862: 68, + 0x863: 68, + 0x864: 68, + 0x865: 68, + 0x866: 85, + 0x867: 82, + 0x868: 68, + 0x869: 82, + 0x86a: 82, + 0x870: 82, + 0x871: 82, + 0x872: 82, + 0x873: 82, + 0x874: 82, + 0x875: 82, + 0x876: 82, + 0x877: 82, + 0x878: 82, + 0x879: 82, + 0x87a: 82, + 0x87b: 82, + 0x87c: 82, + 0x87d: 82, + 0x87e: 82, + 0x87f: 82, + 0x880: 82, + 0x881: 82, + 0x882: 82, + 0x883: 67, + 0x884: 67, + 0x885: 67, + 0x886: 68, + 0x887: 85, + 0x888: 85, + 0x889: 68, + 0x88a: 68, + 0x88b: 68, + 0x88c: 68, + 0x88d: 68, + 0x88e: 82, + 0x890: 85, + 0x891: 85, + 0x8a0: 68, + 0x8a1: 68, + 0x8a2: 68, + 0x8a3: 68, + 0x8a4: 68, + 0x8a5: 68, + 0x8a6: 68, + 0x8a7: 68, + 0x8a8: 68, + 0x8a9: 68, + 0x8aa: 82, + 0x8ab: 82, + 0x8ac: 82, + 0x8ad: 85, + 0x8ae: 82, + 0x8af: 68, + 0x8b0: 68, + 0x8b1: 82, + 0x8b2: 82, + 0x8b3: 68, + 0x8b4: 68, + 0x8b5: 68, + 0x8b6: 68, + 0x8b7: 68, + 0x8b8: 68, + 0x8b9: 82, + 0x8ba: 68, + 0x8bb: 68, + 0x8bc: 68, + 0x8bd: 68, + 0x8be: 68, + 0x8bf: 68, + 0x8c0: 68, + 0x8c1: 68, + 0x8c2: 68, + 0x8c3: 68, + 0x8c4: 68, + 0x8c5: 68, + 0x8c6: 68, + 0x8c7: 68, + 0x8c8: 68, + 0x8e2: 85, + 0x1806: 85, + 0x1807: 68, + 0x180a: 67, + 0x180e: 85, + 0x1820: 68, + 0x1821: 68, + 0x1822: 68, + 0x1823: 68, + 0x1824: 68, + 0x1825: 68, + 0x1826: 68, + 0x1827: 68, + 0x1828: 68, + 0x1829: 68, + 0x182a: 68, + 0x182b: 68, + 0x182c: 68, + 0x182d: 68, + 0x182e: 68, + 0x182f: 68, + 0x1830: 68, + 0x1831: 68, + 0x1832: 68, + 0x1833: 68, + 0x1834: 68, + 0x1835: 68, + 0x1836: 68, + 0x1837: 68, + 0x1838: 68, + 0x1839: 68, + 0x183a: 68, + 0x183b: 68, + 0x183c: 68, + 0x183d: 68, + 0x183e: 68, + 0x183f: 68, + 0x1840: 68, + 0x1841: 68, + 0x1842: 68, + 0x1843: 68, + 0x1844: 68, + 0x1845: 68, + 0x1846: 68, + 0x1847: 68, + 0x1848: 68, + 0x1849: 68, + 0x184a: 68, + 0x184b: 68, + 0x184c: 68, + 0x184d: 68, + 0x184e: 68, + 0x184f: 68, + 0x1850: 68, + 0x1851: 68, + 0x1852: 68, + 0x1853: 68, + 0x1854: 68, + 0x1855: 68, + 0x1856: 68, + 0x1857: 68, + 0x1858: 68, + 0x1859: 68, + 0x185a: 68, + 0x185b: 68, + 0x185c: 68, + 0x185d: 68, + 0x185e: 68, + 0x185f: 68, + 0x1860: 68, + 0x1861: 68, + 0x1862: 68, + 0x1863: 68, + 0x1864: 68, + 0x1865: 68, + 0x1866: 68, + 0x1867: 68, + 0x1868: 68, + 0x1869: 68, + 0x186a: 68, + 0x186b: 68, + 0x186c: 68, + 0x186d: 68, + 0x186e: 68, + 0x186f: 68, + 0x1870: 68, + 0x1871: 68, + 0x1872: 68, + 0x1873: 68, + 0x1874: 68, + 0x1875: 68, + 0x1876: 68, + 0x1877: 68, + 0x1878: 68, + 0x1880: 85, + 0x1881: 85, + 0x1882: 85, + 0x1883: 85, + 0x1884: 85, + 0x1885: 84, + 0x1886: 84, + 0x1887: 68, + 0x1888: 68, + 0x1889: 68, + 0x188a: 68, + 0x188b: 68, + 0x188c: 68, + 0x188d: 68, + 0x188e: 68, + 0x188f: 68, + 0x1890: 68, + 0x1891: 68, + 0x1892: 68, + 0x1893: 68, + 0x1894: 68, + 0x1895: 68, + 0x1896: 68, + 0x1897: 68, + 0x1898: 68, + 0x1899: 68, + 0x189a: 68, + 0x189b: 68, + 0x189c: 68, + 0x189d: 68, + 0x189e: 68, + 0x189f: 68, + 0x18a0: 68, + 0x18a1: 68, + 0x18a2: 68, + 0x18a3: 68, + 0x18a4: 68, + 0x18a5: 68, + 0x18a6: 68, + 0x18a7: 68, + 0x18a8: 68, + 0x18aa: 68, + 0x200c: 85, + 0x200d: 67, + 0x202f: 85, + 0x2066: 85, + 0x2067: 85, + 0x2068: 85, + 0x2069: 85, + 0xa840: 68, + 0xa841: 68, + 0xa842: 68, + 0xa843: 68, + 0xa844: 68, + 0xa845: 68, + 0xa846: 68, + 0xa847: 68, + 0xa848: 68, + 0xa849: 68, + 0xa84a: 68, + 0xa84b: 68, + 0xa84c: 68, + 0xa84d: 68, + 0xa84e: 68, + 0xa84f: 68, + 0xa850: 68, + 0xa851: 68, + 0xa852: 68, + 0xa853: 68, + 0xa854: 68, + 0xa855: 68, + 0xa856: 68, + 0xa857: 68, + 0xa858: 68, + 0xa859: 68, + 0xa85a: 68, + 0xa85b: 68, + 0xa85c: 68, + 0xa85d: 68, + 0xa85e: 68, + 0xa85f: 68, + 0xa860: 68, + 0xa861: 68, + 0xa862: 68, + 0xa863: 68, + 0xa864: 68, + 0xa865: 68, + 0xa866: 68, + 0xa867: 68, + 0xa868: 68, + 0xa869: 68, + 0xa86a: 68, + 0xa86b: 68, + 0xa86c: 68, + 0xa86d: 68, + 0xa86e: 68, + 0xa86f: 68, + 0xa870: 68, + 0xa871: 68, + 0xa872: 76, + 0xa873: 85, + 0x10ac0: 68, + 0x10ac1: 68, + 0x10ac2: 68, + 0x10ac3: 68, + 0x10ac4: 68, + 0x10ac5: 82, + 0x10ac6: 85, + 0x10ac7: 82, + 0x10ac8: 85, + 0x10ac9: 82, + 0x10aca: 82, + 0x10acb: 85, + 0x10acc: 85, + 0x10acd: 76, + 0x10ace: 82, + 0x10acf: 82, + 0x10ad0: 82, + 0x10ad1: 82, + 0x10ad2: 82, + 0x10ad3: 68, + 0x10ad4: 68, + 0x10ad5: 68, + 0x10ad6: 68, + 0x10ad7: 76, + 0x10ad8: 68, + 0x10ad9: 68, + 0x10ada: 68, + 0x10adb: 68, + 0x10adc: 68, + 0x10add: 82, + 0x10ade: 68, + 0x10adf: 68, + 0x10ae0: 68, + 0x10ae1: 82, + 0x10ae2: 85, + 0x10ae3: 85, + 0x10ae4: 82, + 0x10aeb: 68, + 0x10aec: 68, + 0x10aed: 68, + 0x10aee: 68, + 0x10aef: 82, + 0x10b80: 68, + 0x10b81: 82, + 0x10b82: 68, + 0x10b83: 82, + 0x10b84: 82, + 0x10b85: 82, + 0x10b86: 68, + 0x10b87: 68, + 0x10b88: 68, + 0x10b89: 82, + 0x10b8a: 68, + 0x10b8b: 68, + 0x10b8c: 82, + 0x10b8d: 68, + 0x10b8e: 82, + 0x10b8f: 82, + 0x10b90: 68, + 0x10b91: 82, + 0x10ba9: 82, + 0x10baa: 82, + 0x10bab: 82, + 0x10bac: 82, + 0x10bad: 68, + 0x10bae: 68, + 0x10baf: 85, + 0x10d00: 76, + 0x10d01: 68, + 0x10d02: 68, + 0x10d03: 68, + 0x10d04: 68, + 0x10d05: 68, + 0x10d06: 68, + 0x10d07: 68, + 0x10d08: 68, + 0x10d09: 68, + 0x10d0a: 68, + 0x10d0b: 68, + 0x10d0c: 68, + 0x10d0d: 68, + 0x10d0e: 68, + 0x10d0f: 68, + 0x10d10: 68, + 0x10d11: 68, + 0x10d12: 68, + 0x10d13: 68, + 0x10d14: 68, + 0x10d15: 68, + 0x10d16: 68, + 0x10d17: 68, + 0x10d18: 68, + 0x10d19: 68, + 0x10d1a: 68, + 0x10d1b: 68, + 0x10d1c: 68, + 0x10d1d: 68, + 0x10d1e: 68, + 0x10d1f: 68, + 0x10d20: 68, + 0x10d21: 68, + 0x10d22: 82, + 0x10d23: 68, + 0x10f30: 68, + 0x10f31: 68, + 0x10f32: 68, + 0x10f33: 82, + 0x10f34: 68, + 0x10f35: 68, + 0x10f36: 68, + 0x10f37: 68, + 0x10f38: 68, + 0x10f39: 68, + 0x10f3a: 68, + 0x10f3b: 68, + 0x10f3c: 68, + 0x10f3d: 68, + 0x10f3e: 68, + 0x10f3f: 68, + 0x10f40: 68, + 0x10f41: 68, + 0x10f42: 68, + 0x10f43: 68, + 0x10f44: 68, + 0x10f45: 85, + 0x10f51: 68, + 0x10f52: 68, + 0x10f53: 68, + 0x10f54: 82, + 0x10f70: 68, + 0x10f71: 68, + 0x10f72: 68, + 0x10f73: 68, + 0x10f74: 82, + 0x10f75: 82, + 0x10f76: 68, + 0x10f77: 68, + 0x10f78: 68, + 0x10f79: 68, + 0x10f7a: 68, + 0x10f7b: 68, + 0x10f7c: 68, + 0x10f7d: 68, + 0x10f7e: 68, + 0x10f7f: 68, + 0x10f80: 68, + 0x10f81: 68, + 0x10fb0: 68, + 0x10fb1: 85, + 0x10fb2: 68, + 0x10fb3: 68, + 0x10fb4: 82, + 0x10fb5: 82, + 0x10fb6: 82, + 0x10fb7: 85, + 0x10fb8: 68, + 0x10fb9: 82, + 0x10fba: 82, + 0x10fbb: 68, + 0x10fbc: 68, + 0x10fbd: 82, + 0x10fbe: 68, + 0x10fbf: 68, + 0x10fc0: 85, + 0x10fc1: 68, + 0x10fc2: 82, + 0x10fc3: 82, + 0x10fc4: 68, + 0x10fc5: 85, + 0x10fc6: 85, + 0x10fc7: 85, + 0x10fc8: 85, + 0x10fc9: 82, + 0x10fca: 68, + 0x10fcb: 76, + 0x110bd: 85, + 0x110cd: 85, + 0x1e900: 68, + 0x1e901: 68, + 0x1e902: 68, + 0x1e903: 68, + 0x1e904: 68, + 0x1e905: 68, + 0x1e906: 68, + 0x1e907: 68, + 0x1e908: 68, + 0x1e909: 68, + 0x1e90a: 68, + 0x1e90b: 68, + 0x1e90c: 68, + 0x1e90d: 68, + 0x1e90e: 68, + 0x1e90f: 68, + 0x1e910: 68, + 0x1e911: 68, + 0x1e912: 68, + 0x1e913: 68, + 0x1e914: 68, + 0x1e915: 68, + 0x1e916: 68, + 0x1e917: 68, + 0x1e918: 68, + 0x1e919: 68, + 0x1e91a: 68, + 0x1e91b: 68, + 0x1e91c: 68, + 0x1e91d: 68, + 0x1e91e: 68, + 0x1e91f: 68, + 0x1e920: 68, + 0x1e921: 68, + 0x1e922: 68, + 0x1e923: 68, + 0x1e924: 68, + 0x1e925: 68, + 0x1e926: 68, + 0x1e927: 68, + 0x1e928: 68, + 0x1e929: 68, + 0x1e92a: 68, + 0x1e92b: 68, + 0x1e92c: 68, + 0x1e92d: 68, + 0x1e92e: 68, + 0x1e92f: 68, + 0x1e930: 68, + 0x1e931: 68, + 0x1e932: 68, + 0x1e933: 68, + 0x1e934: 68, + 0x1e935: 68, + 0x1e936: 68, + 0x1e937: 68, + 0x1e938: 68, + 0x1e939: 68, + 0x1e93a: 68, + 0x1e93b: 68, + 0x1e93c: 68, + 0x1e93d: 68, + 0x1e93e: 68, + 0x1e93f: 68, + 0x1e940: 68, + 0x1e941: 68, + 0x1e942: 68, + 0x1e943: 68, + 0x1e94b: 84, +} +codepoint_classes = { + 'PVALID': ( + 0x2d0000002e, + 0x300000003a, + 0x610000007b, + 0xdf000000f7, + 0xf800000100, + 0x10100000102, + 0x10300000104, + 0x10500000106, + 0x10700000108, + 0x1090000010a, + 0x10b0000010c, + 0x10d0000010e, + 0x10f00000110, + 0x11100000112, + 0x11300000114, + 0x11500000116, + 0x11700000118, + 0x1190000011a, + 0x11b0000011c, + 0x11d0000011e, + 0x11f00000120, + 0x12100000122, + 0x12300000124, + 0x12500000126, + 0x12700000128, + 0x1290000012a, + 0x12b0000012c, + 0x12d0000012e, + 0x12f00000130, + 0x13100000132, + 0x13500000136, + 0x13700000139, + 0x13a0000013b, + 0x13c0000013d, + 0x13e0000013f, + 0x14200000143, + 0x14400000145, + 0x14600000147, + 0x14800000149, + 0x14b0000014c, + 0x14d0000014e, + 0x14f00000150, + 0x15100000152, + 0x15300000154, + 0x15500000156, + 0x15700000158, + 0x1590000015a, + 0x15b0000015c, + 0x15d0000015e, + 0x15f00000160, + 0x16100000162, + 0x16300000164, + 0x16500000166, + 0x16700000168, + 0x1690000016a, + 0x16b0000016c, + 0x16d0000016e, + 0x16f00000170, + 0x17100000172, + 0x17300000174, + 0x17500000176, + 0x17700000178, + 0x17a0000017b, + 0x17c0000017d, + 0x17e0000017f, + 0x18000000181, + 0x18300000184, + 0x18500000186, + 0x18800000189, + 0x18c0000018e, + 0x19200000193, + 0x19500000196, + 0x1990000019c, + 0x19e0000019f, + 0x1a1000001a2, + 0x1a3000001a4, + 0x1a5000001a6, + 0x1a8000001a9, + 0x1aa000001ac, + 0x1ad000001ae, + 0x1b0000001b1, + 0x1b4000001b5, + 0x1b6000001b7, + 0x1b9000001bc, + 0x1bd000001c4, + 0x1ce000001cf, + 0x1d0000001d1, + 0x1d2000001d3, + 0x1d4000001d5, + 0x1d6000001d7, + 0x1d8000001d9, + 0x1da000001db, + 0x1dc000001de, + 0x1df000001e0, + 0x1e1000001e2, + 0x1e3000001e4, + 0x1e5000001e6, + 0x1e7000001e8, + 0x1e9000001ea, + 0x1eb000001ec, + 0x1ed000001ee, + 0x1ef000001f1, + 0x1f5000001f6, + 0x1f9000001fa, + 0x1fb000001fc, + 0x1fd000001fe, + 0x1ff00000200, + 0x20100000202, + 0x20300000204, + 0x20500000206, + 0x20700000208, + 0x2090000020a, + 0x20b0000020c, + 0x20d0000020e, + 0x20f00000210, + 0x21100000212, + 0x21300000214, + 0x21500000216, + 0x21700000218, + 0x2190000021a, + 0x21b0000021c, + 0x21d0000021e, + 0x21f00000220, + 0x22100000222, + 0x22300000224, + 0x22500000226, + 0x22700000228, + 0x2290000022a, + 0x22b0000022c, + 0x22d0000022e, + 0x22f00000230, + 0x23100000232, + 0x2330000023a, + 0x23c0000023d, + 0x23f00000241, + 0x24200000243, + 0x24700000248, + 0x2490000024a, + 0x24b0000024c, + 0x24d0000024e, + 0x24f000002b0, + 0x2b9000002c2, + 0x2c6000002d2, + 0x2ec000002ed, + 0x2ee000002ef, + 0x30000000340, + 0x34200000343, + 0x3460000034f, + 0x35000000370, + 0x37100000372, + 0x37300000374, + 0x37700000378, + 0x37b0000037e, + 0x39000000391, + 0x3ac000003cf, + 0x3d7000003d8, + 0x3d9000003da, + 0x3db000003dc, + 0x3dd000003de, + 0x3df000003e0, + 0x3e1000003e2, + 0x3e3000003e4, + 0x3e5000003e6, + 0x3e7000003e8, + 0x3e9000003ea, + 0x3eb000003ec, + 0x3ed000003ee, + 0x3ef000003f0, + 0x3f3000003f4, + 0x3f8000003f9, + 0x3fb000003fd, + 0x43000000460, + 0x46100000462, + 0x46300000464, + 0x46500000466, + 0x46700000468, + 0x4690000046a, + 0x46b0000046c, + 0x46d0000046e, + 0x46f00000470, + 0x47100000472, + 0x47300000474, + 0x47500000476, + 0x47700000478, + 0x4790000047a, + 0x47b0000047c, + 0x47d0000047e, + 0x47f00000480, + 0x48100000482, + 0x48300000488, + 0x48b0000048c, + 0x48d0000048e, + 0x48f00000490, + 0x49100000492, + 0x49300000494, + 0x49500000496, + 0x49700000498, + 0x4990000049a, + 0x49b0000049c, + 0x49d0000049e, + 0x49f000004a0, + 0x4a1000004a2, + 0x4a3000004a4, + 0x4a5000004a6, + 0x4a7000004a8, + 0x4a9000004aa, + 0x4ab000004ac, + 0x4ad000004ae, + 0x4af000004b0, + 0x4b1000004b2, + 0x4b3000004b4, + 0x4b5000004b6, + 0x4b7000004b8, + 0x4b9000004ba, + 0x4bb000004bc, + 0x4bd000004be, + 0x4bf000004c0, + 0x4c2000004c3, + 0x4c4000004c5, + 0x4c6000004c7, + 0x4c8000004c9, + 0x4ca000004cb, + 0x4cc000004cd, + 0x4ce000004d0, + 0x4d1000004d2, + 0x4d3000004d4, + 0x4d5000004d6, + 0x4d7000004d8, + 0x4d9000004da, + 0x4db000004dc, + 0x4dd000004de, + 0x4df000004e0, + 0x4e1000004e2, + 0x4e3000004e4, + 0x4e5000004e6, + 0x4e7000004e8, + 0x4e9000004ea, + 0x4eb000004ec, + 0x4ed000004ee, + 0x4ef000004f0, + 0x4f1000004f2, + 0x4f3000004f4, + 0x4f5000004f6, + 0x4f7000004f8, + 0x4f9000004fa, + 0x4fb000004fc, + 0x4fd000004fe, + 0x4ff00000500, + 0x50100000502, + 0x50300000504, + 0x50500000506, + 0x50700000508, + 0x5090000050a, + 0x50b0000050c, + 0x50d0000050e, + 0x50f00000510, + 0x51100000512, + 0x51300000514, + 0x51500000516, + 0x51700000518, + 0x5190000051a, + 0x51b0000051c, + 0x51d0000051e, + 0x51f00000520, + 0x52100000522, + 0x52300000524, + 0x52500000526, + 0x52700000528, + 0x5290000052a, + 0x52b0000052c, + 0x52d0000052e, + 0x52f00000530, + 0x5590000055a, + 0x56000000587, + 0x58800000589, + 0x591000005be, + 0x5bf000005c0, + 0x5c1000005c3, + 0x5c4000005c6, + 0x5c7000005c8, + 0x5d0000005eb, + 0x5ef000005f3, + 0x6100000061b, + 0x62000000640, + 0x64100000660, + 0x66e00000675, + 0x679000006d4, + 0x6d5000006dd, + 0x6df000006e9, + 0x6ea000006f0, + 0x6fa00000700, + 0x7100000074b, + 0x74d000007b2, + 0x7c0000007f6, + 0x7fd000007fe, + 0x8000000082e, + 0x8400000085c, + 0x8600000086b, + 0x87000000888, + 0x8890000088f, + 0x898000008e2, + 0x8e300000958, + 0x96000000964, + 0x96600000970, + 0x97100000984, + 0x9850000098d, + 0x98f00000991, + 0x993000009a9, + 0x9aa000009b1, + 0x9b2000009b3, + 0x9b6000009ba, + 0x9bc000009c5, + 0x9c7000009c9, + 0x9cb000009cf, + 0x9d7000009d8, + 0x9e0000009e4, + 0x9e6000009f2, + 0x9fc000009fd, + 0x9fe000009ff, + 0xa0100000a04, + 0xa0500000a0b, + 0xa0f00000a11, + 0xa1300000a29, + 0xa2a00000a31, + 0xa3200000a33, + 0xa3500000a36, + 0xa3800000a3a, + 0xa3c00000a3d, + 0xa3e00000a43, + 0xa4700000a49, + 0xa4b00000a4e, + 0xa5100000a52, + 0xa5c00000a5d, + 0xa6600000a76, + 0xa8100000a84, + 0xa8500000a8e, + 0xa8f00000a92, + 0xa9300000aa9, + 0xaaa00000ab1, + 0xab200000ab4, + 0xab500000aba, + 0xabc00000ac6, + 0xac700000aca, + 0xacb00000ace, + 0xad000000ad1, + 0xae000000ae4, + 0xae600000af0, + 0xaf900000b00, + 0xb0100000b04, + 0xb0500000b0d, + 0xb0f00000b11, + 0xb1300000b29, + 0xb2a00000b31, + 0xb3200000b34, + 0xb3500000b3a, + 0xb3c00000b45, + 0xb4700000b49, + 0xb4b00000b4e, + 0xb5500000b58, + 0xb5f00000b64, + 0xb6600000b70, + 0xb7100000b72, + 0xb8200000b84, + 0xb8500000b8b, + 0xb8e00000b91, + 0xb9200000b96, + 0xb9900000b9b, + 0xb9c00000b9d, + 0xb9e00000ba0, + 0xba300000ba5, + 0xba800000bab, + 0xbae00000bba, + 0xbbe00000bc3, + 0xbc600000bc9, + 0xbca00000bce, + 0xbd000000bd1, + 0xbd700000bd8, + 0xbe600000bf0, + 0xc0000000c0d, + 0xc0e00000c11, + 0xc1200000c29, + 0xc2a00000c3a, + 0xc3c00000c45, + 0xc4600000c49, + 0xc4a00000c4e, + 0xc5500000c57, + 0xc5800000c5b, + 0xc5d00000c5e, + 0xc6000000c64, + 0xc6600000c70, + 0xc8000000c84, + 0xc8500000c8d, + 0xc8e00000c91, + 0xc9200000ca9, + 0xcaa00000cb4, + 0xcb500000cba, + 0xcbc00000cc5, + 0xcc600000cc9, + 0xcca00000cce, + 0xcd500000cd7, + 0xcdd00000cdf, + 0xce000000ce4, + 0xce600000cf0, + 0xcf100000cf4, + 0xd0000000d0d, + 0xd0e00000d11, + 0xd1200000d45, + 0xd4600000d49, + 0xd4a00000d4f, + 0xd5400000d58, + 0xd5f00000d64, + 0xd6600000d70, + 0xd7a00000d80, + 0xd8100000d84, + 0xd8500000d97, + 0xd9a00000db2, + 0xdb300000dbc, + 0xdbd00000dbe, + 0xdc000000dc7, + 0xdca00000dcb, + 0xdcf00000dd5, + 0xdd600000dd7, + 0xdd800000de0, + 0xde600000df0, + 0xdf200000df4, + 0xe0100000e33, + 0xe3400000e3b, + 0xe4000000e4f, + 0xe5000000e5a, + 0xe8100000e83, + 0xe8400000e85, + 0xe8600000e8b, + 0xe8c00000ea4, + 0xea500000ea6, + 0xea700000eb3, + 0xeb400000ebe, + 0xec000000ec5, + 0xec600000ec7, + 0xec800000ecf, + 0xed000000eda, + 0xede00000ee0, + 0xf0000000f01, + 0xf0b00000f0c, + 0xf1800000f1a, + 0xf2000000f2a, + 0xf3500000f36, + 0xf3700000f38, + 0xf3900000f3a, + 0xf3e00000f43, + 0xf4400000f48, + 0xf4900000f4d, + 0xf4e00000f52, + 0xf5300000f57, + 0xf5800000f5c, + 0xf5d00000f69, + 0xf6a00000f6d, + 0xf7100000f73, + 0xf7400000f75, + 0xf7a00000f81, + 0xf8200000f85, + 0xf8600000f93, + 0xf9400000f98, + 0xf9900000f9d, + 0xf9e00000fa2, + 0xfa300000fa7, + 0xfa800000fac, + 0xfad00000fb9, + 0xfba00000fbd, + 0xfc600000fc7, + 0x10000000104a, + 0x10500000109e, + 0x10d0000010fb, + 0x10fd00001100, + 0x120000001249, + 0x124a0000124e, + 0x125000001257, + 0x125800001259, + 0x125a0000125e, + 0x126000001289, + 0x128a0000128e, + 0x1290000012b1, + 0x12b2000012b6, + 0x12b8000012bf, + 0x12c0000012c1, + 0x12c2000012c6, + 0x12c8000012d7, + 0x12d800001311, + 0x131200001316, + 0x13180000135b, + 0x135d00001360, + 0x138000001390, + 0x13a0000013f6, + 0x14010000166d, + 0x166f00001680, + 0x16810000169b, + 0x16a0000016eb, + 0x16f1000016f9, + 0x170000001716, + 0x171f00001735, + 0x174000001754, + 0x17600000176d, + 0x176e00001771, + 0x177200001774, + 0x1780000017b4, + 0x17b6000017d4, + 0x17d7000017d8, + 0x17dc000017de, + 0x17e0000017ea, + 0x18100000181a, + 0x182000001879, + 0x1880000018ab, + 0x18b0000018f6, + 0x19000000191f, + 0x19200000192c, + 0x19300000193c, + 0x19460000196e, + 0x197000001975, + 0x1980000019ac, + 0x19b0000019ca, + 0x19d0000019da, + 0x1a0000001a1c, + 0x1a2000001a5f, + 0x1a6000001a7d, + 0x1a7f00001a8a, + 0x1a9000001a9a, + 0x1aa700001aa8, + 0x1ab000001abe, + 0x1abf00001acf, + 0x1b0000001b4d, + 0x1b5000001b5a, + 0x1b6b00001b74, + 0x1b8000001bf4, + 0x1c0000001c38, + 0x1c4000001c4a, + 0x1c4d00001c7e, + 0x1cd000001cd3, + 0x1cd400001cfb, + 0x1d0000001d2c, + 0x1d2f00001d30, + 0x1d3b00001d3c, + 0x1d4e00001d4f, + 0x1d6b00001d78, + 0x1d7900001d9b, + 0x1dc000001e00, + 0x1e0100001e02, + 0x1e0300001e04, + 0x1e0500001e06, + 0x1e0700001e08, + 0x1e0900001e0a, + 0x1e0b00001e0c, + 0x1e0d00001e0e, + 0x1e0f00001e10, + 0x1e1100001e12, + 0x1e1300001e14, + 0x1e1500001e16, + 0x1e1700001e18, + 0x1e1900001e1a, + 0x1e1b00001e1c, + 0x1e1d00001e1e, + 0x1e1f00001e20, + 0x1e2100001e22, + 0x1e2300001e24, + 0x1e2500001e26, + 0x1e2700001e28, + 0x1e2900001e2a, + 0x1e2b00001e2c, + 0x1e2d00001e2e, + 0x1e2f00001e30, + 0x1e3100001e32, + 0x1e3300001e34, + 0x1e3500001e36, + 0x1e3700001e38, + 0x1e3900001e3a, + 0x1e3b00001e3c, + 0x1e3d00001e3e, + 0x1e3f00001e40, + 0x1e4100001e42, + 0x1e4300001e44, + 0x1e4500001e46, + 0x1e4700001e48, + 0x1e4900001e4a, + 0x1e4b00001e4c, + 0x1e4d00001e4e, + 0x1e4f00001e50, + 0x1e5100001e52, + 0x1e5300001e54, + 0x1e5500001e56, + 0x1e5700001e58, + 0x1e5900001e5a, + 0x1e5b00001e5c, + 0x1e5d00001e5e, + 0x1e5f00001e60, + 0x1e6100001e62, + 0x1e6300001e64, + 0x1e6500001e66, + 0x1e6700001e68, + 0x1e6900001e6a, + 0x1e6b00001e6c, + 0x1e6d00001e6e, + 0x1e6f00001e70, + 0x1e7100001e72, + 0x1e7300001e74, + 0x1e7500001e76, + 0x1e7700001e78, + 0x1e7900001e7a, + 0x1e7b00001e7c, + 0x1e7d00001e7e, + 0x1e7f00001e80, + 0x1e8100001e82, + 0x1e8300001e84, + 0x1e8500001e86, + 0x1e8700001e88, + 0x1e8900001e8a, + 0x1e8b00001e8c, + 0x1e8d00001e8e, + 0x1e8f00001e90, + 0x1e9100001e92, + 0x1e9300001e94, + 0x1e9500001e9a, + 0x1e9c00001e9e, + 0x1e9f00001ea0, + 0x1ea100001ea2, + 0x1ea300001ea4, + 0x1ea500001ea6, + 0x1ea700001ea8, + 0x1ea900001eaa, + 0x1eab00001eac, + 0x1ead00001eae, + 0x1eaf00001eb0, + 0x1eb100001eb2, + 0x1eb300001eb4, + 0x1eb500001eb6, + 0x1eb700001eb8, + 0x1eb900001eba, + 0x1ebb00001ebc, + 0x1ebd00001ebe, + 0x1ebf00001ec0, + 0x1ec100001ec2, + 0x1ec300001ec4, + 0x1ec500001ec6, + 0x1ec700001ec8, + 0x1ec900001eca, + 0x1ecb00001ecc, + 0x1ecd00001ece, + 0x1ecf00001ed0, + 0x1ed100001ed2, + 0x1ed300001ed4, + 0x1ed500001ed6, + 0x1ed700001ed8, + 0x1ed900001eda, + 0x1edb00001edc, + 0x1edd00001ede, + 0x1edf00001ee0, + 0x1ee100001ee2, + 0x1ee300001ee4, + 0x1ee500001ee6, + 0x1ee700001ee8, + 0x1ee900001eea, + 0x1eeb00001eec, + 0x1eed00001eee, + 0x1eef00001ef0, + 0x1ef100001ef2, + 0x1ef300001ef4, + 0x1ef500001ef6, + 0x1ef700001ef8, + 0x1ef900001efa, + 0x1efb00001efc, + 0x1efd00001efe, + 0x1eff00001f08, + 0x1f1000001f16, + 0x1f2000001f28, + 0x1f3000001f38, + 0x1f4000001f46, + 0x1f5000001f58, + 0x1f6000001f68, + 0x1f7000001f71, + 0x1f7200001f73, + 0x1f7400001f75, + 0x1f7600001f77, + 0x1f7800001f79, + 0x1f7a00001f7b, + 0x1f7c00001f7d, + 0x1fb000001fb2, + 0x1fb600001fb7, + 0x1fc600001fc7, + 0x1fd000001fd3, + 0x1fd600001fd8, + 0x1fe000001fe3, + 0x1fe400001fe8, + 0x1ff600001ff7, + 0x214e0000214f, + 0x218400002185, + 0x2c3000002c60, + 0x2c6100002c62, + 0x2c6500002c67, + 0x2c6800002c69, + 0x2c6a00002c6b, + 0x2c6c00002c6d, + 0x2c7100002c72, + 0x2c7300002c75, + 0x2c7600002c7c, + 0x2c8100002c82, + 0x2c8300002c84, + 0x2c8500002c86, + 0x2c8700002c88, + 0x2c8900002c8a, + 0x2c8b00002c8c, + 0x2c8d00002c8e, + 0x2c8f00002c90, + 0x2c9100002c92, + 0x2c9300002c94, + 0x2c9500002c96, + 0x2c9700002c98, + 0x2c9900002c9a, + 0x2c9b00002c9c, + 0x2c9d00002c9e, + 0x2c9f00002ca0, + 0x2ca100002ca2, + 0x2ca300002ca4, + 0x2ca500002ca6, + 0x2ca700002ca8, + 0x2ca900002caa, + 0x2cab00002cac, + 0x2cad00002cae, + 0x2caf00002cb0, + 0x2cb100002cb2, + 0x2cb300002cb4, + 0x2cb500002cb6, + 0x2cb700002cb8, + 0x2cb900002cba, + 0x2cbb00002cbc, + 0x2cbd00002cbe, + 0x2cbf00002cc0, + 0x2cc100002cc2, + 0x2cc300002cc4, + 0x2cc500002cc6, + 0x2cc700002cc8, + 0x2cc900002cca, + 0x2ccb00002ccc, + 0x2ccd00002cce, + 0x2ccf00002cd0, + 0x2cd100002cd2, + 0x2cd300002cd4, + 0x2cd500002cd6, + 0x2cd700002cd8, + 0x2cd900002cda, + 0x2cdb00002cdc, + 0x2cdd00002cde, + 0x2cdf00002ce0, + 0x2ce100002ce2, + 0x2ce300002ce5, + 0x2cec00002ced, + 0x2cee00002cf2, + 0x2cf300002cf4, + 0x2d0000002d26, + 0x2d2700002d28, + 0x2d2d00002d2e, + 0x2d3000002d68, + 0x2d7f00002d97, + 0x2da000002da7, + 0x2da800002daf, + 0x2db000002db7, + 0x2db800002dbf, + 0x2dc000002dc7, + 0x2dc800002dcf, + 0x2dd000002dd7, + 0x2dd800002ddf, + 0x2de000002e00, + 0x2e2f00002e30, + 0x300500003008, + 0x302a0000302e, + 0x303c0000303d, + 0x304100003097, + 0x30990000309b, + 0x309d0000309f, + 0x30a1000030fb, + 0x30fc000030ff, + 0x310500003130, + 0x31a0000031c0, + 0x31f000003200, + 0x340000004dc0, + 0x4e000000a48d, + 0xa4d00000a4fe, + 0xa5000000a60d, + 0xa6100000a62c, + 0xa6410000a642, + 0xa6430000a644, + 0xa6450000a646, + 0xa6470000a648, + 0xa6490000a64a, + 0xa64b0000a64c, + 0xa64d0000a64e, + 0xa64f0000a650, + 0xa6510000a652, + 0xa6530000a654, + 0xa6550000a656, + 0xa6570000a658, + 0xa6590000a65a, + 0xa65b0000a65c, + 0xa65d0000a65e, + 0xa65f0000a660, + 0xa6610000a662, + 0xa6630000a664, + 0xa6650000a666, + 0xa6670000a668, + 0xa6690000a66a, + 0xa66b0000a66c, + 0xa66d0000a670, + 0xa6740000a67e, + 0xa67f0000a680, + 0xa6810000a682, + 0xa6830000a684, + 0xa6850000a686, + 0xa6870000a688, + 0xa6890000a68a, + 0xa68b0000a68c, + 0xa68d0000a68e, + 0xa68f0000a690, + 0xa6910000a692, + 0xa6930000a694, + 0xa6950000a696, + 0xa6970000a698, + 0xa6990000a69a, + 0xa69b0000a69c, + 0xa69e0000a6e6, + 0xa6f00000a6f2, + 0xa7170000a720, + 0xa7230000a724, + 0xa7250000a726, + 0xa7270000a728, + 0xa7290000a72a, + 0xa72b0000a72c, + 0xa72d0000a72e, + 0xa72f0000a732, + 0xa7330000a734, + 0xa7350000a736, + 0xa7370000a738, + 0xa7390000a73a, + 0xa73b0000a73c, + 0xa73d0000a73e, + 0xa73f0000a740, + 0xa7410000a742, + 0xa7430000a744, + 0xa7450000a746, + 0xa7470000a748, + 0xa7490000a74a, + 0xa74b0000a74c, + 0xa74d0000a74e, + 0xa74f0000a750, + 0xa7510000a752, + 0xa7530000a754, + 0xa7550000a756, + 0xa7570000a758, + 0xa7590000a75a, + 0xa75b0000a75c, + 0xa75d0000a75e, + 0xa75f0000a760, + 0xa7610000a762, + 0xa7630000a764, + 0xa7650000a766, + 0xa7670000a768, + 0xa7690000a76a, + 0xa76b0000a76c, + 0xa76d0000a76e, + 0xa76f0000a770, + 0xa7710000a779, + 0xa77a0000a77b, + 0xa77c0000a77d, + 0xa77f0000a780, + 0xa7810000a782, + 0xa7830000a784, + 0xa7850000a786, + 0xa7870000a789, + 0xa78c0000a78d, + 0xa78e0000a790, + 0xa7910000a792, + 0xa7930000a796, + 0xa7970000a798, + 0xa7990000a79a, + 0xa79b0000a79c, + 0xa79d0000a79e, + 0xa79f0000a7a0, + 0xa7a10000a7a2, + 0xa7a30000a7a4, + 0xa7a50000a7a6, + 0xa7a70000a7a8, + 0xa7a90000a7aa, + 0xa7af0000a7b0, + 0xa7b50000a7b6, + 0xa7b70000a7b8, + 0xa7b90000a7ba, + 0xa7bb0000a7bc, + 0xa7bd0000a7be, + 0xa7bf0000a7c0, + 0xa7c10000a7c2, + 0xa7c30000a7c4, + 0xa7c80000a7c9, + 0xa7ca0000a7cb, + 0xa7d10000a7d2, + 0xa7d30000a7d4, + 0xa7d50000a7d6, + 0xa7d70000a7d8, + 0xa7d90000a7da, + 0xa7f60000a7f8, + 0xa7fa0000a828, + 0xa82c0000a82d, + 0xa8400000a874, + 0xa8800000a8c6, + 0xa8d00000a8da, + 0xa8e00000a8f8, + 0xa8fb0000a8fc, + 0xa8fd0000a92e, + 0xa9300000a954, + 0xa9800000a9c1, + 0xa9cf0000a9da, + 0xa9e00000a9ff, + 0xaa000000aa37, + 0xaa400000aa4e, + 0xaa500000aa5a, + 0xaa600000aa77, + 0xaa7a0000aac3, + 0xaadb0000aade, + 0xaae00000aaf0, + 0xaaf20000aaf7, + 0xab010000ab07, + 0xab090000ab0f, + 0xab110000ab17, + 0xab200000ab27, + 0xab280000ab2f, + 0xab300000ab5b, + 0xab600000ab69, + 0xabc00000abeb, + 0xabec0000abee, + 0xabf00000abfa, + 0xac000000d7a4, + 0xfa0e0000fa10, + 0xfa110000fa12, + 0xfa130000fa15, + 0xfa1f0000fa20, + 0xfa210000fa22, + 0xfa230000fa25, + 0xfa270000fa2a, + 0xfb1e0000fb1f, + 0xfe200000fe30, + 0xfe730000fe74, + 0x100000001000c, + 0x1000d00010027, + 0x100280001003b, + 0x1003c0001003e, + 0x1003f0001004e, + 0x100500001005e, + 0x10080000100fb, + 0x101fd000101fe, + 0x102800001029d, + 0x102a0000102d1, + 0x102e0000102e1, + 0x1030000010320, + 0x1032d00010341, + 0x103420001034a, + 0x103500001037b, + 0x103800001039e, + 0x103a0000103c4, + 0x103c8000103d0, + 0x104280001049e, + 0x104a0000104aa, + 0x104d8000104fc, + 0x1050000010528, + 0x1053000010564, + 0x10597000105a2, + 0x105a3000105b2, + 0x105b3000105ba, + 0x105bb000105bd, + 0x1060000010737, + 0x1074000010756, + 0x1076000010768, + 0x1078000010781, + 0x1080000010806, + 0x1080800010809, + 0x1080a00010836, + 0x1083700010839, + 0x1083c0001083d, + 0x1083f00010856, + 0x1086000010877, + 0x108800001089f, + 0x108e0000108f3, + 0x108f4000108f6, + 0x1090000010916, + 0x109200001093a, + 0x10980000109b8, + 0x109be000109c0, + 0x10a0000010a04, + 0x10a0500010a07, + 0x10a0c00010a14, + 0x10a1500010a18, + 0x10a1900010a36, + 0x10a3800010a3b, + 0x10a3f00010a40, + 0x10a6000010a7d, + 0x10a8000010a9d, + 0x10ac000010ac8, + 0x10ac900010ae7, + 0x10b0000010b36, + 0x10b4000010b56, + 0x10b6000010b73, + 0x10b8000010b92, + 0x10c0000010c49, + 0x10cc000010cf3, + 0x10d0000010d28, + 0x10d3000010d3a, + 0x10e8000010eaa, + 0x10eab00010ead, + 0x10eb000010eb2, + 0x10efd00010f1d, + 0x10f2700010f28, + 0x10f3000010f51, + 0x10f7000010f86, + 0x10fb000010fc5, + 0x10fe000010ff7, + 0x1100000011047, + 0x1106600011076, + 0x1107f000110bb, + 0x110c2000110c3, + 0x110d0000110e9, + 0x110f0000110fa, + 0x1110000011135, + 0x1113600011140, + 0x1114400011148, + 0x1115000011174, + 0x1117600011177, + 0x11180000111c5, + 0x111c9000111cd, + 0x111ce000111db, + 0x111dc000111dd, + 0x1120000011212, + 0x1121300011238, + 0x1123e00011242, + 0x1128000011287, + 0x1128800011289, + 0x1128a0001128e, + 0x1128f0001129e, + 0x1129f000112a9, + 0x112b0000112eb, + 0x112f0000112fa, + 0x1130000011304, + 0x113050001130d, + 0x1130f00011311, + 0x1131300011329, + 0x1132a00011331, + 0x1133200011334, + 0x113350001133a, + 0x1133b00011345, + 0x1134700011349, + 0x1134b0001134e, + 0x1135000011351, + 0x1135700011358, + 0x1135d00011364, + 0x113660001136d, + 0x1137000011375, + 0x114000001144b, + 0x114500001145a, + 0x1145e00011462, + 0x11480000114c6, + 0x114c7000114c8, + 0x114d0000114da, + 0x11580000115b6, + 0x115b8000115c1, + 0x115d8000115de, + 0x1160000011641, + 0x1164400011645, + 0x116500001165a, + 0x11680000116b9, + 0x116c0000116ca, + 0x117000001171b, + 0x1171d0001172c, + 0x117300001173a, + 0x1174000011747, + 0x118000001183b, + 0x118c0000118ea, + 0x118ff00011907, + 0x119090001190a, + 0x1190c00011914, + 0x1191500011917, + 0x1191800011936, + 0x1193700011939, + 0x1193b00011944, + 0x119500001195a, + 0x119a0000119a8, + 0x119aa000119d8, + 0x119da000119e2, + 0x119e3000119e5, + 0x11a0000011a3f, + 0x11a4700011a48, + 0x11a5000011a9a, + 0x11a9d00011a9e, + 0x11ab000011af9, + 0x11c0000011c09, + 0x11c0a00011c37, + 0x11c3800011c41, + 0x11c5000011c5a, + 0x11c7200011c90, + 0x11c9200011ca8, + 0x11ca900011cb7, + 0x11d0000011d07, + 0x11d0800011d0a, + 0x11d0b00011d37, + 0x11d3a00011d3b, + 0x11d3c00011d3e, + 0x11d3f00011d48, + 0x11d5000011d5a, + 0x11d6000011d66, + 0x11d6700011d69, + 0x11d6a00011d8f, + 0x11d9000011d92, + 0x11d9300011d99, + 0x11da000011daa, + 0x11ee000011ef7, + 0x11f0000011f11, + 0x11f1200011f3b, + 0x11f3e00011f43, + 0x11f5000011f5a, + 0x11fb000011fb1, + 0x120000001239a, + 0x1248000012544, + 0x12f9000012ff1, + 0x1300000013430, + 0x1344000013456, + 0x1440000014647, + 0x1680000016a39, + 0x16a4000016a5f, + 0x16a6000016a6a, + 0x16a7000016abf, + 0x16ac000016aca, + 0x16ad000016aee, + 0x16af000016af5, + 0x16b0000016b37, + 0x16b4000016b44, + 0x16b5000016b5a, + 0x16b6300016b78, + 0x16b7d00016b90, + 0x16e6000016e80, + 0x16f0000016f4b, + 0x16f4f00016f88, + 0x16f8f00016fa0, + 0x16fe000016fe2, + 0x16fe300016fe5, + 0x16ff000016ff2, + 0x17000000187f8, + 0x1880000018cd6, + 0x18d0000018d09, + 0x1aff00001aff4, + 0x1aff50001affc, + 0x1affd0001afff, + 0x1b0000001b123, + 0x1b1320001b133, + 0x1b1500001b153, + 0x1b1550001b156, + 0x1b1640001b168, + 0x1b1700001b2fc, + 0x1bc000001bc6b, + 0x1bc700001bc7d, + 0x1bc800001bc89, + 0x1bc900001bc9a, + 0x1bc9d0001bc9f, + 0x1cf000001cf2e, + 0x1cf300001cf47, + 0x1da000001da37, + 0x1da3b0001da6d, + 0x1da750001da76, + 0x1da840001da85, + 0x1da9b0001daa0, + 0x1daa10001dab0, + 0x1df000001df1f, + 0x1df250001df2b, + 0x1e0000001e007, + 0x1e0080001e019, + 0x1e01b0001e022, + 0x1e0230001e025, + 0x1e0260001e02b, + 0x1e0300001e06e, + 0x1e08f0001e090, + 0x1e1000001e12d, + 0x1e1300001e13e, + 0x1e1400001e14a, + 0x1e14e0001e14f, + 0x1e2900001e2af, + 0x1e2c00001e2fa, + 0x1e4d00001e4fa, + 0x1e7e00001e7e7, + 0x1e7e80001e7ec, + 0x1e7ed0001e7ef, + 0x1e7f00001e7ff, + 0x1e8000001e8c5, + 0x1e8d00001e8d7, + 0x1e9220001e94c, + 0x1e9500001e95a, + 0x200000002a6e0, + 0x2a7000002b73a, + 0x2b7400002b81e, + 0x2b8200002cea2, + 0x2ceb00002ebe1, + 0x300000003134b, + 0x31350000323b0, + ), + 'CONTEXTJ': ( + 0x200c0000200e, + ), + 'CONTEXTO': ( + 0xb7000000b8, + 0x37500000376, + 0x5f3000005f5, + 0x6600000066a, + 0x6f0000006fa, + 0x30fb000030fc, + ), +} diff --git a/idna/intranges.py b/idna/intranges.py new file mode 100644 index 0000000..6a43b04 --- /dev/null +++ b/idna/intranges.py @@ -0,0 +1,54 @@ +""" +Given a list of integers, made up of (hopefully) a small number of long runs +of consecutive integers, compute a representation of the form +((start1, end1), (start2, end2) ...). Then answer the question "was x present +in the original list?" in time O(log(# runs)). +""" + +import bisect +from typing import List, Tuple + +def intranges_from_list(list_: List[int]) -> Tuple[int, ...]: + """Represent a list of integers as a sequence of ranges: + ((start_0, end_0), (start_1, end_1), ...), such that the original + integers are exactly those x such that start_i <= x < end_i for some i. + + Ranges are encoded as single integers (start << 32 | end), not as tuples. + """ + + sorted_list = sorted(list_) + ranges = [] + last_write = -1 + for i in range(len(sorted_list)): + if i+1 < len(sorted_list): + if sorted_list[i] == sorted_list[i+1]-1: + continue + current_range = sorted_list[last_write+1:i+1] + ranges.append(_encode_range(current_range[0], current_range[-1] + 1)) + last_write = i + + return tuple(ranges) + +def _encode_range(start: int, end: int) -> int: + return (start << 32) | end + +def _decode_range(r: int) -> Tuple[int, int]: + return (r >> 32), (r & ((1 << 32) - 1)) + + +def intranges_contain(int_: int, ranges: Tuple[int, ...]) -> bool: + """Determine if `int_` falls into one of the ranges in `ranges`.""" + tuple_ = _encode_range(int_, 0) + pos = bisect.bisect_left(ranges, tuple_) + # we could be immediately ahead of a tuple (start, end) + # with start < int_ <= end + if pos > 0: + left, right = _decode_range(ranges[pos-1]) + if left <= int_ < right: + return True + # or we could be immediately behind a tuple (int_, end) + if pos < len(ranges): + left, _ = _decode_range(ranges[pos]) + if left == int_: + return True + return False diff --git a/idna/package_data.py b/idna/package_data.py new file mode 100644 index 0000000..8501893 --- /dev/null +++ b/idna/package_data.py @@ -0,0 +1,2 @@ +__version__ = '3.4' + diff --git a/idna/py.typed b/idna/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/idna/uts46data.py b/idna/uts46data.py new file mode 100644 index 0000000..186796c --- /dev/null +++ b/idna/uts46data.py @@ -0,0 +1,8600 @@ +# This file is automatically generated by tools/idna-data +# vim: set fileencoding=utf-8 : + +from typing import List, Tuple, Union + + +"""IDNA Mapping Table from UTS46.""" + + +__version__ = '15.0.0' +def _seg_0() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x0, '3'), + (0x1, '3'), + (0x2, '3'), + (0x3, '3'), + (0x4, '3'), + (0x5, '3'), + (0x6, '3'), + (0x7, '3'), + (0x8, '3'), + (0x9, '3'), + (0xA, '3'), + (0xB, '3'), + (0xC, '3'), + (0xD, '3'), + (0xE, '3'), + (0xF, '3'), + (0x10, '3'), + (0x11, '3'), + (0x12, '3'), + (0x13, '3'), + (0x14, '3'), + (0x15, '3'), + (0x16, '3'), + (0x17, '3'), + (0x18, '3'), + (0x19, '3'), + (0x1A, '3'), + (0x1B, '3'), + (0x1C, '3'), + (0x1D, '3'), + (0x1E, '3'), + (0x1F, '3'), + (0x20, '3'), + (0x21, '3'), + (0x22, '3'), + (0x23, '3'), + (0x24, '3'), + (0x25, '3'), + (0x26, '3'), + (0x27, '3'), + (0x28, '3'), + (0x29, '3'), + (0x2A, '3'), + (0x2B, '3'), + (0x2C, '3'), + (0x2D, 'V'), + (0x2E, 'V'), + (0x2F, '3'), + (0x30, 'V'), + (0x31, 'V'), + (0x32, 'V'), + (0x33, 'V'), + (0x34, 'V'), + (0x35, 'V'), + (0x36, 'V'), + (0x37, 'V'), + (0x38, 'V'), + (0x39, 'V'), + (0x3A, '3'), + (0x3B, '3'), + (0x3C, '3'), + (0x3D, '3'), + (0x3E, '3'), + (0x3F, '3'), + (0x40, '3'), + (0x41, 'M', 'a'), + (0x42, 'M', 'b'), + (0x43, 'M', 'c'), + (0x44, 'M', 'd'), + (0x45, 'M', 'e'), + (0x46, 'M', 'f'), + (0x47, 'M', 'g'), + (0x48, 'M', 'h'), + (0x49, 'M', 'i'), + (0x4A, 'M', 'j'), + (0x4B, 'M', 'k'), + (0x4C, 'M', 'l'), + (0x4D, 'M', 'm'), + (0x4E, 'M', 'n'), + (0x4F, 'M', 'o'), + (0x50, 'M', 'p'), + (0x51, 'M', 'q'), + (0x52, 'M', 'r'), + (0x53, 'M', 's'), + (0x54, 'M', 't'), + (0x55, 'M', 'u'), + (0x56, 'M', 'v'), + (0x57, 'M', 'w'), + (0x58, 'M', 'x'), + (0x59, 'M', 'y'), + (0x5A, 'M', 'z'), + (0x5B, '3'), + (0x5C, '3'), + (0x5D, '3'), + (0x5E, '3'), + (0x5F, '3'), + (0x60, '3'), + (0x61, 'V'), + (0x62, 'V'), + (0x63, 'V'), + ] + +def _seg_1() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x64, 'V'), + (0x65, 'V'), + (0x66, 'V'), + (0x67, 'V'), + (0x68, 'V'), + (0x69, 'V'), + (0x6A, 'V'), + (0x6B, 'V'), + (0x6C, 'V'), + (0x6D, 'V'), + (0x6E, 'V'), + (0x6F, 'V'), + (0x70, 'V'), + (0x71, 'V'), + (0x72, 'V'), + (0x73, 'V'), + (0x74, 'V'), + (0x75, 'V'), + (0x76, 'V'), + (0x77, 'V'), + (0x78, 'V'), + (0x79, 'V'), + (0x7A, 'V'), + (0x7B, '3'), + (0x7C, '3'), + (0x7D, '3'), + (0x7E, '3'), + (0x7F, '3'), + (0x80, 'X'), + (0x81, 'X'), + (0x82, 'X'), + (0x83, 'X'), + (0x84, 'X'), + (0x85, 'X'), + (0x86, 'X'), + (0x87, 'X'), + (0x88, 'X'), + (0x89, 'X'), + (0x8A, 'X'), + (0x8B, 'X'), + (0x8C, 'X'), + (0x8D, 'X'), + (0x8E, 'X'), + (0x8F, 'X'), + (0x90, 'X'), + (0x91, 'X'), + (0x92, 'X'), + (0x93, 'X'), + (0x94, 'X'), + (0x95, 'X'), + (0x96, 'X'), + (0x97, 'X'), + (0x98, 'X'), + (0x99, 'X'), + (0x9A, 'X'), + (0x9B, 'X'), + (0x9C, 'X'), + (0x9D, 'X'), + (0x9E, 'X'), + (0x9F, 'X'), + (0xA0, '3', ' '), + (0xA1, 'V'), + (0xA2, 'V'), + (0xA3, 'V'), + (0xA4, 'V'), + (0xA5, 'V'), + (0xA6, 'V'), + (0xA7, 'V'), + (0xA8, '3', ' ̈'), + (0xA9, 'V'), + (0xAA, 'M', 'a'), + (0xAB, 'V'), + (0xAC, 'V'), + (0xAD, 'I'), + (0xAE, 'V'), + (0xAF, '3', ' ̄'), + (0xB0, 'V'), + (0xB1, 'V'), + (0xB2, 'M', '2'), + (0xB3, 'M', '3'), + (0xB4, '3', ' ́'), + (0xB5, 'M', 'μ'), + (0xB6, 'V'), + (0xB7, 'V'), + (0xB8, '3', ' ̧'), + (0xB9, 'M', '1'), + (0xBA, 'M', 'o'), + (0xBB, 'V'), + (0xBC, 'M', '1⁄4'), + (0xBD, 'M', '1⁄2'), + (0xBE, 'M', '3⁄4'), + (0xBF, 'V'), + (0xC0, 'M', 'à'), + (0xC1, 'M', 'á'), + (0xC2, 'M', 'â'), + (0xC3, 'M', 'ã'), + (0xC4, 'M', 'ä'), + (0xC5, 'M', 'å'), + (0xC6, 'M', 'æ'), + (0xC7, 'M', 'ç'), + ] + +def _seg_2() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xC8, 'M', 'è'), + (0xC9, 'M', 'é'), + (0xCA, 'M', 'ê'), + (0xCB, 'M', 'ë'), + (0xCC, 'M', 'ì'), + (0xCD, 'M', 'í'), + (0xCE, 'M', 'î'), + (0xCF, 'M', 'ï'), + (0xD0, 'M', 'ð'), + (0xD1, 'M', 'ñ'), + (0xD2, 'M', 'ò'), + (0xD3, 'M', 'ó'), + (0xD4, 'M', 'ô'), + (0xD5, 'M', 'õ'), + (0xD6, 'M', 'ö'), + (0xD7, 'V'), + (0xD8, 'M', 'ø'), + (0xD9, 'M', 'ù'), + (0xDA, 'M', 'ú'), + (0xDB, 'M', 'û'), + (0xDC, 'M', 'ü'), + (0xDD, 'M', 'ý'), + (0xDE, 'M', 'þ'), + (0xDF, 'D', 'ss'), + (0xE0, 'V'), + (0xE1, 'V'), + (0xE2, 'V'), + (0xE3, 'V'), + (0xE4, 'V'), + (0xE5, 'V'), + (0xE6, 'V'), + (0xE7, 'V'), + (0xE8, 'V'), + (0xE9, 'V'), + (0xEA, 'V'), + (0xEB, 'V'), + (0xEC, 'V'), + (0xED, 'V'), + (0xEE, 'V'), + (0xEF, 'V'), + (0xF0, 'V'), + (0xF1, 'V'), + (0xF2, 'V'), + (0xF3, 'V'), + (0xF4, 'V'), + (0xF5, 'V'), + (0xF6, 'V'), + (0xF7, 'V'), + (0xF8, 'V'), + (0xF9, 'V'), + (0xFA, 'V'), + (0xFB, 'V'), + (0xFC, 'V'), + (0xFD, 'V'), + (0xFE, 'V'), + (0xFF, 'V'), + (0x100, 'M', 'ā'), + (0x101, 'V'), + (0x102, 'M', 'ă'), + (0x103, 'V'), + (0x104, 'M', 'ą'), + (0x105, 'V'), + (0x106, 'M', 'ć'), + (0x107, 'V'), + (0x108, 'M', 'ĉ'), + (0x109, 'V'), + (0x10A, 'M', 'ċ'), + (0x10B, 'V'), + (0x10C, 'M', 'č'), + (0x10D, 'V'), + (0x10E, 'M', 'ď'), + (0x10F, 'V'), + (0x110, 'M', 'đ'), + (0x111, 'V'), + (0x112, 'M', 'ē'), + (0x113, 'V'), + (0x114, 'M', 'ĕ'), + (0x115, 'V'), + (0x116, 'M', 'ė'), + (0x117, 'V'), + (0x118, 'M', 'ę'), + (0x119, 'V'), + (0x11A, 'M', 'ě'), + (0x11B, 'V'), + (0x11C, 'M', 'ĝ'), + (0x11D, 'V'), + (0x11E, 'M', 'ğ'), + (0x11F, 'V'), + (0x120, 'M', 'ġ'), + (0x121, 'V'), + (0x122, 'M', 'ģ'), + (0x123, 'V'), + (0x124, 'M', 'ĥ'), + (0x125, 'V'), + (0x126, 'M', 'ħ'), + (0x127, 'V'), + (0x128, 'M', 'ĩ'), + (0x129, 'V'), + (0x12A, 'M', 'ī'), + (0x12B, 'V'), + ] + +def _seg_3() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x12C, 'M', 'ĭ'), + (0x12D, 'V'), + (0x12E, 'M', 'į'), + (0x12F, 'V'), + (0x130, 'M', 'i̇'), + (0x131, 'V'), + (0x132, 'M', 'ij'), + (0x134, 'M', 'ĵ'), + (0x135, 'V'), + (0x136, 'M', 'ķ'), + (0x137, 'V'), + (0x139, 'M', 'ĺ'), + (0x13A, 'V'), + (0x13B, 'M', 'ļ'), + (0x13C, 'V'), + (0x13D, 'M', 'ľ'), + (0x13E, 'V'), + (0x13F, 'M', 'l·'), + (0x141, 'M', 'ł'), + (0x142, 'V'), + (0x143, 'M', 'ń'), + (0x144, 'V'), + (0x145, 'M', 'ņ'), + (0x146, 'V'), + (0x147, 'M', 'ň'), + (0x148, 'V'), + (0x149, 'M', 'ʼn'), + (0x14A, 'M', 'ŋ'), + (0x14B, 'V'), + (0x14C, 'M', 'ō'), + (0x14D, 'V'), + (0x14E, 'M', 'ŏ'), + (0x14F, 'V'), + (0x150, 'M', 'ő'), + (0x151, 'V'), + (0x152, 'M', 'œ'), + (0x153, 'V'), + (0x154, 'M', 'ŕ'), + (0x155, 'V'), + (0x156, 'M', 'ŗ'), + (0x157, 'V'), + (0x158, 'M', 'ř'), + (0x159, 'V'), + (0x15A, 'M', 'ś'), + (0x15B, 'V'), + (0x15C, 'M', 'ŝ'), + (0x15D, 'V'), + (0x15E, 'M', 'ş'), + (0x15F, 'V'), + (0x160, 'M', 'š'), + (0x161, 'V'), + (0x162, 'M', 'ţ'), + (0x163, 'V'), + (0x164, 'M', 'ť'), + (0x165, 'V'), + (0x166, 'M', 'ŧ'), + (0x167, 'V'), + (0x168, 'M', 'ũ'), + (0x169, 'V'), + (0x16A, 'M', 'ū'), + (0x16B, 'V'), + (0x16C, 'M', 'ŭ'), + (0x16D, 'V'), + (0x16E, 'M', 'ů'), + (0x16F, 'V'), + (0x170, 'M', 'ű'), + (0x171, 'V'), + (0x172, 'M', 'ų'), + (0x173, 'V'), + (0x174, 'M', 'ŵ'), + (0x175, 'V'), + (0x176, 'M', 'ŷ'), + (0x177, 'V'), + (0x178, 'M', 'ÿ'), + (0x179, 'M', 'ź'), + (0x17A, 'V'), + (0x17B, 'M', 'ż'), + (0x17C, 'V'), + (0x17D, 'M', 'ž'), + (0x17E, 'V'), + (0x17F, 'M', 's'), + (0x180, 'V'), + (0x181, 'M', 'ɓ'), + (0x182, 'M', 'ƃ'), + (0x183, 'V'), + (0x184, 'M', 'ƅ'), + (0x185, 'V'), + (0x186, 'M', 'ɔ'), + (0x187, 'M', 'ƈ'), + (0x188, 'V'), + (0x189, 'M', 'ɖ'), + (0x18A, 'M', 'ɗ'), + (0x18B, 'M', 'ƌ'), + (0x18C, 'V'), + (0x18E, 'M', 'ǝ'), + (0x18F, 'M', 'ə'), + (0x190, 'M', 'ɛ'), + (0x191, 'M', 'ƒ'), + (0x192, 'V'), + (0x193, 'M', 'ɠ'), + ] + +def _seg_4() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x194, 'M', 'ɣ'), + (0x195, 'V'), + (0x196, 'M', 'ɩ'), + (0x197, 'M', 'ɨ'), + (0x198, 'M', 'ƙ'), + (0x199, 'V'), + (0x19C, 'M', 'ɯ'), + (0x19D, 'M', 'ɲ'), + (0x19E, 'V'), + (0x19F, 'M', 'ɵ'), + (0x1A0, 'M', 'ơ'), + (0x1A1, 'V'), + (0x1A2, 'M', 'ƣ'), + (0x1A3, 'V'), + (0x1A4, 'M', 'ƥ'), + (0x1A5, 'V'), + (0x1A6, 'M', 'ʀ'), + (0x1A7, 'M', 'ƨ'), + (0x1A8, 'V'), + (0x1A9, 'M', 'ʃ'), + (0x1AA, 'V'), + (0x1AC, 'M', 'ƭ'), + (0x1AD, 'V'), + (0x1AE, 'M', 'ʈ'), + (0x1AF, 'M', 'ư'), + (0x1B0, 'V'), + (0x1B1, 'M', 'ʊ'), + (0x1B2, 'M', 'ʋ'), + (0x1B3, 'M', 'ƴ'), + (0x1B4, 'V'), + (0x1B5, 'M', 'ƶ'), + (0x1B6, 'V'), + (0x1B7, 'M', 'ʒ'), + (0x1B8, 'M', 'ƹ'), + (0x1B9, 'V'), + (0x1BC, 'M', 'ƽ'), + (0x1BD, 'V'), + (0x1C4, 'M', 'dž'), + (0x1C7, 'M', 'lj'), + (0x1CA, 'M', 'nj'), + (0x1CD, 'M', 'ǎ'), + (0x1CE, 'V'), + (0x1CF, 'M', 'ǐ'), + (0x1D0, 'V'), + (0x1D1, 'M', 'ǒ'), + (0x1D2, 'V'), + (0x1D3, 'M', 'ǔ'), + (0x1D4, 'V'), + (0x1D5, 'M', 'ǖ'), + (0x1D6, 'V'), + (0x1D7, 'M', 'ǘ'), + (0x1D8, 'V'), + (0x1D9, 'M', 'ǚ'), + (0x1DA, 'V'), + (0x1DB, 'M', 'ǜ'), + (0x1DC, 'V'), + (0x1DE, 'M', 'ǟ'), + (0x1DF, 'V'), + (0x1E0, 'M', 'ǡ'), + (0x1E1, 'V'), + (0x1E2, 'M', 'ǣ'), + (0x1E3, 'V'), + (0x1E4, 'M', 'ǥ'), + (0x1E5, 'V'), + (0x1E6, 'M', 'ǧ'), + (0x1E7, 'V'), + (0x1E8, 'M', 'ǩ'), + (0x1E9, 'V'), + (0x1EA, 'M', 'ǫ'), + (0x1EB, 'V'), + (0x1EC, 'M', 'ǭ'), + (0x1ED, 'V'), + (0x1EE, 'M', 'ǯ'), + (0x1EF, 'V'), + (0x1F1, 'M', 'dz'), + (0x1F4, 'M', 'ǵ'), + (0x1F5, 'V'), + (0x1F6, 'M', 'ƕ'), + (0x1F7, 'M', 'ƿ'), + (0x1F8, 'M', 'ǹ'), + (0x1F9, 'V'), + (0x1FA, 'M', 'ǻ'), + (0x1FB, 'V'), + (0x1FC, 'M', 'ǽ'), + (0x1FD, 'V'), + (0x1FE, 'M', 'ǿ'), + (0x1FF, 'V'), + (0x200, 'M', 'ȁ'), + (0x201, 'V'), + (0x202, 'M', 'ȃ'), + (0x203, 'V'), + (0x204, 'M', 'ȅ'), + (0x205, 'V'), + (0x206, 'M', 'ȇ'), + (0x207, 'V'), + (0x208, 'M', 'ȉ'), + (0x209, 'V'), + (0x20A, 'M', 'ȋ'), + (0x20B, 'V'), + (0x20C, 'M', 'ȍ'), + ] + +def _seg_5() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x20D, 'V'), + (0x20E, 'M', 'ȏ'), + (0x20F, 'V'), + (0x210, 'M', 'ȑ'), + (0x211, 'V'), + (0x212, 'M', 'ȓ'), + (0x213, 'V'), + (0x214, 'M', 'ȕ'), + (0x215, 'V'), + (0x216, 'M', 'ȗ'), + (0x217, 'V'), + (0x218, 'M', 'ș'), + (0x219, 'V'), + (0x21A, 'M', 'ț'), + (0x21B, 'V'), + (0x21C, 'M', 'ȝ'), + (0x21D, 'V'), + (0x21E, 'M', 'ȟ'), + (0x21F, 'V'), + (0x220, 'M', 'ƞ'), + (0x221, 'V'), + (0x222, 'M', 'ȣ'), + (0x223, 'V'), + (0x224, 'M', 'ȥ'), + (0x225, 'V'), + (0x226, 'M', 'ȧ'), + (0x227, 'V'), + (0x228, 'M', 'ȩ'), + (0x229, 'V'), + (0x22A, 'M', 'ȫ'), + (0x22B, 'V'), + (0x22C, 'M', 'ȭ'), + (0x22D, 'V'), + (0x22E, 'M', 'ȯ'), + (0x22F, 'V'), + (0x230, 'M', 'ȱ'), + (0x231, 'V'), + (0x232, 'M', 'ȳ'), + (0x233, 'V'), + (0x23A, 'M', 'ⱥ'), + (0x23B, 'M', 'ȼ'), + (0x23C, 'V'), + (0x23D, 'M', 'ƚ'), + (0x23E, 'M', 'ⱦ'), + (0x23F, 'V'), + (0x241, 'M', 'ɂ'), + (0x242, 'V'), + (0x243, 'M', 'ƀ'), + (0x244, 'M', 'ʉ'), + (0x245, 'M', 'ʌ'), + (0x246, 'M', 'ɇ'), + (0x247, 'V'), + (0x248, 'M', 'ɉ'), + (0x249, 'V'), + (0x24A, 'M', 'ɋ'), + (0x24B, 'V'), + (0x24C, 'M', 'ɍ'), + (0x24D, 'V'), + (0x24E, 'M', 'ɏ'), + (0x24F, 'V'), + (0x2B0, 'M', 'h'), + (0x2B1, 'M', 'ɦ'), + (0x2B2, 'M', 'j'), + (0x2B3, 'M', 'r'), + (0x2B4, 'M', 'ɹ'), + (0x2B5, 'M', 'ɻ'), + (0x2B6, 'M', 'ʁ'), + (0x2B7, 'M', 'w'), + (0x2B8, 'M', 'y'), + (0x2B9, 'V'), + (0x2D8, '3', ' ̆'), + (0x2D9, '3', ' ̇'), + (0x2DA, '3', ' ̊'), + (0x2DB, '3', ' ̨'), + (0x2DC, '3', ' ̃'), + (0x2DD, '3', ' ̋'), + (0x2DE, 'V'), + (0x2E0, 'M', 'ɣ'), + (0x2E1, 'M', 'l'), + (0x2E2, 'M', 's'), + (0x2E3, 'M', 'x'), + (0x2E4, 'M', 'ʕ'), + (0x2E5, 'V'), + (0x340, 'M', '̀'), + (0x341, 'M', '́'), + (0x342, 'V'), + (0x343, 'M', '̓'), + (0x344, 'M', '̈́'), + (0x345, 'M', 'ι'), + (0x346, 'V'), + (0x34F, 'I'), + (0x350, 'V'), + (0x370, 'M', 'ͱ'), + (0x371, 'V'), + (0x372, 'M', 'ͳ'), + (0x373, 'V'), + (0x374, 'M', 'ʹ'), + (0x375, 'V'), + (0x376, 'M', 'ͷ'), + (0x377, 'V'), + ] + +def _seg_6() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x378, 'X'), + (0x37A, '3', ' ι'), + (0x37B, 'V'), + (0x37E, '3', ';'), + (0x37F, 'M', 'ϳ'), + (0x380, 'X'), + (0x384, '3', ' ́'), + (0x385, '3', ' ̈́'), + (0x386, 'M', 'ά'), + (0x387, 'M', '·'), + (0x388, 'M', 'έ'), + (0x389, 'M', 'ή'), + (0x38A, 'M', 'ί'), + (0x38B, 'X'), + (0x38C, 'M', 'ό'), + (0x38D, 'X'), + (0x38E, 'M', 'ύ'), + (0x38F, 'M', 'ώ'), + (0x390, 'V'), + (0x391, 'M', 'α'), + (0x392, 'M', 'β'), + (0x393, 'M', 'γ'), + (0x394, 'M', 'δ'), + (0x395, 'M', 'ε'), + (0x396, 'M', 'ζ'), + (0x397, 'M', 'η'), + (0x398, 'M', 'θ'), + (0x399, 'M', 'ι'), + (0x39A, 'M', 'κ'), + (0x39B, 'M', 'λ'), + (0x39C, 'M', 'μ'), + (0x39D, 'M', 'ν'), + (0x39E, 'M', 'ξ'), + (0x39F, 'M', 'ο'), + (0x3A0, 'M', 'π'), + (0x3A1, 'M', 'ρ'), + (0x3A2, 'X'), + (0x3A3, 'M', 'σ'), + (0x3A4, 'M', 'τ'), + (0x3A5, 'M', 'υ'), + (0x3A6, 'M', 'φ'), + (0x3A7, 'M', 'χ'), + (0x3A8, 'M', 'ψ'), + (0x3A9, 'M', 'ω'), + (0x3AA, 'M', 'ϊ'), + (0x3AB, 'M', 'ϋ'), + (0x3AC, 'V'), + (0x3C2, 'D', 'σ'), + (0x3C3, 'V'), + (0x3CF, 'M', 'ϗ'), + (0x3D0, 'M', 'β'), + (0x3D1, 'M', 'θ'), + (0x3D2, 'M', 'υ'), + (0x3D3, 'M', 'ύ'), + (0x3D4, 'M', 'ϋ'), + (0x3D5, 'M', 'φ'), + (0x3D6, 'M', 'π'), + (0x3D7, 'V'), + (0x3D8, 'M', 'ϙ'), + (0x3D9, 'V'), + (0x3DA, 'M', 'ϛ'), + (0x3DB, 'V'), + (0x3DC, 'M', 'ϝ'), + (0x3DD, 'V'), + (0x3DE, 'M', 'ϟ'), + (0x3DF, 'V'), + (0x3E0, 'M', 'ϡ'), + (0x3E1, 'V'), + (0x3E2, 'M', 'ϣ'), + (0x3E3, 'V'), + (0x3E4, 'M', 'ϥ'), + (0x3E5, 'V'), + (0x3E6, 'M', 'ϧ'), + (0x3E7, 'V'), + (0x3E8, 'M', 'ϩ'), + (0x3E9, 'V'), + (0x3EA, 'M', 'ϫ'), + (0x3EB, 'V'), + (0x3EC, 'M', 'ϭ'), + (0x3ED, 'V'), + (0x3EE, 'M', 'ϯ'), + (0x3EF, 'V'), + (0x3F0, 'M', 'κ'), + (0x3F1, 'M', 'ρ'), + (0x3F2, 'M', 'σ'), + (0x3F3, 'V'), + (0x3F4, 'M', 'θ'), + (0x3F5, 'M', 'ε'), + (0x3F6, 'V'), + (0x3F7, 'M', 'ϸ'), + (0x3F8, 'V'), + (0x3F9, 'M', 'σ'), + (0x3FA, 'M', 'ϻ'), + (0x3FB, 'V'), + (0x3FD, 'M', 'ͻ'), + (0x3FE, 'M', 'ͼ'), + (0x3FF, 'M', 'ͽ'), + (0x400, 'M', 'ѐ'), + (0x401, 'M', 'ё'), + (0x402, 'M', 'ђ'), + ] + +def _seg_7() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x403, 'M', 'ѓ'), + (0x404, 'M', 'є'), + (0x405, 'M', 'ѕ'), + (0x406, 'M', 'і'), + (0x407, 'M', 'ї'), + (0x408, 'M', 'ј'), + (0x409, 'M', 'љ'), + (0x40A, 'M', 'њ'), + (0x40B, 'M', 'ћ'), + (0x40C, 'M', 'ќ'), + (0x40D, 'M', 'ѝ'), + (0x40E, 'M', 'ў'), + (0x40F, 'M', 'џ'), + (0x410, 'M', 'а'), + (0x411, 'M', 'б'), + (0x412, 'M', 'в'), + (0x413, 'M', 'г'), + (0x414, 'M', 'д'), + (0x415, 'M', 'е'), + (0x416, 'M', 'ж'), + (0x417, 'M', 'з'), + (0x418, 'M', 'и'), + (0x419, 'M', 'й'), + (0x41A, 'M', 'к'), + (0x41B, 'M', 'л'), + (0x41C, 'M', 'м'), + (0x41D, 'M', 'н'), + (0x41E, 'M', 'о'), + (0x41F, 'M', 'п'), + (0x420, 'M', 'р'), + (0x421, 'M', 'с'), + (0x422, 'M', 'т'), + (0x423, 'M', 'у'), + (0x424, 'M', 'ф'), + (0x425, 'M', 'х'), + (0x426, 'M', 'ц'), + (0x427, 'M', 'ч'), + (0x428, 'M', 'ш'), + (0x429, 'M', 'щ'), + (0x42A, 'M', 'ъ'), + (0x42B, 'M', 'ы'), + (0x42C, 'M', 'ь'), + (0x42D, 'M', 'э'), + (0x42E, 'M', 'ю'), + (0x42F, 'M', 'я'), + (0x430, 'V'), + (0x460, 'M', 'ѡ'), + (0x461, 'V'), + (0x462, 'M', 'ѣ'), + (0x463, 'V'), + (0x464, 'M', 'ѥ'), + (0x465, 'V'), + (0x466, 'M', 'ѧ'), + (0x467, 'V'), + (0x468, 'M', 'ѩ'), + (0x469, 'V'), + (0x46A, 'M', 'ѫ'), + (0x46B, 'V'), + (0x46C, 'M', 'ѭ'), + (0x46D, 'V'), + (0x46E, 'M', 'ѯ'), + (0x46F, 'V'), + (0x470, 'M', 'ѱ'), + (0x471, 'V'), + (0x472, 'M', 'ѳ'), + (0x473, 'V'), + (0x474, 'M', 'ѵ'), + (0x475, 'V'), + (0x476, 'M', 'ѷ'), + (0x477, 'V'), + (0x478, 'M', 'ѹ'), + (0x479, 'V'), + (0x47A, 'M', 'ѻ'), + (0x47B, 'V'), + (0x47C, 'M', 'ѽ'), + (0x47D, 'V'), + (0x47E, 'M', 'ѿ'), + (0x47F, 'V'), + (0x480, 'M', 'ҁ'), + (0x481, 'V'), + (0x48A, 'M', 'ҋ'), + (0x48B, 'V'), + (0x48C, 'M', 'ҍ'), + (0x48D, 'V'), + (0x48E, 'M', 'ҏ'), + (0x48F, 'V'), + (0x490, 'M', 'ґ'), + (0x491, 'V'), + (0x492, 'M', 'ғ'), + (0x493, 'V'), + (0x494, 'M', 'ҕ'), + (0x495, 'V'), + (0x496, 'M', 'җ'), + (0x497, 'V'), + (0x498, 'M', 'ҙ'), + (0x499, 'V'), + (0x49A, 'M', 'қ'), + (0x49B, 'V'), + (0x49C, 'M', 'ҝ'), + (0x49D, 'V'), + ] + +def _seg_8() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x49E, 'M', 'ҟ'), + (0x49F, 'V'), + (0x4A0, 'M', 'ҡ'), + (0x4A1, 'V'), + (0x4A2, 'M', 'ң'), + (0x4A3, 'V'), + (0x4A4, 'M', 'ҥ'), + (0x4A5, 'V'), + (0x4A6, 'M', 'ҧ'), + (0x4A7, 'V'), + (0x4A8, 'M', 'ҩ'), + (0x4A9, 'V'), + (0x4AA, 'M', 'ҫ'), + (0x4AB, 'V'), + (0x4AC, 'M', 'ҭ'), + (0x4AD, 'V'), + (0x4AE, 'M', 'ү'), + (0x4AF, 'V'), + (0x4B0, 'M', 'ұ'), + (0x4B1, 'V'), + (0x4B2, 'M', 'ҳ'), + (0x4B3, 'V'), + (0x4B4, 'M', 'ҵ'), + (0x4B5, 'V'), + (0x4B6, 'M', 'ҷ'), + (0x4B7, 'V'), + (0x4B8, 'M', 'ҹ'), + (0x4B9, 'V'), + (0x4BA, 'M', 'һ'), + (0x4BB, 'V'), + (0x4BC, 'M', 'ҽ'), + (0x4BD, 'V'), + (0x4BE, 'M', 'ҿ'), + (0x4BF, 'V'), + (0x4C0, 'X'), + (0x4C1, 'M', 'ӂ'), + (0x4C2, 'V'), + (0x4C3, 'M', 'ӄ'), + (0x4C4, 'V'), + (0x4C5, 'M', 'ӆ'), + (0x4C6, 'V'), + (0x4C7, 'M', 'ӈ'), + (0x4C8, 'V'), + (0x4C9, 'M', 'ӊ'), + (0x4CA, 'V'), + (0x4CB, 'M', 'ӌ'), + (0x4CC, 'V'), + (0x4CD, 'M', 'ӎ'), + (0x4CE, 'V'), + (0x4D0, 'M', 'ӑ'), + (0x4D1, 'V'), + (0x4D2, 'M', 'ӓ'), + (0x4D3, 'V'), + (0x4D4, 'M', 'ӕ'), + (0x4D5, 'V'), + (0x4D6, 'M', 'ӗ'), + (0x4D7, 'V'), + (0x4D8, 'M', 'ә'), + (0x4D9, 'V'), + (0x4DA, 'M', 'ӛ'), + (0x4DB, 'V'), + (0x4DC, 'M', 'ӝ'), + (0x4DD, 'V'), + (0x4DE, 'M', 'ӟ'), + (0x4DF, 'V'), + (0x4E0, 'M', 'ӡ'), + (0x4E1, 'V'), + (0x4E2, 'M', 'ӣ'), + (0x4E3, 'V'), + (0x4E4, 'M', 'ӥ'), + (0x4E5, 'V'), + (0x4E6, 'M', 'ӧ'), + (0x4E7, 'V'), + (0x4E8, 'M', 'ө'), + (0x4E9, 'V'), + (0x4EA, 'M', 'ӫ'), + (0x4EB, 'V'), + (0x4EC, 'M', 'ӭ'), + (0x4ED, 'V'), + (0x4EE, 'M', 'ӯ'), + (0x4EF, 'V'), + (0x4F0, 'M', 'ӱ'), + (0x4F1, 'V'), + (0x4F2, 'M', 'ӳ'), + (0x4F3, 'V'), + (0x4F4, 'M', 'ӵ'), + (0x4F5, 'V'), + (0x4F6, 'M', 'ӷ'), + (0x4F7, 'V'), + (0x4F8, 'M', 'ӹ'), + (0x4F9, 'V'), + (0x4FA, 'M', 'ӻ'), + (0x4FB, 'V'), + (0x4FC, 'M', 'ӽ'), + (0x4FD, 'V'), + (0x4FE, 'M', 'ӿ'), + (0x4FF, 'V'), + (0x500, 'M', 'ԁ'), + (0x501, 'V'), + (0x502, 'M', 'ԃ'), + ] + +def _seg_9() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x503, 'V'), + (0x504, 'M', 'ԅ'), + (0x505, 'V'), + (0x506, 'M', 'ԇ'), + (0x507, 'V'), + (0x508, 'M', 'ԉ'), + (0x509, 'V'), + (0x50A, 'M', 'ԋ'), + (0x50B, 'V'), + (0x50C, 'M', 'ԍ'), + (0x50D, 'V'), + (0x50E, 'M', 'ԏ'), + (0x50F, 'V'), + (0x510, 'M', 'ԑ'), + (0x511, 'V'), + (0x512, 'M', 'ԓ'), + (0x513, 'V'), + (0x514, 'M', 'ԕ'), + (0x515, 'V'), + (0x516, 'M', 'ԗ'), + (0x517, 'V'), + (0x518, 'M', 'ԙ'), + (0x519, 'V'), + (0x51A, 'M', 'ԛ'), + (0x51B, 'V'), + (0x51C, 'M', 'ԝ'), + (0x51D, 'V'), + (0x51E, 'M', 'ԟ'), + (0x51F, 'V'), + (0x520, 'M', 'ԡ'), + (0x521, 'V'), + (0x522, 'M', 'ԣ'), + (0x523, 'V'), + (0x524, 'M', 'ԥ'), + (0x525, 'V'), + (0x526, 'M', 'ԧ'), + (0x527, 'V'), + (0x528, 'M', 'ԩ'), + (0x529, 'V'), + (0x52A, 'M', 'ԫ'), + (0x52B, 'V'), + (0x52C, 'M', 'ԭ'), + (0x52D, 'V'), + (0x52E, 'M', 'ԯ'), + (0x52F, 'V'), + (0x530, 'X'), + (0x531, 'M', 'ա'), + (0x532, 'M', 'բ'), + (0x533, 'M', 'գ'), + (0x534, 'M', 'դ'), + (0x535, 'M', 'ե'), + (0x536, 'M', 'զ'), + (0x537, 'M', 'է'), + (0x538, 'M', 'ը'), + (0x539, 'M', 'թ'), + (0x53A, 'M', 'ժ'), + (0x53B, 'M', 'ի'), + (0x53C, 'M', 'լ'), + (0x53D, 'M', 'խ'), + (0x53E, 'M', 'ծ'), + (0x53F, 'M', 'կ'), + (0x540, 'M', 'հ'), + (0x541, 'M', 'ձ'), + (0x542, 'M', 'ղ'), + (0x543, 'M', 'ճ'), + (0x544, 'M', 'մ'), + (0x545, 'M', 'յ'), + (0x546, 'M', 'ն'), + (0x547, 'M', 'շ'), + (0x548, 'M', 'ո'), + (0x549, 'M', 'չ'), + (0x54A, 'M', 'պ'), + (0x54B, 'M', 'ջ'), + (0x54C, 'M', 'ռ'), + (0x54D, 'M', 'ս'), + (0x54E, 'M', 'վ'), + (0x54F, 'M', 'տ'), + (0x550, 'M', 'ր'), + (0x551, 'M', 'ց'), + (0x552, 'M', 'ւ'), + (0x553, 'M', 'փ'), + (0x554, 'M', 'ք'), + (0x555, 'M', 'օ'), + (0x556, 'M', 'ֆ'), + (0x557, 'X'), + (0x559, 'V'), + (0x587, 'M', 'եւ'), + (0x588, 'V'), + (0x58B, 'X'), + (0x58D, 'V'), + (0x590, 'X'), + (0x591, 'V'), + (0x5C8, 'X'), + (0x5D0, 'V'), + (0x5EB, 'X'), + (0x5EF, 'V'), + (0x5F5, 'X'), + (0x606, 'V'), + (0x61C, 'X'), + (0x61D, 'V'), + ] + +def _seg_10() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x675, 'M', 'اٴ'), + (0x676, 'M', 'وٴ'), + (0x677, 'M', 'ۇٴ'), + (0x678, 'M', 'يٴ'), + (0x679, 'V'), + (0x6DD, 'X'), + (0x6DE, 'V'), + (0x70E, 'X'), + (0x710, 'V'), + (0x74B, 'X'), + (0x74D, 'V'), + (0x7B2, 'X'), + (0x7C0, 'V'), + (0x7FB, 'X'), + (0x7FD, 'V'), + (0x82E, 'X'), + (0x830, 'V'), + (0x83F, 'X'), + (0x840, 'V'), + (0x85C, 'X'), + (0x85E, 'V'), + (0x85F, 'X'), + (0x860, 'V'), + (0x86B, 'X'), + (0x870, 'V'), + (0x88F, 'X'), + (0x898, 'V'), + (0x8E2, 'X'), + (0x8E3, 'V'), + (0x958, 'M', 'क़'), + (0x959, 'M', 'ख़'), + (0x95A, 'M', 'ग़'), + (0x95B, 'M', 'ज़'), + (0x95C, 'M', 'ड़'), + (0x95D, 'M', 'ढ़'), + (0x95E, 'M', 'फ़'), + (0x95F, 'M', 'य़'), + (0x960, 'V'), + (0x984, 'X'), + (0x985, 'V'), + (0x98D, 'X'), + (0x98F, 'V'), + (0x991, 'X'), + (0x993, 'V'), + (0x9A9, 'X'), + (0x9AA, 'V'), + (0x9B1, 'X'), + (0x9B2, 'V'), + (0x9B3, 'X'), + (0x9B6, 'V'), + (0x9BA, 'X'), + (0x9BC, 'V'), + (0x9C5, 'X'), + (0x9C7, 'V'), + (0x9C9, 'X'), + (0x9CB, 'V'), + (0x9CF, 'X'), + (0x9D7, 'V'), + (0x9D8, 'X'), + (0x9DC, 'M', 'ড়'), + (0x9DD, 'M', 'ঢ়'), + (0x9DE, 'X'), + (0x9DF, 'M', 'য়'), + (0x9E0, 'V'), + (0x9E4, 'X'), + (0x9E6, 'V'), + (0x9FF, 'X'), + (0xA01, 'V'), + (0xA04, 'X'), + (0xA05, 'V'), + (0xA0B, 'X'), + (0xA0F, 'V'), + (0xA11, 'X'), + (0xA13, 'V'), + (0xA29, 'X'), + (0xA2A, 'V'), + (0xA31, 'X'), + (0xA32, 'V'), + (0xA33, 'M', 'ਲ਼'), + (0xA34, 'X'), + (0xA35, 'V'), + (0xA36, 'M', 'ਸ਼'), + (0xA37, 'X'), + (0xA38, 'V'), + (0xA3A, 'X'), + (0xA3C, 'V'), + (0xA3D, 'X'), + (0xA3E, 'V'), + (0xA43, 'X'), + (0xA47, 'V'), + (0xA49, 'X'), + (0xA4B, 'V'), + (0xA4E, 'X'), + (0xA51, 'V'), + (0xA52, 'X'), + (0xA59, 'M', 'ਖ਼'), + (0xA5A, 'M', 'ਗ਼'), + (0xA5B, 'M', 'ਜ਼'), + (0xA5C, 'V'), + (0xA5D, 'X'), + ] + +def _seg_11() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA5E, 'M', 'ਫ਼'), + (0xA5F, 'X'), + (0xA66, 'V'), + (0xA77, 'X'), + (0xA81, 'V'), + (0xA84, 'X'), + (0xA85, 'V'), + (0xA8E, 'X'), + (0xA8F, 'V'), + (0xA92, 'X'), + (0xA93, 'V'), + (0xAA9, 'X'), + (0xAAA, 'V'), + (0xAB1, 'X'), + (0xAB2, 'V'), + (0xAB4, 'X'), + (0xAB5, 'V'), + (0xABA, 'X'), + (0xABC, 'V'), + (0xAC6, 'X'), + (0xAC7, 'V'), + (0xACA, 'X'), + (0xACB, 'V'), + (0xACE, 'X'), + (0xAD0, 'V'), + (0xAD1, 'X'), + (0xAE0, 'V'), + (0xAE4, 'X'), + (0xAE6, 'V'), + (0xAF2, 'X'), + (0xAF9, 'V'), + (0xB00, 'X'), + (0xB01, 'V'), + (0xB04, 'X'), + (0xB05, 'V'), + (0xB0D, 'X'), + (0xB0F, 'V'), + (0xB11, 'X'), + (0xB13, 'V'), + (0xB29, 'X'), + (0xB2A, 'V'), + (0xB31, 'X'), + (0xB32, 'V'), + (0xB34, 'X'), + (0xB35, 'V'), + (0xB3A, 'X'), + (0xB3C, 'V'), + (0xB45, 'X'), + (0xB47, 'V'), + (0xB49, 'X'), + (0xB4B, 'V'), + (0xB4E, 'X'), + (0xB55, 'V'), + (0xB58, 'X'), + (0xB5C, 'M', 'ଡ଼'), + (0xB5D, 'M', 'ଢ଼'), + (0xB5E, 'X'), + (0xB5F, 'V'), + (0xB64, 'X'), + (0xB66, 'V'), + (0xB78, 'X'), + (0xB82, 'V'), + (0xB84, 'X'), + (0xB85, 'V'), + (0xB8B, 'X'), + (0xB8E, 'V'), + (0xB91, 'X'), + (0xB92, 'V'), + (0xB96, 'X'), + (0xB99, 'V'), + (0xB9B, 'X'), + (0xB9C, 'V'), + (0xB9D, 'X'), + (0xB9E, 'V'), + (0xBA0, 'X'), + (0xBA3, 'V'), + (0xBA5, 'X'), + (0xBA8, 'V'), + (0xBAB, 'X'), + (0xBAE, 'V'), + (0xBBA, 'X'), + (0xBBE, 'V'), + (0xBC3, 'X'), + (0xBC6, 'V'), + (0xBC9, 'X'), + (0xBCA, 'V'), + (0xBCE, 'X'), + (0xBD0, 'V'), + (0xBD1, 'X'), + (0xBD7, 'V'), + (0xBD8, 'X'), + (0xBE6, 'V'), + (0xBFB, 'X'), + (0xC00, 'V'), + (0xC0D, 'X'), + (0xC0E, 'V'), + (0xC11, 'X'), + (0xC12, 'V'), + (0xC29, 'X'), + (0xC2A, 'V'), + ] + +def _seg_12() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xC3A, 'X'), + (0xC3C, 'V'), + (0xC45, 'X'), + (0xC46, 'V'), + (0xC49, 'X'), + (0xC4A, 'V'), + (0xC4E, 'X'), + (0xC55, 'V'), + (0xC57, 'X'), + (0xC58, 'V'), + (0xC5B, 'X'), + (0xC5D, 'V'), + (0xC5E, 'X'), + (0xC60, 'V'), + (0xC64, 'X'), + (0xC66, 'V'), + (0xC70, 'X'), + (0xC77, 'V'), + (0xC8D, 'X'), + (0xC8E, 'V'), + (0xC91, 'X'), + (0xC92, 'V'), + (0xCA9, 'X'), + (0xCAA, 'V'), + (0xCB4, 'X'), + (0xCB5, 'V'), + (0xCBA, 'X'), + (0xCBC, 'V'), + (0xCC5, 'X'), + (0xCC6, 'V'), + (0xCC9, 'X'), + (0xCCA, 'V'), + (0xCCE, 'X'), + (0xCD5, 'V'), + (0xCD7, 'X'), + (0xCDD, 'V'), + (0xCDF, 'X'), + (0xCE0, 'V'), + (0xCE4, 'X'), + (0xCE6, 'V'), + (0xCF0, 'X'), + (0xCF1, 'V'), + (0xCF4, 'X'), + (0xD00, 'V'), + (0xD0D, 'X'), + (0xD0E, 'V'), + (0xD11, 'X'), + (0xD12, 'V'), + (0xD45, 'X'), + (0xD46, 'V'), + (0xD49, 'X'), + (0xD4A, 'V'), + (0xD50, 'X'), + (0xD54, 'V'), + (0xD64, 'X'), + (0xD66, 'V'), + (0xD80, 'X'), + (0xD81, 'V'), + (0xD84, 'X'), + (0xD85, 'V'), + (0xD97, 'X'), + (0xD9A, 'V'), + (0xDB2, 'X'), + (0xDB3, 'V'), + (0xDBC, 'X'), + (0xDBD, 'V'), + (0xDBE, 'X'), + (0xDC0, 'V'), + (0xDC7, 'X'), + (0xDCA, 'V'), + (0xDCB, 'X'), + (0xDCF, 'V'), + (0xDD5, 'X'), + (0xDD6, 'V'), + (0xDD7, 'X'), + (0xDD8, 'V'), + (0xDE0, 'X'), + (0xDE6, 'V'), + (0xDF0, 'X'), + (0xDF2, 'V'), + (0xDF5, 'X'), + (0xE01, 'V'), + (0xE33, 'M', 'ํา'), + (0xE34, 'V'), + (0xE3B, 'X'), + (0xE3F, 'V'), + (0xE5C, 'X'), + (0xE81, 'V'), + (0xE83, 'X'), + (0xE84, 'V'), + (0xE85, 'X'), + (0xE86, 'V'), + (0xE8B, 'X'), + (0xE8C, 'V'), + (0xEA4, 'X'), + (0xEA5, 'V'), + (0xEA6, 'X'), + (0xEA7, 'V'), + (0xEB3, 'M', 'ໍາ'), + (0xEB4, 'V'), + ] + +def _seg_13() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xEBE, 'X'), + (0xEC0, 'V'), + (0xEC5, 'X'), + (0xEC6, 'V'), + (0xEC7, 'X'), + (0xEC8, 'V'), + (0xECF, 'X'), + (0xED0, 'V'), + (0xEDA, 'X'), + (0xEDC, 'M', 'ຫນ'), + (0xEDD, 'M', 'ຫມ'), + (0xEDE, 'V'), + (0xEE0, 'X'), + (0xF00, 'V'), + (0xF0C, 'M', '་'), + (0xF0D, 'V'), + (0xF43, 'M', 'གྷ'), + (0xF44, 'V'), + (0xF48, 'X'), + (0xF49, 'V'), + (0xF4D, 'M', 'ཌྷ'), + (0xF4E, 'V'), + (0xF52, 'M', 'དྷ'), + (0xF53, 'V'), + (0xF57, 'M', 'བྷ'), + (0xF58, 'V'), + (0xF5C, 'M', 'ཛྷ'), + (0xF5D, 'V'), + (0xF69, 'M', 'ཀྵ'), + (0xF6A, 'V'), + (0xF6D, 'X'), + (0xF71, 'V'), + (0xF73, 'M', 'ཱི'), + (0xF74, 'V'), + (0xF75, 'M', 'ཱུ'), + (0xF76, 'M', 'ྲྀ'), + (0xF77, 'M', 'ྲཱྀ'), + (0xF78, 'M', 'ླྀ'), + (0xF79, 'M', 'ླཱྀ'), + (0xF7A, 'V'), + (0xF81, 'M', 'ཱྀ'), + (0xF82, 'V'), + (0xF93, 'M', 'ྒྷ'), + (0xF94, 'V'), + (0xF98, 'X'), + (0xF99, 'V'), + (0xF9D, 'M', 'ྜྷ'), + (0xF9E, 'V'), + (0xFA2, 'M', 'ྡྷ'), + (0xFA3, 'V'), + (0xFA7, 'M', 'ྦྷ'), + (0xFA8, 'V'), + (0xFAC, 'M', 'ྫྷ'), + (0xFAD, 'V'), + (0xFB9, 'M', 'ྐྵ'), + (0xFBA, 'V'), + (0xFBD, 'X'), + (0xFBE, 'V'), + (0xFCD, 'X'), + (0xFCE, 'V'), + (0xFDB, 'X'), + (0x1000, 'V'), + (0x10A0, 'X'), + (0x10C7, 'M', 'ⴧ'), + (0x10C8, 'X'), + (0x10CD, 'M', 'ⴭ'), + (0x10CE, 'X'), + (0x10D0, 'V'), + (0x10FC, 'M', 'ნ'), + (0x10FD, 'V'), + (0x115F, 'X'), + (0x1161, 'V'), + (0x1249, 'X'), + (0x124A, 'V'), + (0x124E, 'X'), + (0x1250, 'V'), + (0x1257, 'X'), + (0x1258, 'V'), + (0x1259, 'X'), + (0x125A, 'V'), + (0x125E, 'X'), + (0x1260, 'V'), + (0x1289, 'X'), + (0x128A, 'V'), + (0x128E, 'X'), + (0x1290, 'V'), + (0x12B1, 'X'), + (0x12B2, 'V'), + (0x12B6, 'X'), + (0x12B8, 'V'), + (0x12BF, 'X'), + (0x12C0, 'V'), + (0x12C1, 'X'), + (0x12C2, 'V'), + (0x12C6, 'X'), + (0x12C8, 'V'), + (0x12D7, 'X'), + (0x12D8, 'V'), + (0x1311, 'X'), + (0x1312, 'V'), + ] + +def _seg_14() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1316, 'X'), + (0x1318, 'V'), + (0x135B, 'X'), + (0x135D, 'V'), + (0x137D, 'X'), + (0x1380, 'V'), + (0x139A, 'X'), + (0x13A0, 'V'), + (0x13F6, 'X'), + (0x13F8, 'M', 'Ᏸ'), + (0x13F9, 'M', 'Ᏹ'), + (0x13FA, 'M', 'Ᏺ'), + (0x13FB, 'M', 'Ᏻ'), + (0x13FC, 'M', 'Ᏼ'), + (0x13FD, 'M', 'Ᏽ'), + (0x13FE, 'X'), + (0x1400, 'V'), + (0x1680, 'X'), + (0x1681, 'V'), + (0x169D, 'X'), + (0x16A0, 'V'), + (0x16F9, 'X'), + (0x1700, 'V'), + (0x1716, 'X'), + (0x171F, 'V'), + (0x1737, 'X'), + (0x1740, 'V'), + (0x1754, 'X'), + (0x1760, 'V'), + (0x176D, 'X'), + (0x176E, 'V'), + (0x1771, 'X'), + (0x1772, 'V'), + (0x1774, 'X'), + (0x1780, 'V'), + (0x17B4, 'X'), + (0x17B6, 'V'), + (0x17DE, 'X'), + (0x17E0, 'V'), + (0x17EA, 'X'), + (0x17F0, 'V'), + (0x17FA, 'X'), + (0x1800, 'V'), + (0x1806, 'X'), + (0x1807, 'V'), + (0x180B, 'I'), + (0x180E, 'X'), + (0x180F, 'I'), + (0x1810, 'V'), + (0x181A, 'X'), + (0x1820, 'V'), + (0x1879, 'X'), + (0x1880, 'V'), + (0x18AB, 'X'), + (0x18B0, 'V'), + (0x18F6, 'X'), + (0x1900, 'V'), + (0x191F, 'X'), + (0x1920, 'V'), + (0x192C, 'X'), + (0x1930, 'V'), + (0x193C, 'X'), + (0x1940, 'V'), + (0x1941, 'X'), + (0x1944, 'V'), + (0x196E, 'X'), + (0x1970, 'V'), + (0x1975, 'X'), + (0x1980, 'V'), + (0x19AC, 'X'), + (0x19B0, 'V'), + (0x19CA, 'X'), + (0x19D0, 'V'), + (0x19DB, 'X'), + (0x19DE, 'V'), + (0x1A1C, 'X'), + (0x1A1E, 'V'), + (0x1A5F, 'X'), + (0x1A60, 'V'), + (0x1A7D, 'X'), + (0x1A7F, 'V'), + (0x1A8A, 'X'), + (0x1A90, 'V'), + (0x1A9A, 'X'), + (0x1AA0, 'V'), + (0x1AAE, 'X'), + (0x1AB0, 'V'), + (0x1ACF, 'X'), + (0x1B00, 'V'), + (0x1B4D, 'X'), + (0x1B50, 'V'), + (0x1B7F, 'X'), + (0x1B80, 'V'), + (0x1BF4, 'X'), + (0x1BFC, 'V'), + (0x1C38, 'X'), + (0x1C3B, 'V'), + (0x1C4A, 'X'), + (0x1C4D, 'V'), + (0x1C80, 'M', 'в'), + ] + +def _seg_15() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1C81, 'M', 'д'), + (0x1C82, 'M', 'о'), + (0x1C83, 'M', 'с'), + (0x1C84, 'M', 'т'), + (0x1C86, 'M', 'ъ'), + (0x1C87, 'M', 'ѣ'), + (0x1C88, 'M', 'ꙋ'), + (0x1C89, 'X'), + (0x1C90, 'M', 'ა'), + (0x1C91, 'M', 'ბ'), + (0x1C92, 'M', 'გ'), + (0x1C93, 'M', 'დ'), + (0x1C94, 'M', 'ე'), + (0x1C95, 'M', 'ვ'), + (0x1C96, 'M', 'ზ'), + (0x1C97, 'M', 'თ'), + (0x1C98, 'M', 'ი'), + (0x1C99, 'M', 'კ'), + (0x1C9A, 'M', 'ლ'), + (0x1C9B, 'M', 'მ'), + (0x1C9C, 'M', 'ნ'), + (0x1C9D, 'M', 'ო'), + (0x1C9E, 'M', 'პ'), + (0x1C9F, 'M', 'ჟ'), + (0x1CA0, 'M', 'რ'), + (0x1CA1, 'M', 'ს'), + (0x1CA2, 'M', 'ტ'), + (0x1CA3, 'M', 'უ'), + (0x1CA4, 'M', 'ფ'), + (0x1CA5, 'M', 'ქ'), + (0x1CA6, 'M', 'ღ'), + (0x1CA7, 'M', 'ყ'), + (0x1CA8, 'M', 'შ'), + (0x1CA9, 'M', 'ჩ'), + (0x1CAA, 'M', 'ც'), + (0x1CAB, 'M', 'ძ'), + (0x1CAC, 'M', 'წ'), + (0x1CAD, 'M', 'ჭ'), + (0x1CAE, 'M', 'ხ'), + (0x1CAF, 'M', 'ჯ'), + (0x1CB0, 'M', 'ჰ'), + (0x1CB1, 'M', 'ჱ'), + (0x1CB2, 'M', 'ჲ'), + (0x1CB3, 'M', 'ჳ'), + (0x1CB4, 'M', 'ჴ'), + (0x1CB5, 'M', 'ჵ'), + (0x1CB6, 'M', 'ჶ'), + (0x1CB7, 'M', 'ჷ'), + (0x1CB8, 'M', 'ჸ'), + (0x1CB9, 'M', 'ჹ'), + (0x1CBA, 'M', 'ჺ'), + (0x1CBB, 'X'), + (0x1CBD, 'M', 'ჽ'), + (0x1CBE, 'M', 'ჾ'), + (0x1CBF, 'M', 'ჿ'), + (0x1CC0, 'V'), + (0x1CC8, 'X'), + (0x1CD0, 'V'), + (0x1CFB, 'X'), + (0x1D00, 'V'), + (0x1D2C, 'M', 'a'), + (0x1D2D, 'M', 'æ'), + (0x1D2E, 'M', 'b'), + (0x1D2F, 'V'), + (0x1D30, 'M', 'd'), + (0x1D31, 'M', 'e'), + (0x1D32, 'M', 'ǝ'), + (0x1D33, 'M', 'g'), + (0x1D34, 'M', 'h'), + (0x1D35, 'M', 'i'), + (0x1D36, 'M', 'j'), + (0x1D37, 'M', 'k'), + (0x1D38, 'M', 'l'), + (0x1D39, 'M', 'm'), + (0x1D3A, 'M', 'n'), + (0x1D3B, 'V'), + (0x1D3C, 'M', 'o'), + (0x1D3D, 'M', 'ȣ'), + (0x1D3E, 'M', 'p'), + (0x1D3F, 'M', 'r'), + (0x1D40, 'M', 't'), + (0x1D41, 'M', 'u'), + (0x1D42, 'M', 'w'), + (0x1D43, 'M', 'a'), + (0x1D44, 'M', 'ɐ'), + (0x1D45, 'M', 'ɑ'), + (0x1D46, 'M', 'ᴂ'), + (0x1D47, 'M', 'b'), + (0x1D48, 'M', 'd'), + (0x1D49, 'M', 'e'), + (0x1D4A, 'M', 'ə'), + (0x1D4B, 'M', 'ɛ'), + (0x1D4C, 'M', 'ɜ'), + (0x1D4D, 'M', 'g'), + (0x1D4E, 'V'), + (0x1D4F, 'M', 'k'), + (0x1D50, 'M', 'm'), + (0x1D51, 'M', 'ŋ'), + (0x1D52, 'M', 'o'), + (0x1D53, 'M', 'ɔ'), + ] + +def _seg_16() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D54, 'M', 'ᴖ'), + (0x1D55, 'M', 'ᴗ'), + (0x1D56, 'M', 'p'), + (0x1D57, 'M', 't'), + (0x1D58, 'M', 'u'), + (0x1D59, 'M', 'ᴝ'), + (0x1D5A, 'M', 'ɯ'), + (0x1D5B, 'M', 'v'), + (0x1D5C, 'M', 'ᴥ'), + (0x1D5D, 'M', 'β'), + (0x1D5E, 'M', 'γ'), + (0x1D5F, 'M', 'δ'), + (0x1D60, 'M', 'φ'), + (0x1D61, 'M', 'χ'), + (0x1D62, 'M', 'i'), + (0x1D63, 'M', 'r'), + (0x1D64, 'M', 'u'), + (0x1D65, 'M', 'v'), + (0x1D66, 'M', 'β'), + (0x1D67, 'M', 'γ'), + (0x1D68, 'M', 'ρ'), + (0x1D69, 'M', 'φ'), + (0x1D6A, 'M', 'χ'), + (0x1D6B, 'V'), + (0x1D78, 'M', 'н'), + (0x1D79, 'V'), + (0x1D9B, 'M', 'ɒ'), + (0x1D9C, 'M', 'c'), + (0x1D9D, 'M', 'ɕ'), + (0x1D9E, 'M', 'ð'), + (0x1D9F, 'M', 'ɜ'), + (0x1DA0, 'M', 'f'), + (0x1DA1, 'M', 'ɟ'), + (0x1DA2, 'M', 'ɡ'), + (0x1DA3, 'M', 'ɥ'), + (0x1DA4, 'M', 'ɨ'), + (0x1DA5, 'M', 'ɩ'), + (0x1DA6, 'M', 'ɪ'), + (0x1DA7, 'M', 'ᵻ'), + (0x1DA8, 'M', 'ʝ'), + (0x1DA9, 'M', 'ɭ'), + (0x1DAA, 'M', 'ᶅ'), + (0x1DAB, 'M', 'ʟ'), + (0x1DAC, 'M', 'ɱ'), + (0x1DAD, 'M', 'ɰ'), + (0x1DAE, 'M', 'ɲ'), + (0x1DAF, 'M', 'ɳ'), + (0x1DB0, 'M', 'ɴ'), + (0x1DB1, 'M', 'ɵ'), + (0x1DB2, 'M', 'ɸ'), + (0x1DB3, 'M', 'ʂ'), + (0x1DB4, 'M', 'ʃ'), + (0x1DB5, 'M', 'ƫ'), + (0x1DB6, 'M', 'ʉ'), + (0x1DB7, 'M', 'ʊ'), + (0x1DB8, 'M', 'ᴜ'), + (0x1DB9, 'M', 'ʋ'), + (0x1DBA, 'M', 'ʌ'), + (0x1DBB, 'M', 'z'), + (0x1DBC, 'M', 'ʐ'), + (0x1DBD, 'M', 'ʑ'), + (0x1DBE, 'M', 'ʒ'), + (0x1DBF, 'M', 'θ'), + (0x1DC0, 'V'), + (0x1E00, 'M', 'ḁ'), + (0x1E01, 'V'), + (0x1E02, 'M', 'ḃ'), + (0x1E03, 'V'), + (0x1E04, 'M', 'ḅ'), + (0x1E05, 'V'), + (0x1E06, 'M', 'ḇ'), + (0x1E07, 'V'), + (0x1E08, 'M', 'ḉ'), + (0x1E09, 'V'), + (0x1E0A, 'M', 'ḋ'), + (0x1E0B, 'V'), + (0x1E0C, 'M', 'ḍ'), + (0x1E0D, 'V'), + (0x1E0E, 'M', 'ḏ'), + (0x1E0F, 'V'), + (0x1E10, 'M', 'ḑ'), + (0x1E11, 'V'), + (0x1E12, 'M', 'ḓ'), + (0x1E13, 'V'), + (0x1E14, 'M', 'ḕ'), + (0x1E15, 'V'), + (0x1E16, 'M', 'ḗ'), + (0x1E17, 'V'), + (0x1E18, 'M', 'ḙ'), + (0x1E19, 'V'), + (0x1E1A, 'M', 'ḛ'), + (0x1E1B, 'V'), + (0x1E1C, 'M', 'ḝ'), + (0x1E1D, 'V'), + (0x1E1E, 'M', 'ḟ'), + (0x1E1F, 'V'), + (0x1E20, 'M', 'ḡ'), + (0x1E21, 'V'), + (0x1E22, 'M', 'ḣ'), + (0x1E23, 'V'), + ] + +def _seg_17() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1E24, 'M', 'ḥ'), + (0x1E25, 'V'), + (0x1E26, 'M', 'ḧ'), + (0x1E27, 'V'), + (0x1E28, 'M', 'ḩ'), + (0x1E29, 'V'), + (0x1E2A, 'M', 'ḫ'), + (0x1E2B, 'V'), + (0x1E2C, 'M', 'ḭ'), + (0x1E2D, 'V'), + (0x1E2E, 'M', 'ḯ'), + (0x1E2F, 'V'), + (0x1E30, 'M', 'ḱ'), + (0x1E31, 'V'), + (0x1E32, 'M', 'ḳ'), + (0x1E33, 'V'), + (0x1E34, 'M', 'ḵ'), + (0x1E35, 'V'), + (0x1E36, 'M', 'ḷ'), + (0x1E37, 'V'), + (0x1E38, 'M', 'ḹ'), + (0x1E39, 'V'), + (0x1E3A, 'M', 'ḻ'), + (0x1E3B, 'V'), + (0x1E3C, 'M', 'ḽ'), + (0x1E3D, 'V'), + (0x1E3E, 'M', 'ḿ'), + (0x1E3F, 'V'), + (0x1E40, 'M', 'ṁ'), + (0x1E41, 'V'), + (0x1E42, 'M', 'ṃ'), + (0x1E43, 'V'), + (0x1E44, 'M', 'ṅ'), + (0x1E45, 'V'), + (0x1E46, 'M', 'ṇ'), + (0x1E47, 'V'), + (0x1E48, 'M', 'ṉ'), + (0x1E49, 'V'), + (0x1E4A, 'M', 'ṋ'), + (0x1E4B, 'V'), + (0x1E4C, 'M', 'ṍ'), + (0x1E4D, 'V'), + (0x1E4E, 'M', 'ṏ'), + (0x1E4F, 'V'), + (0x1E50, 'M', 'ṑ'), + (0x1E51, 'V'), + (0x1E52, 'M', 'ṓ'), + (0x1E53, 'V'), + (0x1E54, 'M', 'ṕ'), + (0x1E55, 'V'), + (0x1E56, 'M', 'ṗ'), + (0x1E57, 'V'), + (0x1E58, 'M', 'ṙ'), + (0x1E59, 'V'), + (0x1E5A, 'M', 'ṛ'), + (0x1E5B, 'V'), + (0x1E5C, 'M', 'ṝ'), + (0x1E5D, 'V'), + (0x1E5E, 'M', 'ṟ'), + (0x1E5F, 'V'), + (0x1E60, 'M', 'ṡ'), + (0x1E61, 'V'), + (0x1E62, 'M', 'ṣ'), + (0x1E63, 'V'), + (0x1E64, 'M', 'ṥ'), + (0x1E65, 'V'), + (0x1E66, 'M', 'ṧ'), + (0x1E67, 'V'), + (0x1E68, 'M', 'ṩ'), + (0x1E69, 'V'), + (0x1E6A, 'M', 'ṫ'), + (0x1E6B, 'V'), + (0x1E6C, 'M', 'ṭ'), + (0x1E6D, 'V'), + (0x1E6E, 'M', 'ṯ'), + (0x1E6F, 'V'), + (0x1E70, 'M', 'ṱ'), + (0x1E71, 'V'), + (0x1E72, 'M', 'ṳ'), + (0x1E73, 'V'), + (0x1E74, 'M', 'ṵ'), + (0x1E75, 'V'), + (0x1E76, 'M', 'ṷ'), + (0x1E77, 'V'), + (0x1E78, 'M', 'ṹ'), + (0x1E79, 'V'), + (0x1E7A, 'M', 'ṻ'), + (0x1E7B, 'V'), + (0x1E7C, 'M', 'ṽ'), + (0x1E7D, 'V'), + (0x1E7E, 'M', 'ṿ'), + (0x1E7F, 'V'), + (0x1E80, 'M', 'ẁ'), + (0x1E81, 'V'), + (0x1E82, 'M', 'ẃ'), + (0x1E83, 'V'), + (0x1E84, 'M', 'ẅ'), + (0x1E85, 'V'), + (0x1E86, 'M', 'ẇ'), + (0x1E87, 'V'), + ] + +def _seg_18() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1E88, 'M', 'ẉ'), + (0x1E89, 'V'), + (0x1E8A, 'M', 'ẋ'), + (0x1E8B, 'V'), + (0x1E8C, 'M', 'ẍ'), + (0x1E8D, 'V'), + (0x1E8E, 'M', 'ẏ'), + (0x1E8F, 'V'), + (0x1E90, 'M', 'ẑ'), + (0x1E91, 'V'), + (0x1E92, 'M', 'ẓ'), + (0x1E93, 'V'), + (0x1E94, 'M', 'ẕ'), + (0x1E95, 'V'), + (0x1E9A, 'M', 'aʾ'), + (0x1E9B, 'M', 'ṡ'), + (0x1E9C, 'V'), + (0x1E9E, 'M', 'ss'), + (0x1E9F, 'V'), + (0x1EA0, 'M', 'ạ'), + (0x1EA1, 'V'), + (0x1EA2, 'M', 'ả'), + (0x1EA3, 'V'), + (0x1EA4, 'M', 'ấ'), + (0x1EA5, 'V'), + (0x1EA6, 'M', 'ầ'), + (0x1EA7, 'V'), + (0x1EA8, 'M', 'ẩ'), + (0x1EA9, 'V'), + (0x1EAA, 'M', 'ẫ'), + (0x1EAB, 'V'), + (0x1EAC, 'M', 'ậ'), + (0x1EAD, 'V'), + (0x1EAE, 'M', 'ắ'), + (0x1EAF, 'V'), + (0x1EB0, 'M', 'ằ'), + (0x1EB1, 'V'), + (0x1EB2, 'M', 'ẳ'), + (0x1EB3, 'V'), + (0x1EB4, 'M', 'ẵ'), + (0x1EB5, 'V'), + (0x1EB6, 'M', 'ặ'), + (0x1EB7, 'V'), + (0x1EB8, 'M', 'ẹ'), + (0x1EB9, 'V'), + (0x1EBA, 'M', 'ẻ'), + (0x1EBB, 'V'), + (0x1EBC, 'M', 'ẽ'), + (0x1EBD, 'V'), + (0x1EBE, 'M', 'ế'), + (0x1EBF, 'V'), + (0x1EC0, 'M', 'ề'), + (0x1EC1, 'V'), + (0x1EC2, 'M', 'ể'), + (0x1EC3, 'V'), + (0x1EC4, 'M', 'ễ'), + (0x1EC5, 'V'), + (0x1EC6, 'M', 'ệ'), + (0x1EC7, 'V'), + (0x1EC8, 'M', 'ỉ'), + (0x1EC9, 'V'), + (0x1ECA, 'M', 'ị'), + (0x1ECB, 'V'), + (0x1ECC, 'M', 'ọ'), + (0x1ECD, 'V'), + (0x1ECE, 'M', 'ỏ'), + (0x1ECF, 'V'), + (0x1ED0, 'M', 'ố'), + (0x1ED1, 'V'), + (0x1ED2, 'M', 'ồ'), + (0x1ED3, 'V'), + (0x1ED4, 'M', 'ổ'), + (0x1ED5, 'V'), + (0x1ED6, 'M', 'ỗ'), + (0x1ED7, 'V'), + (0x1ED8, 'M', 'ộ'), + (0x1ED9, 'V'), + (0x1EDA, 'M', 'ớ'), + (0x1EDB, 'V'), + (0x1EDC, 'M', 'ờ'), + (0x1EDD, 'V'), + (0x1EDE, 'M', 'ở'), + (0x1EDF, 'V'), + (0x1EE0, 'M', 'ỡ'), + (0x1EE1, 'V'), + (0x1EE2, 'M', 'ợ'), + (0x1EE3, 'V'), + (0x1EE4, 'M', 'ụ'), + (0x1EE5, 'V'), + (0x1EE6, 'M', 'ủ'), + (0x1EE7, 'V'), + (0x1EE8, 'M', 'ứ'), + (0x1EE9, 'V'), + (0x1EEA, 'M', 'ừ'), + (0x1EEB, 'V'), + (0x1EEC, 'M', 'ử'), + (0x1EED, 'V'), + (0x1EEE, 'M', 'ữ'), + (0x1EEF, 'V'), + (0x1EF0, 'M', 'ự'), + ] + +def _seg_19() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1EF1, 'V'), + (0x1EF2, 'M', 'ỳ'), + (0x1EF3, 'V'), + (0x1EF4, 'M', 'ỵ'), + (0x1EF5, 'V'), + (0x1EF6, 'M', 'ỷ'), + (0x1EF7, 'V'), + (0x1EF8, 'M', 'ỹ'), + (0x1EF9, 'V'), + (0x1EFA, 'M', 'ỻ'), + (0x1EFB, 'V'), + (0x1EFC, 'M', 'ỽ'), + (0x1EFD, 'V'), + (0x1EFE, 'M', 'ỿ'), + (0x1EFF, 'V'), + (0x1F08, 'M', 'ἀ'), + (0x1F09, 'M', 'ἁ'), + (0x1F0A, 'M', 'ἂ'), + (0x1F0B, 'M', 'ἃ'), + (0x1F0C, 'M', 'ἄ'), + (0x1F0D, 'M', 'ἅ'), + (0x1F0E, 'M', 'ἆ'), + (0x1F0F, 'M', 'ἇ'), + (0x1F10, 'V'), + (0x1F16, 'X'), + (0x1F18, 'M', 'ἐ'), + (0x1F19, 'M', 'ἑ'), + (0x1F1A, 'M', 'ἒ'), + (0x1F1B, 'M', 'ἓ'), + (0x1F1C, 'M', 'ἔ'), + (0x1F1D, 'M', 'ἕ'), + (0x1F1E, 'X'), + (0x1F20, 'V'), + (0x1F28, 'M', 'ἠ'), + (0x1F29, 'M', 'ἡ'), + (0x1F2A, 'M', 'ἢ'), + (0x1F2B, 'M', 'ἣ'), + (0x1F2C, 'M', 'ἤ'), + (0x1F2D, 'M', 'ἥ'), + (0x1F2E, 'M', 'ἦ'), + (0x1F2F, 'M', 'ἧ'), + (0x1F30, 'V'), + (0x1F38, 'M', 'ἰ'), + (0x1F39, 'M', 'ἱ'), + (0x1F3A, 'M', 'ἲ'), + (0x1F3B, 'M', 'ἳ'), + (0x1F3C, 'M', 'ἴ'), + (0x1F3D, 'M', 'ἵ'), + (0x1F3E, 'M', 'ἶ'), + (0x1F3F, 'M', 'ἷ'), + (0x1F40, 'V'), + (0x1F46, 'X'), + (0x1F48, 'M', 'ὀ'), + (0x1F49, 'M', 'ὁ'), + (0x1F4A, 'M', 'ὂ'), + (0x1F4B, 'M', 'ὃ'), + (0x1F4C, 'M', 'ὄ'), + (0x1F4D, 'M', 'ὅ'), + (0x1F4E, 'X'), + (0x1F50, 'V'), + (0x1F58, 'X'), + (0x1F59, 'M', 'ὑ'), + (0x1F5A, 'X'), + (0x1F5B, 'M', 'ὓ'), + (0x1F5C, 'X'), + (0x1F5D, 'M', 'ὕ'), + (0x1F5E, 'X'), + (0x1F5F, 'M', 'ὗ'), + (0x1F60, 'V'), + (0x1F68, 'M', 'ὠ'), + (0x1F69, 'M', 'ὡ'), + (0x1F6A, 'M', 'ὢ'), + (0x1F6B, 'M', 'ὣ'), + (0x1F6C, 'M', 'ὤ'), + (0x1F6D, 'M', 'ὥ'), + (0x1F6E, 'M', 'ὦ'), + (0x1F6F, 'M', 'ὧ'), + (0x1F70, 'V'), + (0x1F71, 'M', 'ά'), + (0x1F72, 'V'), + (0x1F73, 'M', 'έ'), + (0x1F74, 'V'), + (0x1F75, 'M', 'ή'), + (0x1F76, 'V'), + (0x1F77, 'M', 'ί'), + (0x1F78, 'V'), + (0x1F79, 'M', 'ό'), + (0x1F7A, 'V'), + (0x1F7B, 'M', 'ύ'), + (0x1F7C, 'V'), + (0x1F7D, 'M', 'ώ'), + (0x1F7E, 'X'), + (0x1F80, 'M', 'ἀι'), + (0x1F81, 'M', 'ἁι'), + (0x1F82, 'M', 'ἂι'), + (0x1F83, 'M', 'ἃι'), + (0x1F84, 'M', 'ἄι'), + (0x1F85, 'M', 'ἅι'), + (0x1F86, 'M', 'ἆι'), + (0x1F87, 'M', 'ἇι'), + ] + +def _seg_20() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1F88, 'M', 'ἀι'), + (0x1F89, 'M', 'ἁι'), + (0x1F8A, 'M', 'ἂι'), + (0x1F8B, 'M', 'ἃι'), + (0x1F8C, 'M', 'ἄι'), + (0x1F8D, 'M', 'ἅι'), + (0x1F8E, 'M', 'ἆι'), + (0x1F8F, 'M', 'ἇι'), + (0x1F90, 'M', 'ἠι'), + (0x1F91, 'M', 'ἡι'), + (0x1F92, 'M', 'ἢι'), + (0x1F93, 'M', 'ἣι'), + (0x1F94, 'M', 'ἤι'), + (0x1F95, 'M', 'ἥι'), + (0x1F96, 'M', 'ἦι'), + (0x1F97, 'M', 'ἧι'), + (0x1F98, 'M', 'ἠι'), + (0x1F99, 'M', 'ἡι'), + (0x1F9A, 'M', 'ἢι'), + (0x1F9B, 'M', 'ἣι'), + (0x1F9C, 'M', 'ἤι'), + (0x1F9D, 'M', 'ἥι'), + (0x1F9E, 'M', 'ἦι'), + (0x1F9F, 'M', 'ἧι'), + (0x1FA0, 'M', 'ὠι'), + (0x1FA1, 'M', 'ὡι'), + (0x1FA2, 'M', 'ὢι'), + (0x1FA3, 'M', 'ὣι'), + (0x1FA4, 'M', 'ὤι'), + (0x1FA5, 'M', 'ὥι'), + (0x1FA6, 'M', 'ὦι'), + (0x1FA7, 'M', 'ὧι'), + (0x1FA8, 'M', 'ὠι'), + (0x1FA9, 'M', 'ὡι'), + (0x1FAA, 'M', 'ὢι'), + (0x1FAB, 'M', 'ὣι'), + (0x1FAC, 'M', 'ὤι'), + (0x1FAD, 'M', 'ὥι'), + (0x1FAE, 'M', 'ὦι'), + (0x1FAF, 'M', 'ὧι'), + (0x1FB0, 'V'), + (0x1FB2, 'M', 'ὰι'), + (0x1FB3, 'M', 'αι'), + (0x1FB4, 'M', 'άι'), + (0x1FB5, 'X'), + (0x1FB6, 'V'), + (0x1FB7, 'M', 'ᾶι'), + (0x1FB8, 'M', 'ᾰ'), + (0x1FB9, 'M', 'ᾱ'), + (0x1FBA, 'M', 'ὰ'), + (0x1FBB, 'M', 'ά'), + (0x1FBC, 'M', 'αι'), + (0x1FBD, '3', ' ̓'), + (0x1FBE, 'M', 'ι'), + (0x1FBF, '3', ' ̓'), + (0x1FC0, '3', ' ͂'), + (0x1FC1, '3', ' ̈͂'), + (0x1FC2, 'M', 'ὴι'), + (0x1FC3, 'M', 'ηι'), + (0x1FC4, 'M', 'ήι'), + (0x1FC5, 'X'), + (0x1FC6, 'V'), + (0x1FC7, 'M', 'ῆι'), + (0x1FC8, 'M', 'ὲ'), + (0x1FC9, 'M', 'έ'), + (0x1FCA, 'M', 'ὴ'), + (0x1FCB, 'M', 'ή'), + (0x1FCC, 'M', 'ηι'), + (0x1FCD, '3', ' ̓̀'), + (0x1FCE, '3', ' ̓́'), + (0x1FCF, '3', ' ̓͂'), + (0x1FD0, 'V'), + (0x1FD3, 'M', 'ΐ'), + (0x1FD4, 'X'), + (0x1FD6, 'V'), + (0x1FD8, 'M', 'ῐ'), + (0x1FD9, 'M', 'ῑ'), + (0x1FDA, 'M', 'ὶ'), + (0x1FDB, 'M', 'ί'), + (0x1FDC, 'X'), + (0x1FDD, '3', ' ̔̀'), + (0x1FDE, '3', ' ̔́'), + (0x1FDF, '3', ' ̔͂'), + (0x1FE0, 'V'), + (0x1FE3, 'M', 'ΰ'), + (0x1FE4, 'V'), + (0x1FE8, 'M', 'ῠ'), + (0x1FE9, 'M', 'ῡ'), + (0x1FEA, 'M', 'ὺ'), + (0x1FEB, 'M', 'ύ'), + (0x1FEC, 'M', 'ῥ'), + (0x1FED, '3', ' ̈̀'), + (0x1FEE, '3', ' ̈́'), + (0x1FEF, '3', '`'), + (0x1FF0, 'X'), + (0x1FF2, 'M', 'ὼι'), + (0x1FF3, 'M', 'ωι'), + (0x1FF4, 'M', 'ώι'), + (0x1FF5, 'X'), + (0x1FF6, 'V'), + ] + +def _seg_21() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1FF7, 'M', 'ῶι'), + (0x1FF8, 'M', 'ὸ'), + (0x1FF9, 'M', 'ό'), + (0x1FFA, 'M', 'ὼ'), + (0x1FFB, 'M', 'ώ'), + (0x1FFC, 'M', 'ωι'), + (0x1FFD, '3', ' ́'), + (0x1FFE, '3', ' ̔'), + (0x1FFF, 'X'), + (0x2000, '3', ' '), + (0x200B, 'I'), + (0x200C, 'D', ''), + (0x200E, 'X'), + (0x2010, 'V'), + (0x2011, 'M', '‐'), + (0x2012, 'V'), + (0x2017, '3', ' ̳'), + (0x2018, 'V'), + (0x2024, 'X'), + (0x2027, 'V'), + (0x2028, 'X'), + (0x202F, '3', ' '), + (0x2030, 'V'), + (0x2033, 'M', '′′'), + (0x2034, 'M', '′′′'), + (0x2035, 'V'), + (0x2036, 'M', '‵‵'), + (0x2037, 'M', '‵‵‵'), + (0x2038, 'V'), + (0x203C, '3', '!!'), + (0x203D, 'V'), + (0x203E, '3', ' ̅'), + (0x203F, 'V'), + (0x2047, '3', '??'), + (0x2048, '3', '?!'), + (0x2049, '3', '!?'), + (0x204A, 'V'), + (0x2057, 'M', '′′′′'), + (0x2058, 'V'), + (0x205F, '3', ' '), + (0x2060, 'I'), + (0x2061, 'X'), + (0x2064, 'I'), + (0x2065, 'X'), + (0x2070, 'M', '0'), + (0x2071, 'M', 'i'), + (0x2072, 'X'), + (0x2074, 'M', '4'), + (0x2075, 'M', '5'), + (0x2076, 'M', '6'), + (0x2077, 'M', '7'), + (0x2078, 'M', '8'), + (0x2079, 'M', '9'), + (0x207A, '3', '+'), + (0x207B, 'M', '−'), + (0x207C, '3', '='), + (0x207D, '3', '('), + (0x207E, '3', ')'), + (0x207F, 'M', 'n'), + (0x2080, 'M', '0'), + (0x2081, 'M', '1'), + (0x2082, 'M', '2'), + (0x2083, 'M', '3'), + (0x2084, 'M', '4'), + (0x2085, 'M', '5'), + (0x2086, 'M', '6'), + (0x2087, 'M', '7'), + (0x2088, 'M', '8'), + (0x2089, 'M', '9'), + (0x208A, '3', '+'), + (0x208B, 'M', '−'), + (0x208C, '3', '='), + (0x208D, '3', '('), + (0x208E, '3', ')'), + (0x208F, 'X'), + (0x2090, 'M', 'a'), + (0x2091, 'M', 'e'), + (0x2092, 'M', 'o'), + (0x2093, 'M', 'x'), + (0x2094, 'M', 'ə'), + (0x2095, 'M', 'h'), + (0x2096, 'M', 'k'), + (0x2097, 'M', 'l'), + (0x2098, 'M', 'm'), + (0x2099, 'M', 'n'), + (0x209A, 'M', 'p'), + (0x209B, 'M', 's'), + (0x209C, 'M', 't'), + (0x209D, 'X'), + (0x20A0, 'V'), + (0x20A8, 'M', 'rs'), + (0x20A9, 'V'), + (0x20C1, 'X'), + (0x20D0, 'V'), + (0x20F1, 'X'), + (0x2100, '3', 'a/c'), + (0x2101, '3', 'a/s'), + (0x2102, 'M', 'c'), + (0x2103, 'M', '°c'), + (0x2104, 'V'), + ] + +def _seg_22() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2105, '3', 'c/o'), + (0x2106, '3', 'c/u'), + (0x2107, 'M', 'ɛ'), + (0x2108, 'V'), + (0x2109, 'M', '°f'), + (0x210A, 'M', 'g'), + (0x210B, 'M', 'h'), + (0x210F, 'M', 'ħ'), + (0x2110, 'M', 'i'), + (0x2112, 'M', 'l'), + (0x2114, 'V'), + (0x2115, 'M', 'n'), + (0x2116, 'M', 'no'), + (0x2117, 'V'), + (0x2119, 'M', 'p'), + (0x211A, 'M', 'q'), + (0x211B, 'M', 'r'), + (0x211E, 'V'), + (0x2120, 'M', 'sm'), + (0x2121, 'M', 'tel'), + (0x2122, 'M', 'tm'), + (0x2123, 'V'), + (0x2124, 'M', 'z'), + (0x2125, 'V'), + (0x2126, 'M', 'ω'), + (0x2127, 'V'), + (0x2128, 'M', 'z'), + (0x2129, 'V'), + (0x212A, 'M', 'k'), + (0x212B, 'M', 'å'), + (0x212C, 'M', 'b'), + (0x212D, 'M', 'c'), + (0x212E, 'V'), + (0x212F, 'M', 'e'), + (0x2131, 'M', 'f'), + (0x2132, 'X'), + (0x2133, 'M', 'm'), + (0x2134, 'M', 'o'), + (0x2135, 'M', 'א'), + (0x2136, 'M', 'ב'), + (0x2137, 'M', 'ג'), + (0x2138, 'M', 'ד'), + (0x2139, 'M', 'i'), + (0x213A, 'V'), + (0x213B, 'M', 'fax'), + (0x213C, 'M', 'π'), + (0x213D, 'M', 'γ'), + (0x213F, 'M', 'π'), + (0x2140, 'M', '∑'), + (0x2141, 'V'), + (0x2145, 'M', 'd'), + (0x2147, 'M', 'e'), + (0x2148, 'M', 'i'), + (0x2149, 'M', 'j'), + (0x214A, 'V'), + (0x2150, 'M', '1⁄7'), + (0x2151, 'M', '1⁄9'), + (0x2152, 'M', '1⁄10'), + (0x2153, 'M', '1⁄3'), + (0x2154, 'M', '2⁄3'), + (0x2155, 'M', '1⁄5'), + (0x2156, 'M', '2⁄5'), + (0x2157, 'M', '3⁄5'), + (0x2158, 'M', '4⁄5'), + (0x2159, 'M', '1⁄6'), + (0x215A, 'M', '5⁄6'), + (0x215B, 'M', '1⁄8'), + (0x215C, 'M', '3⁄8'), + (0x215D, 'M', '5⁄8'), + (0x215E, 'M', '7⁄8'), + (0x215F, 'M', '1⁄'), + (0x2160, 'M', 'i'), + (0x2161, 'M', 'ii'), + (0x2162, 'M', 'iii'), + (0x2163, 'M', 'iv'), + (0x2164, 'M', 'v'), + (0x2165, 'M', 'vi'), + (0x2166, 'M', 'vii'), + (0x2167, 'M', 'viii'), + (0x2168, 'M', 'ix'), + (0x2169, 'M', 'x'), + (0x216A, 'M', 'xi'), + (0x216B, 'M', 'xii'), + (0x216C, 'M', 'l'), + (0x216D, 'M', 'c'), + (0x216E, 'M', 'd'), + (0x216F, 'M', 'm'), + (0x2170, 'M', 'i'), + (0x2171, 'M', 'ii'), + (0x2172, 'M', 'iii'), + (0x2173, 'M', 'iv'), + (0x2174, 'M', 'v'), + (0x2175, 'M', 'vi'), + (0x2176, 'M', 'vii'), + (0x2177, 'M', 'viii'), + (0x2178, 'M', 'ix'), + (0x2179, 'M', 'x'), + (0x217A, 'M', 'xi'), + (0x217B, 'M', 'xii'), + (0x217C, 'M', 'l'), + ] + +def _seg_23() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x217D, 'M', 'c'), + (0x217E, 'M', 'd'), + (0x217F, 'M', 'm'), + (0x2180, 'V'), + (0x2183, 'X'), + (0x2184, 'V'), + (0x2189, 'M', '0⁄3'), + (0x218A, 'V'), + (0x218C, 'X'), + (0x2190, 'V'), + (0x222C, 'M', '∫∫'), + (0x222D, 'M', '∫∫∫'), + (0x222E, 'V'), + (0x222F, 'M', '∮∮'), + (0x2230, 'M', '∮∮∮'), + (0x2231, 'V'), + (0x2260, '3'), + (0x2261, 'V'), + (0x226E, '3'), + (0x2270, 'V'), + (0x2329, 'M', '〈'), + (0x232A, 'M', '〉'), + (0x232B, 'V'), + (0x2427, 'X'), + (0x2440, 'V'), + (0x244B, 'X'), + (0x2460, 'M', '1'), + (0x2461, 'M', '2'), + (0x2462, 'M', '3'), + (0x2463, 'M', '4'), + (0x2464, 'M', '5'), + (0x2465, 'M', '6'), + (0x2466, 'M', '7'), + (0x2467, 'M', '8'), + (0x2468, 'M', '9'), + (0x2469, 'M', '10'), + (0x246A, 'M', '11'), + (0x246B, 'M', '12'), + (0x246C, 'M', '13'), + (0x246D, 'M', '14'), + (0x246E, 'M', '15'), + (0x246F, 'M', '16'), + (0x2470, 'M', '17'), + (0x2471, 'M', '18'), + (0x2472, 'M', '19'), + (0x2473, 'M', '20'), + (0x2474, '3', '(1)'), + (0x2475, '3', '(2)'), + (0x2476, '3', '(3)'), + (0x2477, '3', '(4)'), + (0x2478, '3', '(5)'), + (0x2479, '3', '(6)'), + (0x247A, '3', '(7)'), + (0x247B, '3', '(8)'), + (0x247C, '3', '(9)'), + (0x247D, '3', '(10)'), + (0x247E, '3', '(11)'), + (0x247F, '3', '(12)'), + (0x2480, '3', '(13)'), + (0x2481, '3', '(14)'), + (0x2482, '3', '(15)'), + (0x2483, '3', '(16)'), + (0x2484, '3', '(17)'), + (0x2485, '3', '(18)'), + (0x2486, '3', '(19)'), + (0x2487, '3', '(20)'), + (0x2488, 'X'), + (0x249C, '3', '(a)'), + (0x249D, '3', '(b)'), + (0x249E, '3', '(c)'), + (0x249F, '3', '(d)'), + (0x24A0, '3', '(e)'), + (0x24A1, '3', '(f)'), + (0x24A2, '3', '(g)'), + (0x24A3, '3', '(h)'), + (0x24A4, '3', '(i)'), + (0x24A5, '3', '(j)'), + (0x24A6, '3', '(k)'), + (0x24A7, '3', '(l)'), + (0x24A8, '3', '(m)'), + (0x24A9, '3', '(n)'), + (0x24AA, '3', '(o)'), + (0x24AB, '3', '(p)'), + (0x24AC, '3', '(q)'), + (0x24AD, '3', '(r)'), + (0x24AE, '3', '(s)'), + (0x24AF, '3', '(t)'), + (0x24B0, '3', '(u)'), + (0x24B1, '3', '(v)'), + (0x24B2, '3', '(w)'), + (0x24B3, '3', '(x)'), + (0x24B4, '3', '(y)'), + (0x24B5, '3', '(z)'), + (0x24B6, 'M', 'a'), + (0x24B7, 'M', 'b'), + (0x24B8, 'M', 'c'), + (0x24B9, 'M', 'd'), + (0x24BA, 'M', 'e'), + (0x24BB, 'M', 'f'), + (0x24BC, 'M', 'g'), + ] + +def _seg_24() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x24BD, 'M', 'h'), + (0x24BE, 'M', 'i'), + (0x24BF, 'M', 'j'), + (0x24C0, 'M', 'k'), + (0x24C1, 'M', 'l'), + (0x24C2, 'M', 'm'), + (0x24C3, 'M', 'n'), + (0x24C4, 'M', 'o'), + (0x24C5, 'M', 'p'), + (0x24C6, 'M', 'q'), + (0x24C7, 'M', 'r'), + (0x24C8, 'M', 's'), + (0x24C9, 'M', 't'), + (0x24CA, 'M', 'u'), + (0x24CB, 'M', 'v'), + (0x24CC, 'M', 'w'), + (0x24CD, 'M', 'x'), + (0x24CE, 'M', 'y'), + (0x24CF, 'M', 'z'), + (0x24D0, 'M', 'a'), + (0x24D1, 'M', 'b'), + (0x24D2, 'M', 'c'), + (0x24D3, 'M', 'd'), + (0x24D4, 'M', 'e'), + (0x24D5, 'M', 'f'), + (0x24D6, 'M', 'g'), + (0x24D7, 'M', 'h'), + (0x24D8, 'M', 'i'), + (0x24D9, 'M', 'j'), + (0x24DA, 'M', 'k'), + (0x24DB, 'M', 'l'), + (0x24DC, 'M', 'm'), + (0x24DD, 'M', 'n'), + (0x24DE, 'M', 'o'), + (0x24DF, 'M', 'p'), + (0x24E0, 'M', 'q'), + (0x24E1, 'M', 'r'), + (0x24E2, 'M', 's'), + (0x24E3, 'M', 't'), + (0x24E4, 'M', 'u'), + (0x24E5, 'M', 'v'), + (0x24E6, 'M', 'w'), + (0x24E7, 'M', 'x'), + (0x24E8, 'M', 'y'), + (0x24E9, 'M', 'z'), + (0x24EA, 'M', '0'), + (0x24EB, 'V'), + (0x2A0C, 'M', '∫∫∫∫'), + (0x2A0D, 'V'), + (0x2A74, '3', '::='), + (0x2A75, '3', '=='), + (0x2A76, '3', '==='), + (0x2A77, 'V'), + (0x2ADC, 'M', '⫝̸'), + (0x2ADD, 'V'), + (0x2B74, 'X'), + (0x2B76, 'V'), + (0x2B96, 'X'), + (0x2B97, 'V'), + (0x2C00, 'M', 'ⰰ'), + (0x2C01, 'M', 'ⰱ'), + (0x2C02, 'M', 'ⰲ'), + (0x2C03, 'M', 'ⰳ'), + (0x2C04, 'M', 'ⰴ'), + (0x2C05, 'M', 'ⰵ'), + (0x2C06, 'M', 'ⰶ'), + (0x2C07, 'M', 'ⰷ'), + (0x2C08, 'M', 'ⰸ'), + (0x2C09, 'M', 'ⰹ'), + (0x2C0A, 'M', 'ⰺ'), + (0x2C0B, 'M', 'ⰻ'), + (0x2C0C, 'M', 'ⰼ'), + (0x2C0D, 'M', 'ⰽ'), + (0x2C0E, 'M', 'ⰾ'), + (0x2C0F, 'M', 'ⰿ'), + (0x2C10, 'M', 'ⱀ'), + (0x2C11, 'M', 'ⱁ'), + (0x2C12, 'M', 'ⱂ'), + (0x2C13, 'M', 'ⱃ'), + (0x2C14, 'M', 'ⱄ'), + (0x2C15, 'M', 'ⱅ'), + (0x2C16, 'M', 'ⱆ'), + (0x2C17, 'M', 'ⱇ'), + (0x2C18, 'M', 'ⱈ'), + (0x2C19, 'M', 'ⱉ'), + (0x2C1A, 'M', 'ⱊ'), + (0x2C1B, 'M', 'ⱋ'), + (0x2C1C, 'M', 'ⱌ'), + (0x2C1D, 'M', 'ⱍ'), + (0x2C1E, 'M', 'ⱎ'), + (0x2C1F, 'M', 'ⱏ'), + (0x2C20, 'M', 'ⱐ'), + (0x2C21, 'M', 'ⱑ'), + (0x2C22, 'M', 'ⱒ'), + (0x2C23, 'M', 'ⱓ'), + (0x2C24, 'M', 'ⱔ'), + (0x2C25, 'M', 'ⱕ'), + (0x2C26, 'M', 'ⱖ'), + (0x2C27, 'M', 'ⱗ'), + (0x2C28, 'M', 'ⱘ'), + ] + +def _seg_25() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2C29, 'M', 'ⱙ'), + (0x2C2A, 'M', 'ⱚ'), + (0x2C2B, 'M', 'ⱛ'), + (0x2C2C, 'M', 'ⱜ'), + (0x2C2D, 'M', 'ⱝ'), + (0x2C2E, 'M', 'ⱞ'), + (0x2C2F, 'M', 'ⱟ'), + (0x2C30, 'V'), + (0x2C60, 'M', 'ⱡ'), + (0x2C61, 'V'), + (0x2C62, 'M', 'ɫ'), + (0x2C63, 'M', 'ᵽ'), + (0x2C64, 'M', 'ɽ'), + (0x2C65, 'V'), + (0x2C67, 'M', 'ⱨ'), + (0x2C68, 'V'), + (0x2C69, 'M', 'ⱪ'), + (0x2C6A, 'V'), + (0x2C6B, 'M', 'ⱬ'), + (0x2C6C, 'V'), + (0x2C6D, 'M', 'ɑ'), + (0x2C6E, 'M', 'ɱ'), + (0x2C6F, 'M', 'ɐ'), + (0x2C70, 'M', 'ɒ'), + (0x2C71, 'V'), + (0x2C72, 'M', 'ⱳ'), + (0x2C73, 'V'), + (0x2C75, 'M', 'ⱶ'), + (0x2C76, 'V'), + (0x2C7C, 'M', 'j'), + (0x2C7D, 'M', 'v'), + (0x2C7E, 'M', 'ȿ'), + (0x2C7F, 'M', 'ɀ'), + (0x2C80, 'M', 'ⲁ'), + (0x2C81, 'V'), + (0x2C82, 'M', 'ⲃ'), + (0x2C83, 'V'), + (0x2C84, 'M', 'ⲅ'), + (0x2C85, 'V'), + (0x2C86, 'M', 'ⲇ'), + (0x2C87, 'V'), + (0x2C88, 'M', 'ⲉ'), + (0x2C89, 'V'), + (0x2C8A, 'M', 'ⲋ'), + (0x2C8B, 'V'), + (0x2C8C, 'M', 'ⲍ'), + (0x2C8D, 'V'), + (0x2C8E, 'M', 'ⲏ'), + (0x2C8F, 'V'), + (0x2C90, 'M', 'ⲑ'), + (0x2C91, 'V'), + (0x2C92, 'M', 'ⲓ'), + (0x2C93, 'V'), + (0x2C94, 'M', 'ⲕ'), + (0x2C95, 'V'), + (0x2C96, 'M', 'ⲗ'), + (0x2C97, 'V'), + (0x2C98, 'M', 'ⲙ'), + (0x2C99, 'V'), + (0x2C9A, 'M', 'ⲛ'), + (0x2C9B, 'V'), + (0x2C9C, 'M', 'ⲝ'), + (0x2C9D, 'V'), + (0x2C9E, 'M', 'ⲟ'), + (0x2C9F, 'V'), + (0x2CA0, 'M', 'ⲡ'), + (0x2CA1, 'V'), + (0x2CA2, 'M', 'ⲣ'), + (0x2CA3, 'V'), + (0x2CA4, 'M', 'ⲥ'), + (0x2CA5, 'V'), + (0x2CA6, 'M', 'ⲧ'), + (0x2CA7, 'V'), + (0x2CA8, 'M', 'ⲩ'), + (0x2CA9, 'V'), + (0x2CAA, 'M', 'ⲫ'), + (0x2CAB, 'V'), + (0x2CAC, 'M', 'ⲭ'), + (0x2CAD, 'V'), + (0x2CAE, 'M', 'ⲯ'), + (0x2CAF, 'V'), + (0x2CB0, 'M', 'ⲱ'), + (0x2CB1, 'V'), + (0x2CB2, 'M', 'ⲳ'), + (0x2CB3, 'V'), + (0x2CB4, 'M', 'ⲵ'), + (0x2CB5, 'V'), + (0x2CB6, 'M', 'ⲷ'), + (0x2CB7, 'V'), + (0x2CB8, 'M', 'ⲹ'), + (0x2CB9, 'V'), + (0x2CBA, 'M', 'ⲻ'), + (0x2CBB, 'V'), + (0x2CBC, 'M', 'ⲽ'), + (0x2CBD, 'V'), + (0x2CBE, 'M', 'ⲿ'), + (0x2CBF, 'V'), + (0x2CC0, 'M', 'ⳁ'), + (0x2CC1, 'V'), + (0x2CC2, 'M', 'ⳃ'), + ] + +def _seg_26() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2CC3, 'V'), + (0x2CC4, 'M', 'ⳅ'), + (0x2CC5, 'V'), + (0x2CC6, 'M', 'ⳇ'), + (0x2CC7, 'V'), + (0x2CC8, 'M', 'ⳉ'), + (0x2CC9, 'V'), + (0x2CCA, 'M', 'ⳋ'), + (0x2CCB, 'V'), + (0x2CCC, 'M', 'ⳍ'), + (0x2CCD, 'V'), + (0x2CCE, 'M', 'ⳏ'), + (0x2CCF, 'V'), + (0x2CD0, 'M', 'ⳑ'), + (0x2CD1, 'V'), + (0x2CD2, 'M', 'ⳓ'), + (0x2CD3, 'V'), + (0x2CD4, 'M', 'ⳕ'), + (0x2CD5, 'V'), + (0x2CD6, 'M', 'ⳗ'), + (0x2CD7, 'V'), + (0x2CD8, 'M', 'ⳙ'), + (0x2CD9, 'V'), + (0x2CDA, 'M', 'ⳛ'), + (0x2CDB, 'V'), + (0x2CDC, 'M', 'ⳝ'), + (0x2CDD, 'V'), + (0x2CDE, 'M', 'ⳟ'), + (0x2CDF, 'V'), + (0x2CE0, 'M', 'ⳡ'), + (0x2CE1, 'V'), + (0x2CE2, 'M', 'ⳣ'), + (0x2CE3, 'V'), + (0x2CEB, 'M', 'ⳬ'), + (0x2CEC, 'V'), + (0x2CED, 'M', 'ⳮ'), + (0x2CEE, 'V'), + (0x2CF2, 'M', 'ⳳ'), + (0x2CF3, 'V'), + (0x2CF4, 'X'), + (0x2CF9, 'V'), + (0x2D26, 'X'), + (0x2D27, 'V'), + (0x2D28, 'X'), + (0x2D2D, 'V'), + (0x2D2E, 'X'), + (0x2D30, 'V'), + (0x2D68, 'X'), + (0x2D6F, 'M', 'ⵡ'), + (0x2D70, 'V'), + (0x2D71, 'X'), + (0x2D7F, 'V'), + (0x2D97, 'X'), + (0x2DA0, 'V'), + (0x2DA7, 'X'), + (0x2DA8, 'V'), + (0x2DAF, 'X'), + (0x2DB0, 'V'), + (0x2DB7, 'X'), + (0x2DB8, 'V'), + (0x2DBF, 'X'), + (0x2DC0, 'V'), + (0x2DC7, 'X'), + (0x2DC8, 'V'), + (0x2DCF, 'X'), + (0x2DD0, 'V'), + (0x2DD7, 'X'), + (0x2DD8, 'V'), + (0x2DDF, 'X'), + (0x2DE0, 'V'), + (0x2E5E, 'X'), + (0x2E80, 'V'), + (0x2E9A, 'X'), + (0x2E9B, 'V'), + (0x2E9F, 'M', '母'), + (0x2EA0, 'V'), + (0x2EF3, 'M', '龟'), + (0x2EF4, 'X'), + (0x2F00, 'M', '一'), + (0x2F01, 'M', '丨'), + (0x2F02, 'M', '丶'), + (0x2F03, 'M', '丿'), + (0x2F04, 'M', '乙'), + (0x2F05, 'M', '亅'), + (0x2F06, 'M', '二'), + (0x2F07, 'M', '亠'), + (0x2F08, 'M', '人'), + (0x2F09, 'M', '儿'), + (0x2F0A, 'M', '入'), + (0x2F0B, 'M', '八'), + (0x2F0C, 'M', '冂'), + (0x2F0D, 'M', '冖'), + (0x2F0E, 'M', '冫'), + (0x2F0F, 'M', '几'), + (0x2F10, 'M', '凵'), + (0x2F11, 'M', '刀'), + (0x2F12, 'M', '力'), + (0x2F13, 'M', '勹'), + (0x2F14, 'M', '匕'), + (0x2F15, 'M', '匚'), + ] + +def _seg_27() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F16, 'M', '匸'), + (0x2F17, 'M', '十'), + (0x2F18, 'M', '卜'), + (0x2F19, 'M', '卩'), + (0x2F1A, 'M', '厂'), + (0x2F1B, 'M', '厶'), + (0x2F1C, 'M', '又'), + (0x2F1D, 'M', '口'), + (0x2F1E, 'M', '囗'), + (0x2F1F, 'M', '土'), + (0x2F20, 'M', '士'), + (0x2F21, 'M', '夂'), + (0x2F22, 'M', '夊'), + (0x2F23, 'M', '夕'), + (0x2F24, 'M', '大'), + (0x2F25, 'M', '女'), + (0x2F26, 'M', '子'), + (0x2F27, 'M', '宀'), + (0x2F28, 'M', '寸'), + (0x2F29, 'M', '小'), + (0x2F2A, 'M', '尢'), + (0x2F2B, 'M', '尸'), + (0x2F2C, 'M', '屮'), + (0x2F2D, 'M', '山'), + (0x2F2E, 'M', '巛'), + (0x2F2F, 'M', '工'), + (0x2F30, 'M', '己'), + (0x2F31, 'M', '巾'), + (0x2F32, 'M', '干'), + (0x2F33, 'M', '幺'), + (0x2F34, 'M', '广'), + (0x2F35, 'M', '廴'), + (0x2F36, 'M', '廾'), + (0x2F37, 'M', '弋'), + (0x2F38, 'M', '弓'), + (0x2F39, 'M', '彐'), + (0x2F3A, 'M', '彡'), + (0x2F3B, 'M', '彳'), + (0x2F3C, 'M', '心'), + (0x2F3D, 'M', '戈'), + (0x2F3E, 'M', '戶'), + (0x2F3F, 'M', '手'), + (0x2F40, 'M', '支'), + (0x2F41, 'M', '攴'), + (0x2F42, 'M', '文'), + (0x2F43, 'M', '斗'), + (0x2F44, 'M', '斤'), + (0x2F45, 'M', '方'), + (0x2F46, 'M', '无'), + (0x2F47, 'M', '日'), + (0x2F48, 'M', '曰'), + (0x2F49, 'M', '月'), + (0x2F4A, 'M', '木'), + (0x2F4B, 'M', '欠'), + (0x2F4C, 'M', '止'), + (0x2F4D, 'M', '歹'), + (0x2F4E, 'M', '殳'), + (0x2F4F, 'M', '毋'), + (0x2F50, 'M', '比'), + (0x2F51, 'M', '毛'), + (0x2F52, 'M', '氏'), + (0x2F53, 'M', '气'), + (0x2F54, 'M', '水'), + (0x2F55, 'M', '火'), + (0x2F56, 'M', '爪'), + (0x2F57, 'M', '父'), + (0x2F58, 'M', '爻'), + (0x2F59, 'M', '爿'), + (0x2F5A, 'M', '片'), + (0x2F5B, 'M', '牙'), + (0x2F5C, 'M', '牛'), + (0x2F5D, 'M', '犬'), + (0x2F5E, 'M', '玄'), + (0x2F5F, 'M', '玉'), + (0x2F60, 'M', '瓜'), + (0x2F61, 'M', '瓦'), + (0x2F62, 'M', '甘'), + (0x2F63, 'M', '生'), + (0x2F64, 'M', '用'), + (0x2F65, 'M', '田'), + (0x2F66, 'M', '疋'), + (0x2F67, 'M', '疒'), + (0x2F68, 'M', '癶'), + (0x2F69, 'M', '白'), + (0x2F6A, 'M', '皮'), + (0x2F6B, 'M', '皿'), + (0x2F6C, 'M', '目'), + (0x2F6D, 'M', '矛'), + (0x2F6E, 'M', '矢'), + (0x2F6F, 'M', '石'), + (0x2F70, 'M', '示'), + (0x2F71, 'M', '禸'), + (0x2F72, 'M', '禾'), + (0x2F73, 'M', '穴'), + (0x2F74, 'M', '立'), + (0x2F75, 'M', '竹'), + (0x2F76, 'M', '米'), + (0x2F77, 'M', '糸'), + (0x2F78, 'M', '缶'), + (0x2F79, 'M', '网'), + ] + +def _seg_28() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F7A, 'M', '羊'), + (0x2F7B, 'M', '羽'), + (0x2F7C, 'M', '老'), + (0x2F7D, 'M', '而'), + (0x2F7E, 'M', '耒'), + (0x2F7F, 'M', '耳'), + (0x2F80, 'M', '聿'), + (0x2F81, 'M', '肉'), + (0x2F82, 'M', '臣'), + (0x2F83, 'M', '自'), + (0x2F84, 'M', '至'), + (0x2F85, 'M', '臼'), + (0x2F86, 'M', '舌'), + (0x2F87, 'M', '舛'), + (0x2F88, 'M', '舟'), + (0x2F89, 'M', '艮'), + (0x2F8A, 'M', '色'), + (0x2F8B, 'M', '艸'), + (0x2F8C, 'M', '虍'), + (0x2F8D, 'M', '虫'), + (0x2F8E, 'M', '血'), + (0x2F8F, 'M', '行'), + (0x2F90, 'M', '衣'), + (0x2F91, 'M', '襾'), + (0x2F92, 'M', '見'), + (0x2F93, 'M', '角'), + (0x2F94, 'M', '言'), + (0x2F95, 'M', '谷'), + (0x2F96, 'M', '豆'), + (0x2F97, 'M', '豕'), + (0x2F98, 'M', '豸'), + (0x2F99, 'M', '貝'), + (0x2F9A, 'M', '赤'), + (0x2F9B, 'M', '走'), + (0x2F9C, 'M', '足'), + (0x2F9D, 'M', '身'), + (0x2F9E, 'M', '車'), + (0x2F9F, 'M', '辛'), + (0x2FA0, 'M', '辰'), + (0x2FA1, 'M', '辵'), + (0x2FA2, 'M', '邑'), + (0x2FA3, 'M', '酉'), + (0x2FA4, 'M', '釆'), + (0x2FA5, 'M', '里'), + (0x2FA6, 'M', '金'), + (0x2FA7, 'M', '長'), + (0x2FA8, 'M', '門'), + (0x2FA9, 'M', '阜'), + (0x2FAA, 'M', '隶'), + (0x2FAB, 'M', '隹'), + (0x2FAC, 'M', '雨'), + (0x2FAD, 'M', '靑'), + (0x2FAE, 'M', '非'), + (0x2FAF, 'M', '面'), + (0x2FB0, 'M', '革'), + (0x2FB1, 'M', '韋'), + (0x2FB2, 'M', '韭'), + (0x2FB3, 'M', '音'), + (0x2FB4, 'M', '頁'), + (0x2FB5, 'M', '風'), + (0x2FB6, 'M', '飛'), + (0x2FB7, 'M', '食'), + (0x2FB8, 'M', '首'), + (0x2FB9, 'M', '香'), + (0x2FBA, 'M', '馬'), + (0x2FBB, 'M', '骨'), + (0x2FBC, 'M', '高'), + (0x2FBD, 'M', '髟'), + (0x2FBE, 'M', '鬥'), + (0x2FBF, 'M', '鬯'), + (0x2FC0, 'M', '鬲'), + (0x2FC1, 'M', '鬼'), + (0x2FC2, 'M', '魚'), + (0x2FC3, 'M', '鳥'), + (0x2FC4, 'M', '鹵'), + (0x2FC5, 'M', '鹿'), + (0x2FC6, 'M', '麥'), + (0x2FC7, 'M', '麻'), + (0x2FC8, 'M', '黃'), + (0x2FC9, 'M', '黍'), + (0x2FCA, 'M', '黑'), + (0x2FCB, 'M', '黹'), + (0x2FCC, 'M', '黽'), + (0x2FCD, 'M', '鼎'), + (0x2FCE, 'M', '鼓'), + (0x2FCF, 'M', '鼠'), + (0x2FD0, 'M', '鼻'), + (0x2FD1, 'M', '齊'), + (0x2FD2, 'M', '齒'), + (0x2FD3, 'M', '龍'), + (0x2FD4, 'M', '龜'), + (0x2FD5, 'M', '龠'), + (0x2FD6, 'X'), + (0x3000, '3', ' '), + (0x3001, 'V'), + (0x3002, 'M', '.'), + (0x3003, 'V'), + (0x3036, 'M', '〒'), + (0x3037, 'V'), + (0x3038, 'M', '十'), + ] + +def _seg_29() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x3039, 'M', '卄'), + (0x303A, 'M', '卅'), + (0x303B, 'V'), + (0x3040, 'X'), + (0x3041, 'V'), + (0x3097, 'X'), + (0x3099, 'V'), + (0x309B, '3', ' ゙'), + (0x309C, '3', ' ゚'), + (0x309D, 'V'), + (0x309F, 'M', 'より'), + (0x30A0, 'V'), + (0x30FF, 'M', 'コト'), + (0x3100, 'X'), + (0x3105, 'V'), + (0x3130, 'X'), + (0x3131, 'M', 'ᄀ'), + (0x3132, 'M', 'ᄁ'), + (0x3133, 'M', 'ᆪ'), + (0x3134, 'M', 'ᄂ'), + (0x3135, 'M', 'ᆬ'), + (0x3136, 'M', 'ᆭ'), + (0x3137, 'M', 'ᄃ'), + (0x3138, 'M', 'ᄄ'), + (0x3139, 'M', 'ᄅ'), + (0x313A, 'M', 'ᆰ'), + (0x313B, 'M', 'ᆱ'), + (0x313C, 'M', 'ᆲ'), + (0x313D, 'M', 'ᆳ'), + (0x313E, 'M', 'ᆴ'), + (0x313F, 'M', 'ᆵ'), + (0x3140, 'M', 'ᄚ'), + (0x3141, 'M', 'ᄆ'), + (0x3142, 'M', 'ᄇ'), + (0x3143, 'M', 'ᄈ'), + (0x3144, 'M', 'ᄡ'), + (0x3145, 'M', 'ᄉ'), + (0x3146, 'M', 'ᄊ'), + (0x3147, 'M', 'ᄋ'), + (0x3148, 'M', 'ᄌ'), + (0x3149, 'M', 'ᄍ'), + (0x314A, 'M', 'ᄎ'), + (0x314B, 'M', 'ᄏ'), + (0x314C, 'M', 'ᄐ'), + (0x314D, 'M', 'ᄑ'), + (0x314E, 'M', 'ᄒ'), + (0x314F, 'M', 'ᅡ'), + (0x3150, 'M', 'ᅢ'), + (0x3151, 'M', 'ᅣ'), + (0x3152, 'M', 'ᅤ'), + (0x3153, 'M', 'ᅥ'), + (0x3154, 'M', 'ᅦ'), + (0x3155, 'M', 'ᅧ'), + (0x3156, 'M', 'ᅨ'), + (0x3157, 'M', 'ᅩ'), + (0x3158, 'M', 'ᅪ'), + (0x3159, 'M', 'ᅫ'), + (0x315A, 'M', 'ᅬ'), + (0x315B, 'M', 'ᅭ'), + (0x315C, 'M', 'ᅮ'), + (0x315D, 'M', 'ᅯ'), + (0x315E, 'M', 'ᅰ'), + (0x315F, 'M', 'ᅱ'), + (0x3160, 'M', 'ᅲ'), + (0x3161, 'M', 'ᅳ'), + (0x3162, 'M', 'ᅴ'), + (0x3163, 'M', 'ᅵ'), + (0x3164, 'X'), + (0x3165, 'M', 'ᄔ'), + (0x3166, 'M', 'ᄕ'), + (0x3167, 'M', 'ᇇ'), + (0x3168, 'M', 'ᇈ'), + (0x3169, 'M', 'ᇌ'), + (0x316A, 'M', 'ᇎ'), + (0x316B, 'M', 'ᇓ'), + (0x316C, 'M', 'ᇗ'), + (0x316D, 'M', 'ᇙ'), + (0x316E, 'M', 'ᄜ'), + (0x316F, 'M', 'ᇝ'), + (0x3170, 'M', 'ᇟ'), + (0x3171, 'M', 'ᄝ'), + (0x3172, 'M', 'ᄞ'), + (0x3173, 'M', 'ᄠ'), + (0x3174, 'M', 'ᄢ'), + (0x3175, 'M', 'ᄣ'), + (0x3176, 'M', 'ᄧ'), + (0x3177, 'M', 'ᄩ'), + (0x3178, 'M', 'ᄫ'), + (0x3179, 'M', 'ᄬ'), + (0x317A, 'M', 'ᄭ'), + (0x317B, 'M', 'ᄮ'), + (0x317C, 'M', 'ᄯ'), + (0x317D, 'M', 'ᄲ'), + (0x317E, 'M', 'ᄶ'), + (0x317F, 'M', 'ᅀ'), + (0x3180, 'M', 'ᅇ'), + (0x3181, 'M', 'ᅌ'), + (0x3182, 'M', 'ᇱ'), + (0x3183, 'M', 'ᇲ'), + (0x3184, 'M', 'ᅗ'), + ] + +def _seg_30() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x3185, 'M', 'ᅘ'), + (0x3186, 'M', 'ᅙ'), + (0x3187, 'M', 'ᆄ'), + (0x3188, 'M', 'ᆅ'), + (0x3189, 'M', 'ᆈ'), + (0x318A, 'M', 'ᆑ'), + (0x318B, 'M', 'ᆒ'), + (0x318C, 'M', 'ᆔ'), + (0x318D, 'M', 'ᆞ'), + (0x318E, 'M', 'ᆡ'), + (0x318F, 'X'), + (0x3190, 'V'), + (0x3192, 'M', '一'), + (0x3193, 'M', '二'), + (0x3194, 'M', '三'), + (0x3195, 'M', '四'), + (0x3196, 'M', '上'), + (0x3197, 'M', '中'), + (0x3198, 'M', '下'), + (0x3199, 'M', '甲'), + (0x319A, 'M', '乙'), + (0x319B, 'M', '丙'), + (0x319C, 'M', '丁'), + (0x319D, 'M', '天'), + (0x319E, 'M', '地'), + (0x319F, 'M', '人'), + (0x31A0, 'V'), + (0x31E4, 'X'), + (0x31F0, 'V'), + (0x3200, '3', '(ᄀ)'), + (0x3201, '3', '(ᄂ)'), + (0x3202, '3', '(ᄃ)'), + (0x3203, '3', '(ᄅ)'), + (0x3204, '3', '(ᄆ)'), + (0x3205, '3', '(ᄇ)'), + (0x3206, '3', '(ᄉ)'), + (0x3207, '3', '(ᄋ)'), + (0x3208, '3', '(ᄌ)'), + (0x3209, '3', '(ᄎ)'), + (0x320A, '3', '(ᄏ)'), + (0x320B, '3', '(ᄐ)'), + (0x320C, '3', '(ᄑ)'), + (0x320D, '3', '(ᄒ)'), + (0x320E, '3', '(가)'), + (0x320F, '3', '(나)'), + (0x3210, '3', '(다)'), + (0x3211, '3', '(라)'), + (0x3212, '3', '(마)'), + (0x3213, '3', '(바)'), + (0x3214, '3', '(사)'), + (0x3215, '3', '(아)'), + (0x3216, '3', '(자)'), + (0x3217, '3', '(차)'), + (0x3218, '3', '(카)'), + (0x3219, '3', '(타)'), + (0x321A, '3', '(파)'), + (0x321B, '3', '(하)'), + (0x321C, '3', '(주)'), + (0x321D, '3', '(오전)'), + (0x321E, '3', '(오후)'), + (0x321F, 'X'), + (0x3220, '3', '(一)'), + (0x3221, '3', '(二)'), + (0x3222, '3', '(三)'), + (0x3223, '3', '(四)'), + (0x3224, '3', '(五)'), + (0x3225, '3', '(六)'), + (0x3226, '3', '(七)'), + (0x3227, '3', '(八)'), + (0x3228, '3', '(九)'), + (0x3229, '3', '(十)'), + (0x322A, '3', '(月)'), + (0x322B, '3', '(火)'), + (0x322C, '3', '(水)'), + (0x322D, '3', '(木)'), + (0x322E, '3', '(金)'), + (0x322F, '3', '(土)'), + (0x3230, '3', '(日)'), + (0x3231, '3', '(株)'), + (0x3232, '3', '(有)'), + (0x3233, '3', '(社)'), + (0x3234, '3', '(名)'), + (0x3235, '3', '(特)'), + (0x3236, '3', '(財)'), + (0x3237, '3', '(祝)'), + (0x3238, '3', '(労)'), + (0x3239, '3', '(代)'), + (0x323A, '3', '(呼)'), + (0x323B, '3', '(学)'), + (0x323C, '3', '(監)'), + (0x323D, '3', '(企)'), + (0x323E, '3', '(資)'), + (0x323F, '3', '(協)'), + (0x3240, '3', '(祭)'), + (0x3241, '3', '(休)'), + (0x3242, '3', '(自)'), + (0x3243, '3', '(至)'), + (0x3244, 'M', '問'), + (0x3245, 'M', '幼'), + (0x3246, 'M', '文'), + ] + +def _seg_31() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x3247, 'M', '箏'), + (0x3248, 'V'), + (0x3250, 'M', 'pte'), + (0x3251, 'M', '21'), + (0x3252, 'M', '22'), + (0x3253, 'M', '23'), + (0x3254, 'M', '24'), + (0x3255, 'M', '25'), + (0x3256, 'M', '26'), + (0x3257, 'M', '27'), + (0x3258, 'M', '28'), + (0x3259, 'M', '29'), + (0x325A, 'M', '30'), + (0x325B, 'M', '31'), + (0x325C, 'M', '32'), + (0x325D, 'M', '33'), + (0x325E, 'M', '34'), + (0x325F, 'M', '35'), + (0x3260, 'M', 'ᄀ'), + (0x3261, 'M', 'ᄂ'), + (0x3262, 'M', 'ᄃ'), + (0x3263, 'M', 'ᄅ'), + (0x3264, 'M', 'ᄆ'), + (0x3265, 'M', 'ᄇ'), + (0x3266, 'M', 'ᄉ'), + (0x3267, 'M', 'ᄋ'), + (0x3268, 'M', 'ᄌ'), + (0x3269, 'M', 'ᄎ'), + (0x326A, 'M', 'ᄏ'), + (0x326B, 'M', 'ᄐ'), + (0x326C, 'M', 'ᄑ'), + (0x326D, 'M', 'ᄒ'), + (0x326E, 'M', '가'), + (0x326F, 'M', '나'), + (0x3270, 'M', '다'), + (0x3271, 'M', '라'), + (0x3272, 'M', '마'), + (0x3273, 'M', '바'), + (0x3274, 'M', '사'), + (0x3275, 'M', '아'), + (0x3276, 'M', '자'), + (0x3277, 'M', '차'), + (0x3278, 'M', '카'), + (0x3279, 'M', '타'), + (0x327A, 'M', '파'), + (0x327B, 'M', '하'), + (0x327C, 'M', '참고'), + (0x327D, 'M', '주의'), + (0x327E, 'M', '우'), + (0x327F, 'V'), + (0x3280, 'M', '一'), + (0x3281, 'M', '二'), + (0x3282, 'M', '三'), + (0x3283, 'M', '四'), + (0x3284, 'M', '五'), + (0x3285, 'M', '六'), + (0x3286, 'M', '七'), + (0x3287, 'M', '八'), + (0x3288, 'M', '九'), + (0x3289, 'M', '十'), + (0x328A, 'M', '月'), + (0x328B, 'M', '火'), + (0x328C, 'M', '水'), + (0x328D, 'M', '木'), + (0x328E, 'M', '金'), + (0x328F, 'M', '土'), + (0x3290, 'M', '日'), + (0x3291, 'M', '株'), + (0x3292, 'M', '有'), + (0x3293, 'M', '社'), + (0x3294, 'M', '名'), + (0x3295, 'M', '特'), + (0x3296, 'M', '財'), + (0x3297, 'M', '祝'), + (0x3298, 'M', '労'), + (0x3299, 'M', '秘'), + (0x329A, 'M', '男'), + (0x329B, 'M', '女'), + (0x329C, 'M', '適'), + (0x329D, 'M', '優'), + (0x329E, 'M', '印'), + (0x329F, 'M', '注'), + (0x32A0, 'M', '項'), + (0x32A1, 'M', '休'), + (0x32A2, 'M', '写'), + (0x32A3, 'M', '正'), + (0x32A4, 'M', '上'), + (0x32A5, 'M', '中'), + (0x32A6, 'M', '下'), + (0x32A7, 'M', '左'), + (0x32A8, 'M', '右'), + (0x32A9, 'M', '医'), + (0x32AA, 'M', '宗'), + (0x32AB, 'M', '学'), + (0x32AC, 'M', '監'), + (0x32AD, 'M', '企'), + (0x32AE, 'M', '資'), + (0x32AF, 'M', '協'), + (0x32B0, 'M', '夜'), + (0x32B1, 'M', '36'), + ] + +def _seg_32() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x32B2, 'M', '37'), + (0x32B3, 'M', '38'), + (0x32B4, 'M', '39'), + (0x32B5, 'M', '40'), + (0x32B6, 'M', '41'), + (0x32B7, 'M', '42'), + (0x32B8, 'M', '43'), + (0x32B9, 'M', '44'), + (0x32BA, 'M', '45'), + (0x32BB, 'M', '46'), + (0x32BC, 'M', '47'), + (0x32BD, 'M', '48'), + (0x32BE, 'M', '49'), + (0x32BF, 'M', '50'), + (0x32C0, 'M', '1月'), + (0x32C1, 'M', '2月'), + (0x32C2, 'M', '3月'), + (0x32C3, 'M', '4月'), + (0x32C4, 'M', '5月'), + (0x32C5, 'M', '6月'), + (0x32C6, 'M', '7月'), + (0x32C7, 'M', '8月'), + (0x32C8, 'M', '9月'), + (0x32C9, 'M', '10月'), + (0x32CA, 'M', '11月'), + (0x32CB, 'M', '12月'), + (0x32CC, 'M', 'hg'), + (0x32CD, 'M', 'erg'), + (0x32CE, 'M', 'ev'), + (0x32CF, 'M', 'ltd'), + (0x32D0, 'M', 'ア'), + (0x32D1, 'M', 'イ'), + (0x32D2, 'M', 'ウ'), + (0x32D3, 'M', 'エ'), + (0x32D4, 'M', 'オ'), + (0x32D5, 'M', 'カ'), + (0x32D6, 'M', 'キ'), + (0x32D7, 'M', 'ク'), + (0x32D8, 'M', 'ケ'), + (0x32D9, 'M', 'コ'), + (0x32DA, 'M', 'サ'), + (0x32DB, 'M', 'シ'), + (0x32DC, 'M', 'ス'), + (0x32DD, 'M', 'セ'), + (0x32DE, 'M', 'ソ'), + (0x32DF, 'M', 'タ'), + (0x32E0, 'M', 'チ'), + (0x32E1, 'M', 'ツ'), + (0x32E2, 'M', 'テ'), + (0x32E3, 'M', 'ト'), + (0x32E4, 'M', 'ナ'), + (0x32E5, 'M', 'ニ'), + (0x32E6, 'M', 'ヌ'), + (0x32E7, 'M', 'ネ'), + (0x32E8, 'M', 'ノ'), + (0x32E9, 'M', 'ハ'), + (0x32EA, 'M', 'ヒ'), + (0x32EB, 'M', 'フ'), + (0x32EC, 'M', 'ヘ'), + (0x32ED, 'M', 'ホ'), + (0x32EE, 'M', 'マ'), + (0x32EF, 'M', 'ミ'), + (0x32F0, 'M', 'ム'), + (0x32F1, 'M', 'メ'), + (0x32F2, 'M', 'モ'), + (0x32F3, 'M', 'ヤ'), + (0x32F4, 'M', 'ユ'), + (0x32F5, 'M', 'ヨ'), + (0x32F6, 'M', 'ラ'), + (0x32F7, 'M', 'リ'), + (0x32F8, 'M', 'ル'), + (0x32F9, 'M', 'レ'), + (0x32FA, 'M', 'ロ'), + (0x32FB, 'M', 'ワ'), + (0x32FC, 'M', 'ヰ'), + (0x32FD, 'M', 'ヱ'), + (0x32FE, 'M', 'ヲ'), + (0x32FF, 'M', '令和'), + (0x3300, 'M', 'アパート'), + (0x3301, 'M', 'アルファ'), + (0x3302, 'M', 'アンペア'), + (0x3303, 'M', 'アール'), + (0x3304, 'M', 'イニング'), + (0x3305, 'M', 'インチ'), + (0x3306, 'M', 'ウォン'), + (0x3307, 'M', 'エスクード'), + (0x3308, 'M', 'エーカー'), + (0x3309, 'M', 'オンス'), + (0x330A, 'M', 'オーム'), + (0x330B, 'M', 'カイリ'), + (0x330C, 'M', 'カラット'), + (0x330D, 'M', 'カロリー'), + (0x330E, 'M', 'ガロン'), + (0x330F, 'M', 'ガンマ'), + (0x3310, 'M', 'ギガ'), + (0x3311, 'M', 'ギニー'), + (0x3312, 'M', 'キュリー'), + (0x3313, 'M', 'ギルダー'), + (0x3314, 'M', 'キロ'), + (0x3315, 'M', 'キログラム'), + ] + +def _seg_33() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x3316, 'M', 'キロメートル'), + (0x3317, 'M', 'キロワット'), + (0x3318, 'M', 'グラム'), + (0x3319, 'M', 'グラムトン'), + (0x331A, 'M', 'クルゼイロ'), + (0x331B, 'M', 'クローネ'), + (0x331C, 'M', 'ケース'), + (0x331D, 'M', 'コルナ'), + (0x331E, 'M', 'コーポ'), + (0x331F, 'M', 'サイクル'), + (0x3320, 'M', 'サンチーム'), + (0x3321, 'M', 'シリング'), + (0x3322, 'M', 'センチ'), + (0x3323, 'M', 'セント'), + (0x3324, 'M', 'ダース'), + (0x3325, 'M', 'デシ'), + (0x3326, 'M', 'ドル'), + (0x3327, 'M', 'トン'), + (0x3328, 'M', 'ナノ'), + (0x3329, 'M', 'ノット'), + (0x332A, 'M', 'ハイツ'), + (0x332B, 'M', 'パーセント'), + (0x332C, 'M', 'パーツ'), + (0x332D, 'M', 'バーレル'), + (0x332E, 'M', 'ピアストル'), + (0x332F, 'M', 'ピクル'), + (0x3330, 'M', 'ピコ'), + (0x3331, 'M', 'ビル'), + (0x3332, 'M', 'ファラッド'), + (0x3333, 'M', 'フィート'), + (0x3334, 'M', 'ブッシェル'), + (0x3335, 'M', 'フラン'), + (0x3336, 'M', 'ヘクタール'), + (0x3337, 'M', 'ペソ'), + (0x3338, 'M', 'ペニヒ'), + (0x3339, 'M', 'ヘルツ'), + (0x333A, 'M', 'ペンス'), + (0x333B, 'M', 'ページ'), + (0x333C, 'M', 'ベータ'), + (0x333D, 'M', 'ポイント'), + (0x333E, 'M', 'ボルト'), + (0x333F, 'M', 'ホン'), + (0x3340, 'M', 'ポンド'), + (0x3341, 'M', 'ホール'), + (0x3342, 'M', 'ホーン'), + (0x3343, 'M', 'マイクロ'), + (0x3344, 'M', 'マイル'), + (0x3345, 'M', 'マッハ'), + (0x3346, 'M', 'マルク'), + (0x3347, 'M', 'マンション'), + (0x3348, 'M', 'ミクロン'), + (0x3349, 'M', 'ミリ'), + (0x334A, 'M', 'ミリバール'), + (0x334B, 'M', 'メガ'), + (0x334C, 'M', 'メガトン'), + (0x334D, 'M', 'メートル'), + (0x334E, 'M', 'ヤード'), + (0x334F, 'M', 'ヤール'), + (0x3350, 'M', 'ユアン'), + (0x3351, 'M', 'リットル'), + (0x3352, 'M', 'リラ'), + (0x3353, 'M', 'ルピー'), + (0x3354, 'M', 'ルーブル'), + (0x3355, 'M', 'レム'), + (0x3356, 'M', 'レントゲン'), + (0x3357, 'M', 'ワット'), + (0x3358, 'M', '0点'), + (0x3359, 'M', '1点'), + (0x335A, 'M', '2点'), + (0x335B, 'M', '3点'), + (0x335C, 'M', '4点'), + (0x335D, 'M', '5点'), + (0x335E, 'M', '6点'), + (0x335F, 'M', '7点'), + (0x3360, 'M', '8点'), + (0x3361, 'M', '9点'), + (0x3362, 'M', '10点'), + (0x3363, 'M', '11点'), + (0x3364, 'M', '12点'), + (0x3365, 'M', '13点'), + (0x3366, 'M', '14点'), + (0x3367, 'M', '15点'), + (0x3368, 'M', '16点'), + (0x3369, 'M', '17点'), + (0x336A, 'M', '18点'), + (0x336B, 'M', '19点'), + (0x336C, 'M', '20点'), + (0x336D, 'M', '21点'), + (0x336E, 'M', '22点'), + (0x336F, 'M', '23点'), + (0x3370, 'M', '24点'), + (0x3371, 'M', 'hpa'), + (0x3372, 'M', 'da'), + (0x3373, 'M', 'au'), + (0x3374, 'M', 'bar'), + (0x3375, 'M', 'ov'), + (0x3376, 'M', 'pc'), + (0x3377, 'M', 'dm'), + (0x3378, 'M', 'dm2'), + (0x3379, 'M', 'dm3'), + ] + +def _seg_34() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x337A, 'M', 'iu'), + (0x337B, 'M', '平成'), + (0x337C, 'M', '昭和'), + (0x337D, 'M', '大正'), + (0x337E, 'M', '明治'), + (0x337F, 'M', '株式会社'), + (0x3380, 'M', 'pa'), + (0x3381, 'M', 'na'), + (0x3382, 'M', 'μa'), + (0x3383, 'M', 'ma'), + (0x3384, 'M', 'ka'), + (0x3385, 'M', 'kb'), + (0x3386, 'M', 'mb'), + (0x3387, 'M', 'gb'), + (0x3388, 'M', 'cal'), + (0x3389, 'M', 'kcal'), + (0x338A, 'M', 'pf'), + (0x338B, 'M', 'nf'), + (0x338C, 'M', 'μf'), + (0x338D, 'M', 'μg'), + (0x338E, 'M', 'mg'), + (0x338F, 'M', 'kg'), + (0x3390, 'M', 'hz'), + (0x3391, 'M', 'khz'), + (0x3392, 'M', 'mhz'), + (0x3393, 'M', 'ghz'), + (0x3394, 'M', 'thz'), + (0x3395, 'M', 'μl'), + (0x3396, 'M', 'ml'), + (0x3397, 'M', 'dl'), + (0x3398, 'M', 'kl'), + (0x3399, 'M', 'fm'), + (0x339A, 'M', 'nm'), + (0x339B, 'M', 'μm'), + (0x339C, 'M', 'mm'), + (0x339D, 'M', 'cm'), + (0x339E, 'M', 'km'), + (0x339F, 'M', 'mm2'), + (0x33A0, 'M', 'cm2'), + (0x33A1, 'M', 'm2'), + (0x33A2, 'M', 'km2'), + (0x33A3, 'M', 'mm3'), + (0x33A4, 'M', 'cm3'), + (0x33A5, 'M', 'm3'), + (0x33A6, 'M', 'km3'), + (0x33A7, 'M', 'm∕s'), + (0x33A8, 'M', 'm∕s2'), + (0x33A9, 'M', 'pa'), + (0x33AA, 'M', 'kpa'), + (0x33AB, 'M', 'mpa'), + (0x33AC, 'M', 'gpa'), + (0x33AD, 'M', 'rad'), + (0x33AE, 'M', 'rad∕s'), + (0x33AF, 'M', 'rad∕s2'), + (0x33B0, 'M', 'ps'), + (0x33B1, 'M', 'ns'), + (0x33B2, 'M', 'μs'), + (0x33B3, 'M', 'ms'), + (0x33B4, 'M', 'pv'), + (0x33B5, 'M', 'nv'), + (0x33B6, 'M', 'μv'), + (0x33B7, 'M', 'mv'), + (0x33B8, 'M', 'kv'), + (0x33B9, 'M', 'mv'), + (0x33BA, 'M', 'pw'), + (0x33BB, 'M', 'nw'), + (0x33BC, 'M', 'μw'), + (0x33BD, 'M', 'mw'), + (0x33BE, 'M', 'kw'), + (0x33BF, 'M', 'mw'), + (0x33C0, 'M', 'kω'), + (0x33C1, 'M', 'mω'), + (0x33C2, 'X'), + (0x33C3, 'M', 'bq'), + (0x33C4, 'M', 'cc'), + (0x33C5, 'M', 'cd'), + (0x33C6, 'M', 'c∕kg'), + (0x33C7, 'X'), + (0x33C8, 'M', 'db'), + (0x33C9, 'M', 'gy'), + (0x33CA, 'M', 'ha'), + (0x33CB, 'M', 'hp'), + (0x33CC, 'M', 'in'), + (0x33CD, 'M', 'kk'), + (0x33CE, 'M', 'km'), + (0x33CF, 'M', 'kt'), + (0x33D0, 'M', 'lm'), + (0x33D1, 'M', 'ln'), + (0x33D2, 'M', 'log'), + (0x33D3, 'M', 'lx'), + (0x33D4, 'M', 'mb'), + (0x33D5, 'M', 'mil'), + (0x33D6, 'M', 'mol'), + (0x33D7, 'M', 'ph'), + (0x33D8, 'X'), + (0x33D9, 'M', 'ppm'), + (0x33DA, 'M', 'pr'), + (0x33DB, 'M', 'sr'), + (0x33DC, 'M', 'sv'), + (0x33DD, 'M', 'wb'), + ] + +def _seg_35() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x33DE, 'M', 'v∕m'), + (0x33DF, 'M', 'a∕m'), + (0x33E0, 'M', '1日'), + (0x33E1, 'M', '2日'), + (0x33E2, 'M', '3日'), + (0x33E3, 'M', '4日'), + (0x33E4, 'M', '5日'), + (0x33E5, 'M', '6日'), + (0x33E6, 'M', '7日'), + (0x33E7, 'M', '8日'), + (0x33E8, 'M', '9日'), + (0x33E9, 'M', '10日'), + (0x33EA, 'M', '11日'), + (0x33EB, 'M', '12日'), + (0x33EC, 'M', '13日'), + (0x33ED, 'M', '14日'), + (0x33EE, 'M', '15日'), + (0x33EF, 'M', '16日'), + (0x33F0, 'M', '17日'), + (0x33F1, 'M', '18日'), + (0x33F2, 'M', '19日'), + (0x33F3, 'M', '20日'), + (0x33F4, 'M', '21日'), + (0x33F5, 'M', '22日'), + (0x33F6, 'M', '23日'), + (0x33F7, 'M', '24日'), + (0x33F8, 'M', '25日'), + (0x33F9, 'M', '26日'), + (0x33FA, 'M', '27日'), + (0x33FB, 'M', '28日'), + (0x33FC, 'M', '29日'), + (0x33FD, 'M', '30日'), + (0x33FE, 'M', '31日'), + (0x33FF, 'M', 'gal'), + (0x3400, 'V'), + (0xA48D, 'X'), + (0xA490, 'V'), + (0xA4C7, 'X'), + (0xA4D0, 'V'), + (0xA62C, 'X'), + (0xA640, 'M', 'ꙁ'), + (0xA641, 'V'), + (0xA642, 'M', 'ꙃ'), + (0xA643, 'V'), + (0xA644, 'M', 'ꙅ'), + (0xA645, 'V'), + (0xA646, 'M', 'ꙇ'), + (0xA647, 'V'), + (0xA648, 'M', 'ꙉ'), + (0xA649, 'V'), + (0xA64A, 'M', 'ꙋ'), + (0xA64B, 'V'), + (0xA64C, 'M', 'ꙍ'), + (0xA64D, 'V'), + (0xA64E, 'M', 'ꙏ'), + (0xA64F, 'V'), + (0xA650, 'M', 'ꙑ'), + (0xA651, 'V'), + (0xA652, 'M', 'ꙓ'), + (0xA653, 'V'), + (0xA654, 'M', 'ꙕ'), + (0xA655, 'V'), + (0xA656, 'M', 'ꙗ'), + (0xA657, 'V'), + (0xA658, 'M', 'ꙙ'), + (0xA659, 'V'), + (0xA65A, 'M', 'ꙛ'), + (0xA65B, 'V'), + (0xA65C, 'M', 'ꙝ'), + (0xA65D, 'V'), + (0xA65E, 'M', 'ꙟ'), + (0xA65F, 'V'), + (0xA660, 'M', 'ꙡ'), + (0xA661, 'V'), + (0xA662, 'M', 'ꙣ'), + (0xA663, 'V'), + (0xA664, 'M', 'ꙥ'), + (0xA665, 'V'), + (0xA666, 'M', 'ꙧ'), + (0xA667, 'V'), + (0xA668, 'M', 'ꙩ'), + (0xA669, 'V'), + (0xA66A, 'M', 'ꙫ'), + (0xA66B, 'V'), + (0xA66C, 'M', 'ꙭ'), + (0xA66D, 'V'), + (0xA680, 'M', 'ꚁ'), + (0xA681, 'V'), + (0xA682, 'M', 'ꚃ'), + (0xA683, 'V'), + (0xA684, 'M', 'ꚅ'), + (0xA685, 'V'), + (0xA686, 'M', 'ꚇ'), + (0xA687, 'V'), + (0xA688, 'M', 'ꚉ'), + (0xA689, 'V'), + (0xA68A, 'M', 'ꚋ'), + (0xA68B, 'V'), + (0xA68C, 'M', 'ꚍ'), + (0xA68D, 'V'), + ] + +def _seg_36() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA68E, 'M', 'ꚏ'), + (0xA68F, 'V'), + (0xA690, 'M', 'ꚑ'), + (0xA691, 'V'), + (0xA692, 'M', 'ꚓ'), + (0xA693, 'V'), + (0xA694, 'M', 'ꚕ'), + (0xA695, 'V'), + (0xA696, 'M', 'ꚗ'), + (0xA697, 'V'), + (0xA698, 'M', 'ꚙ'), + (0xA699, 'V'), + (0xA69A, 'M', 'ꚛ'), + (0xA69B, 'V'), + (0xA69C, 'M', 'ъ'), + (0xA69D, 'M', 'ь'), + (0xA69E, 'V'), + (0xA6F8, 'X'), + (0xA700, 'V'), + (0xA722, 'M', 'ꜣ'), + (0xA723, 'V'), + (0xA724, 'M', 'ꜥ'), + (0xA725, 'V'), + (0xA726, 'M', 'ꜧ'), + (0xA727, 'V'), + (0xA728, 'M', 'ꜩ'), + (0xA729, 'V'), + (0xA72A, 'M', 'ꜫ'), + (0xA72B, 'V'), + (0xA72C, 'M', 'ꜭ'), + (0xA72D, 'V'), + (0xA72E, 'M', 'ꜯ'), + (0xA72F, 'V'), + (0xA732, 'M', 'ꜳ'), + (0xA733, 'V'), + (0xA734, 'M', 'ꜵ'), + (0xA735, 'V'), + (0xA736, 'M', 'ꜷ'), + (0xA737, 'V'), + (0xA738, 'M', 'ꜹ'), + (0xA739, 'V'), + (0xA73A, 'M', 'ꜻ'), + (0xA73B, 'V'), + (0xA73C, 'M', 'ꜽ'), + (0xA73D, 'V'), + (0xA73E, 'M', 'ꜿ'), + (0xA73F, 'V'), + (0xA740, 'M', 'ꝁ'), + (0xA741, 'V'), + (0xA742, 'M', 'ꝃ'), + (0xA743, 'V'), + (0xA744, 'M', 'ꝅ'), + (0xA745, 'V'), + (0xA746, 'M', 'ꝇ'), + (0xA747, 'V'), + (0xA748, 'M', 'ꝉ'), + (0xA749, 'V'), + (0xA74A, 'M', 'ꝋ'), + (0xA74B, 'V'), + (0xA74C, 'M', 'ꝍ'), + (0xA74D, 'V'), + (0xA74E, 'M', 'ꝏ'), + (0xA74F, 'V'), + (0xA750, 'M', 'ꝑ'), + (0xA751, 'V'), + (0xA752, 'M', 'ꝓ'), + (0xA753, 'V'), + (0xA754, 'M', 'ꝕ'), + (0xA755, 'V'), + (0xA756, 'M', 'ꝗ'), + (0xA757, 'V'), + (0xA758, 'M', 'ꝙ'), + (0xA759, 'V'), + (0xA75A, 'M', 'ꝛ'), + (0xA75B, 'V'), + (0xA75C, 'M', 'ꝝ'), + (0xA75D, 'V'), + (0xA75E, 'M', 'ꝟ'), + (0xA75F, 'V'), + (0xA760, 'M', 'ꝡ'), + (0xA761, 'V'), + (0xA762, 'M', 'ꝣ'), + (0xA763, 'V'), + (0xA764, 'M', 'ꝥ'), + (0xA765, 'V'), + (0xA766, 'M', 'ꝧ'), + (0xA767, 'V'), + (0xA768, 'M', 'ꝩ'), + (0xA769, 'V'), + (0xA76A, 'M', 'ꝫ'), + (0xA76B, 'V'), + (0xA76C, 'M', 'ꝭ'), + (0xA76D, 'V'), + (0xA76E, 'M', 'ꝯ'), + (0xA76F, 'V'), + (0xA770, 'M', 'ꝯ'), + (0xA771, 'V'), + (0xA779, 'M', 'ꝺ'), + (0xA77A, 'V'), + (0xA77B, 'M', 'ꝼ'), + ] + +def _seg_37() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA77C, 'V'), + (0xA77D, 'M', 'ᵹ'), + (0xA77E, 'M', 'ꝿ'), + (0xA77F, 'V'), + (0xA780, 'M', 'ꞁ'), + (0xA781, 'V'), + (0xA782, 'M', 'ꞃ'), + (0xA783, 'V'), + (0xA784, 'M', 'ꞅ'), + (0xA785, 'V'), + (0xA786, 'M', 'ꞇ'), + (0xA787, 'V'), + (0xA78B, 'M', 'ꞌ'), + (0xA78C, 'V'), + (0xA78D, 'M', 'ɥ'), + (0xA78E, 'V'), + (0xA790, 'M', 'ꞑ'), + (0xA791, 'V'), + (0xA792, 'M', 'ꞓ'), + (0xA793, 'V'), + (0xA796, 'M', 'ꞗ'), + (0xA797, 'V'), + (0xA798, 'M', 'ꞙ'), + (0xA799, 'V'), + (0xA79A, 'M', 'ꞛ'), + (0xA79B, 'V'), + (0xA79C, 'M', 'ꞝ'), + (0xA79D, 'V'), + (0xA79E, 'M', 'ꞟ'), + (0xA79F, 'V'), + (0xA7A0, 'M', 'ꞡ'), + (0xA7A1, 'V'), + (0xA7A2, 'M', 'ꞣ'), + (0xA7A3, 'V'), + (0xA7A4, 'M', 'ꞥ'), + (0xA7A5, 'V'), + (0xA7A6, 'M', 'ꞧ'), + (0xA7A7, 'V'), + (0xA7A8, 'M', 'ꞩ'), + (0xA7A9, 'V'), + (0xA7AA, 'M', 'ɦ'), + (0xA7AB, 'M', 'ɜ'), + (0xA7AC, 'M', 'ɡ'), + (0xA7AD, 'M', 'ɬ'), + (0xA7AE, 'M', 'ɪ'), + (0xA7AF, 'V'), + (0xA7B0, 'M', 'ʞ'), + (0xA7B1, 'M', 'ʇ'), + (0xA7B2, 'M', 'ʝ'), + (0xA7B3, 'M', 'ꭓ'), + (0xA7B4, 'M', 'ꞵ'), + (0xA7B5, 'V'), + (0xA7B6, 'M', 'ꞷ'), + (0xA7B7, 'V'), + (0xA7B8, 'M', 'ꞹ'), + (0xA7B9, 'V'), + (0xA7BA, 'M', 'ꞻ'), + (0xA7BB, 'V'), + (0xA7BC, 'M', 'ꞽ'), + (0xA7BD, 'V'), + (0xA7BE, 'M', 'ꞿ'), + (0xA7BF, 'V'), + (0xA7C0, 'M', 'ꟁ'), + (0xA7C1, 'V'), + (0xA7C2, 'M', 'ꟃ'), + (0xA7C3, 'V'), + (0xA7C4, 'M', 'ꞔ'), + (0xA7C5, 'M', 'ʂ'), + (0xA7C6, 'M', 'ᶎ'), + (0xA7C7, 'M', 'ꟈ'), + (0xA7C8, 'V'), + (0xA7C9, 'M', 'ꟊ'), + (0xA7CA, 'V'), + (0xA7CB, 'X'), + (0xA7D0, 'M', 'ꟑ'), + (0xA7D1, 'V'), + (0xA7D2, 'X'), + (0xA7D3, 'V'), + (0xA7D4, 'X'), + (0xA7D5, 'V'), + (0xA7D6, 'M', 'ꟗ'), + (0xA7D7, 'V'), + (0xA7D8, 'M', 'ꟙ'), + (0xA7D9, 'V'), + (0xA7DA, 'X'), + (0xA7F2, 'M', 'c'), + (0xA7F3, 'M', 'f'), + (0xA7F4, 'M', 'q'), + (0xA7F5, 'M', 'ꟶ'), + (0xA7F6, 'V'), + (0xA7F8, 'M', 'ħ'), + (0xA7F9, 'M', 'œ'), + (0xA7FA, 'V'), + (0xA82D, 'X'), + (0xA830, 'V'), + (0xA83A, 'X'), + (0xA840, 'V'), + (0xA878, 'X'), + (0xA880, 'V'), + (0xA8C6, 'X'), + ] + +def _seg_38() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA8CE, 'V'), + (0xA8DA, 'X'), + (0xA8E0, 'V'), + (0xA954, 'X'), + (0xA95F, 'V'), + (0xA97D, 'X'), + (0xA980, 'V'), + (0xA9CE, 'X'), + (0xA9CF, 'V'), + (0xA9DA, 'X'), + (0xA9DE, 'V'), + (0xA9FF, 'X'), + (0xAA00, 'V'), + (0xAA37, 'X'), + (0xAA40, 'V'), + (0xAA4E, 'X'), + (0xAA50, 'V'), + (0xAA5A, 'X'), + (0xAA5C, 'V'), + (0xAAC3, 'X'), + (0xAADB, 'V'), + (0xAAF7, 'X'), + (0xAB01, 'V'), + (0xAB07, 'X'), + (0xAB09, 'V'), + (0xAB0F, 'X'), + (0xAB11, 'V'), + (0xAB17, 'X'), + (0xAB20, 'V'), + (0xAB27, 'X'), + (0xAB28, 'V'), + (0xAB2F, 'X'), + (0xAB30, 'V'), + (0xAB5C, 'M', 'ꜧ'), + (0xAB5D, 'M', 'ꬷ'), + (0xAB5E, 'M', 'ɫ'), + (0xAB5F, 'M', 'ꭒ'), + (0xAB60, 'V'), + (0xAB69, 'M', 'ʍ'), + (0xAB6A, 'V'), + (0xAB6C, 'X'), + (0xAB70, 'M', 'Ꭰ'), + (0xAB71, 'M', 'Ꭱ'), + (0xAB72, 'M', 'Ꭲ'), + (0xAB73, 'M', 'Ꭳ'), + (0xAB74, 'M', 'Ꭴ'), + (0xAB75, 'M', 'Ꭵ'), + (0xAB76, 'M', 'Ꭶ'), + (0xAB77, 'M', 'Ꭷ'), + (0xAB78, 'M', 'Ꭸ'), + (0xAB79, 'M', 'Ꭹ'), + (0xAB7A, 'M', 'Ꭺ'), + (0xAB7B, 'M', 'Ꭻ'), + (0xAB7C, 'M', 'Ꭼ'), + (0xAB7D, 'M', 'Ꭽ'), + (0xAB7E, 'M', 'Ꭾ'), + (0xAB7F, 'M', 'Ꭿ'), + (0xAB80, 'M', 'Ꮀ'), + (0xAB81, 'M', 'Ꮁ'), + (0xAB82, 'M', 'Ꮂ'), + (0xAB83, 'M', 'Ꮃ'), + (0xAB84, 'M', 'Ꮄ'), + (0xAB85, 'M', 'Ꮅ'), + (0xAB86, 'M', 'Ꮆ'), + (0xAB87, 'M', 'Ꮇ'), + (0xAB88, 'M', 'Ꮈ'), + (0xAB89, 'M', 'Ꮉ'), + (0xAB8A, 'M', 'Ꮊ'), + (0xAB8B, 'M', 'Ꮋ'), + (0xAB8C, 'M', 'Ꮌ'), + (0xAB8D, 'M', 'Ꮍ'), + (0xAB8E, 'M', 'Ꮎ'), + (0xAB8F, 'M', 'Ꮏ'), + (0xAB90, 'M', 'Ꮐ'), + (0xAB91, 'M', 'Ꮑ'), + (0xAB92, 'M', 'Ꮒ'), + (0xAB93, 'M', 'Ꮓ'), + (0xAB94, 'M', 'Ꮔ'), + (0xAB95, 'M', 'Ꮕ'), + (0xAB96, 'M', 'Ꮖ'), + (0xAB97, 'M', 'Ꮗ'), + (0xAB98, 'M', 'Ꮘ'), + (0xAB99, 'M', 'Ꮙ'), + (0xAB9A, 'M', 'Ꮚ'), + (0xAB9B, 'M', 'Ꮛ'), + (0xAB9C, 'M', 'Ꮜ'), + (0xAB9D, 'M', 'Ꮝ'), + (0xAB9E, 'M', 'Ꮞ'), + (0xAB9F, 'M', 'Ꮟ'), + (0xABA0, 'M', 'Ꮠ'), + (0xABA1, 'M', 'Ꮡ'), + (0xABA2, 'M', 'Ꮢ'), + (0xABA3, 'M', 'Ꮣ'), + (0xABA4, 'M', 'Ꮤ'), + (0xABA5, 'M', 'Ꮥ'), + (0xABA6, 'M', 'Ꮦ'), + (0xABA7, 'M', 'Ꮧ'), + (0xABA8, 'M', 'Ꮨ'), + (0xABA9, 'M', 'Ꮩ'), + (0xABAA, 'M', 'Ꮪ'), + ] + +def _seg_39() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xABAB, 'M', 'Ꮫ'), + (0xABAC, 'M', 'Ꮬ'), + (0xABAD, 'M', 'Ꮭ'), + (0xABAE, 'M', 'Ꮮ'), + (0xABAF, 'M', 'Ꮯ'), + (0xABB0, 'M', 'Ꮰ'), + (0xABB1, 'M', 'Ꮱ'), + (0xABB2, 'M', 'Ꮲ'), + (0xABB3, 'M', 'Ꮳ'), + (0xABB4, 'M', 'Ꮴ'), + (0xABB5, 'M', 'Ꮵ'), + (0xABB6, 'M', 'Ꮶ'), + (0xABB7, 'M', 'Ꮷ'), + (0xABB8, 'M', 'Ꮸ'), + (0xABB9, 'M', 'Ꮹ'), + (0xABBA, 'M', 'Ꮺ'), + (0xABBB, 'M', 'Ꮻ'), + (0xABBC, 'M', 'Ꮼ'), + (0xABBD, 'M', 'Ꮽ'), + (0xABBE, 'M', 'Ꮾ'), + (0xABBF, 'M', 'Ꮿ'), + (0xABC0, 'V'), + (0xABEE, 'X'), + (0xABF0, 'V'), + (0xABFA, 'X'), + (0xAC00, 'V'), + (0xD7A4, 'X'), + (0xD7B0, 'V'), + (0xD7C7, 'X'), + (0xD7CB, 'V'), + (0xD7FC, 'X'), + (0xF900, 'M', '豈'), + (0xF901, 'M', '更'), + (0xF902, 'M', '車'), + (0xF903, 'M', '賈'), + (0xF904, 'M', '滑'), + (0xF905, 'M', '串'), + (0xF906, 'M', '句'), + (0xF907, 'M', '龜'), + (0xF909, 'M', '契'), + (0xF90A, 'M', '金'), + (0xF90B, 'M', '喇'), + (0xF90C, 'M', '奈'), + (0xF90D, 'M', '懶'), + (0xF90E, 'M', '癩'), + (0xF90F, 'M', '羅'), + (0xF910, 'M', '蘿'), + (0xF911, 'M', '螺'), + (0xF912, 'M', '裸'), + (0xF913, 'M', '邏'), + (0xF914, 'M', '樂'), + (0xF915, 'M', '洛'), + (0xF916, 'M', '烙'), + (0xF917, 'M', '珞'), + (0xF918, 'M', '落'), + (0xF919, 'M', '酪'), + (0xF91A, 'M', '駱'), + (0xF91B, 'M', '亂'), + (0xF91C, 'M', '卵'), + (0xF91D, 'M', '欄'), + (0xF91E, 'M', '爛'), + (0xF91F, 'M', '蘭'), + (0xF920, 'M', '鸞'), + (0xF921, 'M', '嵐'), + (0xF922, 'M', '濫'), + (0xF923, 'M', '藍'), + (0xF924, 'M', '襤'), + (0xF925, 'M', '拉'), + (0xF926, 'M', '臘'), + (0xF927, 'M', '蠟'), + (0xF928, 'M', '廊'), + (0xF929, 'M', '朗'), + (0xF92A, 'M', '浪'), + (0xF92B, 'M', '狼'), + (0xF92C, 'M', '郎'), + (0xF92D, 'M', '來'), + (0xF92E, 'M', '冷'), + (0xF92F, 'M', '勞'), + (0xF930, 'M', '擄'), + (0xF931, 'M', '櫓'), + (0xF932, 'M', '爐'), + (0xF933, 'M', '盧'), + (0xF934, 'M', '老'), + (0xF935, 'M', '蘆'), + (0xF936, 'M', '虜'), + (0xF937, 'M', '路'), + (0xF938, 'M', '露'), + (0xF939, 'M', '魯'), + (0xF93A, 'M', '鷺'), + (0xF93B, 'M', '碌'), + (0xF93C, 'M', '祿'), + (0xF93D, 'M', '綠'), + (0xF93E, 'M', '菉'), + (0xF93F, 'M', '錄'), + (0xF940, 'M', '鹿'), + (0xF941, 'M', '論'), + (0xF942, 'M', '壟'), + (0xF943, 'M', '弄'), + (0xF944, 'M', '籠'), + (0xF945, 'M', '聾'), + ] + +def _seg_40() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xF946, 'M', '牢'), + (0xF947, 'M', '磊'), + (0xF948, 'M', '賂'), + (0xF949, 'M', '雷'), + (0xF94A, 'M', '壘'), + (0xF94B, 'M', '屢'), + (0xF94C, 'M', '樓'), + (0xF94D, 'M', '淚'), + (0xF94E, 'M', '漏'), + (0xF94F, 'M', '累'), + (0xF950, 'M', '縷'), + (0xF951, 'M', '陋'), + (0xF952, 'M', '勒'), + (0xF953, 'M', '肋'), + (0xF954, 'M', '凜'), + (0xF955, 'M', '凌'), + (0xF956, 'M', '稜'), + (0xF957, 'M', '綾'), + (0xF958, 'M', '菱'), + (0xF959, 'M', '陵'), + (0xF95A, 'M', '讀'), + (0xF95B, 'M', '拏'), + (0xF95C, 'M', '樂'), + (0xF95D, 'M', '諾'), + (0xF95E, 'M', '丹'), + (0xF95F, 'M', '寧'), + (0xF960, 'M', '怒'), + (0xF961, 'M', '率'), + (0xF962, 'M', '異'), + (0xF963, 'M', '北'), + (0xF964, 'M', '磻'), + (0xF965, 'M', '便'), + (0xF966, 'M', '復'), + (0xF967, 'M', '不'), + (0xF968, 'M', '泌'), + (0xF969, 'M', '數'), + (0xF96A, 'M', '索'), + (0xF96B, 'M', '參'), + (0xF96C, 'M', '塞'), + (0xF96D, 'M', '省'), + (0xF96E, 'M', '葉'), + (0xF96F, 'M', '說'), + (0xF970, 'M', '殺'), + (0xF971, 'M', '辰'), + (0xF972, 'M', '沈'), + (0xF973, 'M', '拾'), + (0xF974, 'M', '若'), + (0xF975, 'M', '掠'), + (0xF976, 'M', '略'), + (0xF977, 'M', '亮'), + (0xF978, 'M', '兩'), + (0xF979, 'M', '凉'), + (0xF97A, 'M', '梁'), + (0xF97B, 'M', '糧'), + (0xF97C, 'M', '良'), + (0xF97D, 'M', '諒'), + (0xF97E, 'M', '量'), + (0xF97F, 'M', '勵'), + (0xF980, 'M', '呂'), + (0xF981, 'M', '女'), + (0xF982, 'M', '廬'), + (0xF983, 'M', '旅'), + (0xF984, 'M', '濾'), + (0xF985, 'M', '礪'), + (0xF986, 'M', '閭'), + (0xF987, 'M', '驪'), + (0xF988, 'M', '麗'), + (0xF989, 'M', '黎'), + (0xF98A, 'M', '力'), + (0xF98B, 'M', '曆'), + (0xF98C, 'M', '歷'), + (0xF98D, 'M', '轢'), + (0xF98E, 'M', '年'), + (0xF98F, 'M', '憐'), + (0xF990, 'M', '戀'), + (0xF991, 'M', '撚'), + (0xF992, 'M', '漣'), + (0xF993, 'M', '煉'), + (0xF994, 'M', '璉'), + (0xF995, 'M', '秊'), + (0xF996, 'M', '練'), + (0xF997, 'M', '聯'), + (0xF998, 'M', '輦'), + (0xF999, 'M', '蓮'), + (0xF99A, 'M', '連'), + (0xF99B, 'M', '鍊'), + (0xF99C, 'M', '列'), + (0xF99D, 'M', '劣'), + (0xF99E, 'M', '咽'), + (0xF99F, 'M', '烈'), + (0xF9A0, 'M', '裂'), + (0xF9A1, 'M', '說'), + (0xF9A2, 'M', '廉'), + (0xF9A3, 'M', '念'), + (0xF9A4, 'M', '捻'), + (0xF9A5, 'M', '殮'), + (0xF9A6, 'M', '簾'), + (0xF9A7, 'M', '獵'), + (0xF9A8, 'M', '令'), + (0xF9A9, 'M', '囹'), + ] + +def _seg_41() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xF9AA, 'M', '寧'), + (0xF9AB, 'M', '嶺'), + (0xF9AC, 'M', '怜'), + (0xF9AD, 'M', '玲'), + (0xF9AE, 'M', '瑩'), + (0xF9AF, 'M', '羚'), + (0xF9B0, 'M', '聆'), + (0xF9B1, 'M', '鈴'), + (0xF9B2, 'M', '零'), + (0xF9B3, 'M', '靈'), + (0xF9B4, 'M', '領'), + (0xF9B5, 'M', '例'), + (0xF9B6, 'M', '禮'), + (0xF9B7, 'M', '醴'), + (0xF9B8, 'M', '隸'), + (0xF9B9, 'M', '惡'), + (0xF9BA, 'M', '了'), + (0xF9BB, 'M', '僚'), + (0xF9BC, 'M', '寮'), + (0xF9BD, 'M', '尿'), + (0xF9BE, 'M', '料'), + (0xF9BF, 'M', '樂'), + (0xF9C0, 'M', '燎'), + (0xF9C1, 'M', '療'), + (0xF9C2, 'M', '蓼'), + (0xF9C3, 'M', '遼'), + (0xF9C4, 'M', '龍'), + (0xF9C5, 'M', '暈'), + (0xF9C6, 'M', '阮'), + (0xF9C7, 'M', '劉'), + (0xF9C8, 'M', '杻'), + (0xF9C9, 'M', '柳'), + (0xF9CA, 'M', '流'), + (0xF9CB, 'M', '溜'), + (0xF9CC, 'M', '琉'), + (0xF9CD, 'M', '留'), + (0xF9CE, 'M', '硫'), + (0xF9CF, 'M', '紐'), + (0xF9D0, 'M', '類'), + (0xF9D1, 'M', '六'), + (0xF9D2, 'M', '戮'), + (0xF9D3, 'M', '陸'), + (0xF9D4, 'M', '倫'), + (0xF9D5, 'M', '崙'), + (0xF9D6, 'M', '淪'), + (0xF9D7, 'M', '輪'), + (0xF9D8, 'M', '律'), + (0xF9D9, 'M', '慄'), + (0xF9DA, 'M', '栗'), + (0xF9DB, 'M', '率'), + (0xF9DC, 'M', '隆'), + (0xF9DD, 'M', '利'), + (0xF9DE, 'M', '吏'), + (0xF9DF, 'M', '履'), + (0xF9E0, 'M', '易'), + (0xF9E1, 'M', '李'), + (0xF9E2, 'M', '梨'), + (0xF9E3, 'M', '泥'), + (0xF9E4, 'M', '理'), + (0xF9E5, 'M', '痢'), + (0xF9E6, 'M', '罹'), + (0xF9E7, 'M', '裏'), + (0xF9E8, 'M', '裡'), + (0xF9E9, 'M', '里'), + (0xF9EA, 'M', '離'), + (0xF9EB, 'M', '匿'), + (0xF9EC, 'M', '溺'), + (0xF9ED, 'M', '吝'), + (0xF9EE, 'M', '燐'), + (0xF9EF, 'M', '璘'), + (0xF9F0, 'M', '藺'), + (0xF9F1, 'M', '隣'), + (0xF9F2, 'M', '鱗'), + (0xF9F3, 'M', '麟'), + (0xF9F4, 'M', '林'), + (0xF9F5, 'M', '淋'), + (0xF9F6, 'M', '臨'), + (0xF9F7, 'M', '立'), + (0xF9F8, 'M', '笠'), + (0xF9F9, 'M', '粒'), + (0xF9FA, 'M', '狀'), + (0xF9FB, 'M', '炙'), + (0xF9FC, 'M', '識'), + (0xF9FD, 'M', '什'), + (0xF9FE, 'M', '茶'), + (0xF9FF, 'M', '刺'), + (0xFA00, 'M', '切'), + (0xFA01, 'M', '度'), + (0xFA02, 'M', '拓'), + (0xFA03, 'M', '糖'), + (0xFA04, 'M', '宅'), + (0xFA05, 'M', '洞'), + (0xFA06, 'M', '暴'), + (0xFA07, 'M', '輻'), + (0xFA08, 'M', '行'), + (0xFA09, 'M', '降'), + (0xFA0A, 'M', '見'), + (0xFA0B, 'M', '廓'), + (0xFA0C, 'M', '兀'), + (0xFA0D, 'M', '嗀'), + ] + +def _seg_42() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFA0E, 'V'), + (0xFA10, 'M', '塚'), + (0xFA11, 'V'), + (0xFA12, 'M', '晴'), + (0xFA13, 'V'), + (0xFA15, 'M', '凞'), + (0xFA16, 'M', '猪'), + (0xFA17, 'M', '益'), + (0xFA18, 'M', '礼'), + (0xFA19, 'M', '神'), + (0xFA1A, 'M', '祥'), + (0xFA1B, 'M', '福'), + (0xFA1C, 'M', '靖'), + (0xFA1D, 'M', '精'), + (0xFA1E, 'M', '羽'), + (0xFA1F, 'V'), + (0xFA20, 'M', '蘒'), + (0xFA21, 'V'), + (0xFA22, 'M', '諸'), + (0xFA23, 'V'), + (0xFA25, 'M', '逸'), + (0xFA26, 'M', '都'), + (0xFA27, 'V'), + (0xFA2A, 'M', '飯'), + (0xFA2B, 'M', '飼'), + (0xFA2C, 'M', '館'), + (0xFA2D, 'M', '鶴'), + (0xFA2E, 'M', '郞'), + (0xFA2F, 'M', '隷'), + (0xFA30, 'M', '侮'), + (0xFA31, 'M', '僧'), + (0xFA32, 'M', '免'), + (0xFA33, 'M', '勉'), + (0xFA34, 'M', '勤'), + (0xFA35, 'M', '卑'), + (0xFA36, 'M', '喝'), + (0xFA37, 'M', '嘆'), + (0xFA38, 'M', '器'), + (0xFA39, 'M', '塀'), + (0xFA3A, 'M', '墨'), + (0xFA3B, 'M', '層'), + (0xFA3C, 'M', '屮'), + (0xFA3D, 'M', '悔'), + (0xFA3E, 'M', '慨'), + (0xFA3F, 'M', '憎'), + (0xFA40, 'M', '懲'), + (0xFA41, 'M', '敏'), + (0xFA42, 'M', '既'), + (0xFA43, 'M', '暑'), + (0xFA44, 'M', '梅'), + (0xFA45, 'M', '海'), + (0xFA46, 'M', '渚'), + (0xFA47, 'M', '漢'), + (0xFA48, 'M', '煮'), + (0xFA49, 'M', '爫'), + (0xFA4A, 'M', '琢'), + (0xFA4B, 'M', '碑'), + (0xFA4C, 'M', '社'), + (0xFA4D, 'M', '祉'), + (0xFA4E, 'M', '祈'), + (0xFA4F, 'M', '祐'), + (0xFA50, 'M', '祖'), + (0xFA51, 'M', '祝'), + (0xFA52, 'M', '禍'), + (0xFA53, 'M', '禎'), + (0xFA54, 'M', '穀'), + (0xFA55, 'M', '突'), + (0xFA56, 'M', '節'), + (0xFA57, 'M', '練'), + (0xFA58, 'M', '縉'), + (0xFA59, 'M', '繁'), + (0xFA5A, 'M', '署'), + (0xFA5B, 'M', '者'), + (0xFA5C, 'M', '臭'), + (0xFA5D, 'M', '艹'), + (0xFA5F, 'M', '著'), + (0xFA60, 'M', '褐'), + (0xFA61, 'M', '視'), + (0xFA62, 'M', '謁'), + (0xFA63, 'M', '謹'), + (0xFA64, 'M', '賓'), + (0xFA65, 'M', '贈'), + (0xFA66, 'M', '辶'), + (0xFA67, 'M', '逸'), + (0xFA68, 'M', '難'), + (0xFA69, 'M', '響'), + (0xFA6A, 'M', '頻'), + (0xFA6B, 'M', '恵'), + (0xFA6C, 'M', '𤋮'), + (0xFA6D, 'M', '舘'), + (0xFA6E, 'X'), + (0xFA70, 'M', '並'), + (0xFA71, 'M', '况'), + (0xFA72, 'M', '全'), + (0xFA73, 'M', '侀'), + (0xFA74, 'M', '充'), + (0xFA75, 'M', '冀'), + (0xFA76, 'M', '勇'), + (0xFA77, 'M', '勺'), + (0xFA78, 'M', '喝'), + ] + +def _seg_43() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFA79, 'M', '啕'), + (0xFA7A, 'M', '喙'), + (0xFA7B, 'M', '嗢'), + (0xFA7C, 'M', '塚'), + (0xFA7D, 'M', '墳'), + (0xFA7E, 'M', '奄'), + (0xFA7F, 'M', '奔'), + (0xFA80, 'M', '婢'), + (0xFA81, 'M', '嬨'), + (0xFA82, 'M', '廒'), + (0xFA83, 'M', '廙'), + (0xFA84, 'M', '彩'), + (0xFA85, 'M', '徭'), + (0xFA86, 'M', '惘'), + (0xFA87, 'M', '慎'), + (0xFA88, 'M', '愈'), + (0xFA89, 'M', '憎'), + (0xFA8A, 'M', '慠'), + (0xFA8B, 'M', '懲'), + (0xFA8C, 'M', '戴'), + (0xFA8D, 'M', '揄'), + (0xFA8E, 'M', '搜'), + (0xFA8F, 'M', '摒'), + (0xFA90, 'M', '敖'), + (0xFA91, 'M', '晴'), + (0xFA92, 'M', '朗'), + (0xFA93, 'M', '望'), + (0xFA94, 'M', '杖'), + (0xFA95, 'M', '歹'), + (0xFA96, 'M', '殺'), + (0xFA97, 'M', '流'), + (0xFA98, 'M', '滛'), + (0xFA99, 'M', '滋'), + (0xFA9A, 'M', '漢'), + (0xFA9B, 'M', '瀞'), + (0xFA9C, 'M', '煮'), + (0xFA9D, 'M', '瞧'), + (0xFA9E, 'M', '爵'), + (0xFA9F, 'M', '犯'), + (0xFAA0, 'M', '猪'), + (0xFAA1, 'M', '瑱'), + (0xFAA2, 'M', '甆'), + (0xFAA3, 'M', '画'), + (0xFAA4, 'M', '瘝'), + (0xFAA5, 'M', '瘟'), + (0xFAA6, 'M', '益'), + (0xFAA7, 'M', '盛'), + (0xFAA8, 'M', '直'), + (0xFAA9, 'M', '睊'), + (0xFAAA, 'M', '着'), + (0xFAAB, 'M', '磌'), + (0xFAAC, 'M', '窱'), + (0xFAAD, 'M', '節'), + (0xFAAE, 'M', '类'), + (0xFAAF, 'M', '絛'), + (0xFAB0, 'M', '練'), + (0xFAB1, 'M', '缾'), + (0xFAB2, 'M', '者'), + (0xFAB3, 'M', '荒'), + (0xFAB4, 'M', '華'), + (0xFAB5, 'M', '蝹'), + (0xFAB6, 'M', '襁'), + (0xFAB7, 'M', '覆'), + (0xFAB8, 'M', '視'), + (0xFAB9, 'M', '調'), + (0xFABA, 'M', '諸'), + (0xFABB, 'M', '請'), + (0xFABC, 'M', '謁'), + (0xFABD, 'M', '諾'), + (0xFABE, 'M', '諭'), + (0xFABF, 'M', '謹'), + (0xFAC0, 'M', '變'), + (0xFAC1, 'M', '贈'), + (0xFAC2, 'M', '輸'), + (0xFAC3, 'M', '遲'), + (0xFAC4, 'M', '醙'), + (0xFAC5, 'M', '鉶'), + (0xFAC6, 'M', '陼'), + (0xFAC7, 'M', '難'), + (0xFAC8, 'M', '靖'), + (0xFAC9, 'M', '韛'), + (0xFACA, 'M', '響'), + (0xFACB, 'M', '頋'), + (0xFACC, 'M', '頻'), + (0xFACD, 'M', '鬒'), + (0xFACE, 'M', '龜'), + (0xFACF, 'M', '𢡊'), + (0xFAD0, 'M', '𢡄'), + (0xFAD1, 'M', '𣏕'), + (0xFAD2, 'M', '㮝'), + (0xFAD3, 'M', '䀘'), + (0xFAD4, 'M', '䀹'), + (0xFAD5, 'M', '𥉉'), + (0xFAD6, 'M', '𥳐'), + (0xFAD7, 'M', '𧻓'), + (0xFAD8, 'M', '齃'), + (0xFAD9, 'M', '龎'), + (0xFADA, 'X'), + (0xFB00, 'M', 'ff'), + (0xFB01, 'M', 'fi'), + ] + +def _seg_44() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFB02, 'M', 'fl'), + (0xFB03, 'M', 'ffi'), + (0xFB04, 'M', 'ffl'), + (0xFB05, 'M', 'st'), + (0xFB07, 'X'), + (0xFB13, 'M', 'մն'), + (0xFB14, 'M', 'մե'), + (0xFB15, 'M', 'մի'), + (0xFB16, 'M', 'վն'), + (0xFB17, 'M', 'մխ'), + (0xFB18, 'X'), + (0xFB1D, 'M', 'יִ'), + (0xFB1E, 'V'), + (0xFB1F, 'M', 'ײַ'), + (0xFB20, 'M', 'ע'), + (0xFB21, 'M', 'א'), + (0xFB22, 'M', 'ד'), + (0xFB23, 'M', 'ה'), + (0xFB24, 'M', 'כ'), + (0xFB25, 'M', 'ל'), + (0xFB26, 'M', 'ם'), + (0xFB27, 'M', 'ר'), + (0xFB28, 'M', 'ת'), + (0xFB29, '3', '+'), + (0xFB2A, 'M', 'שׁ'), + (0xFB2B, 'M', 'שׂ'), + (0xFB2C, 'M', 'שּׁ'), + (0xFB2D, 'M', 'שּׂ'), + (0xFB2E, 'M', 'אַ'), + (0xFB2F, 'M', 'אָ'), + (0xFB30, 'M', 'אּ'), + (0xFB31, 'M', 'בּ'), + (0xFB32, 'M', 'גּ'), + (0xFB33, 'M', 'דּ'), + (0xFB34, 'M', 'הּ'), + (0xFB35, 'M', 'וּ'), + (0xFB36, 'M', 'זּ'), + (0xFB37, 'X'), + (0xFB38, 'M', 'טּ'), + (0xFB39, 'M', 'יּ'), + (0xFB3A, 'M', 'ךּ'), + (0xFB3B, 'M', 'כּ'), + (0xFB3C, 'M', 'לּ'), + (0xFB3D, 'X'), + (0xFB3E, 'M', 'מּ'), + (0xFB3F, 'X'), + (0xFB40, 'M', 'נּ'), + (0xFB41, 'M', 'סּ'), + (0xFB42, 'X'), + (0xFB43, 'M', 'ףּ'), + (0xFB44, 'M', 'פּ'), + (0xFB45, 'X'), + (0xFB46, 'M', 'צּ'), + (0xFB47, 'M', 'קּ'), + (0xFB48, 'M', 'רּ'), + (0xFB49, 'M', 'שּ'), + (0xFB4A, 'M', 'תּ'), + (0xFB4B, 'M', 'וֹ'), + (0xFB4C, 'M', 'בֿ'), + (0xFB4D, 'M', 'כֿ'), + (0xFB4E, 'M', 'פֿ'), + (0xFB4F, 'M', 'אל'), + (0xFB50, 'M', 'ٱ'), + (0xFB52, 'M', 'ٻ'), + (0xFB56, 'M', 'پ'), + (0xFB5A, 'M', 'ڀ'), + (0xFB5E, 'M', 'ٺ'), + (0xFB62, 'M', 'ٿ'), + (0xFB66, 'M', 'ٹ'), + (0xFB6A, 'M', 'ڤ'), + (0xFB6E, 'M', 'ڦ'), + (0xFB72, 'M', 'ڄ'), + (0xFB76, 'M', 'ڃ'), + (0xFB7A, 'M', 'چ'), + (0xFB7E, 'M', 'ڇ'), + (0xFB82, 'M', 'ڍ'), + (0xFB84, 'M', 'ڌ'), + (0xFB86, 'M', 'ڎ'), + (0xFB88, 'M', 'ڈ'), + (0xFB8A, 'M', 'ژ'), + (0xFB8C, 'M', 'ڑ'), + (0xFB8E, 'M', 'ک'), + (0xFB92, 'M', 'گ'), + (0xFB96, 'M', 'ڳ'), + (0xFB9A, 'M', 'ڱ'), + (0xFB9E, 'M', 'ں'), + (0xFBA0, 'M', 'ڻ'), + (0xFBA4, 'M', 'ۀ'), + (0xFBA6, 'M', 'ہ'), + (0xFBAA, 'M', 'ھ'), + (0xFBAE, 'M', 'ے'), + (0xFBB0, 'M', 'ۓ'), + (0xFBB2, 'V'), + (0xFBC3, 'X'), + (0xFBD3, 'M', 'ڭ'), + (0xFBD7, 'M', 'ۇ'), + (0xFBD9, 'M', 'ۆ'), + (0xFBDB, 'M', 'ۈ'), + (0xFBDD, 'M', 'ۇٴ'), + (0xFBDE, 'M', 'ۋ'), + ] + +def _seg_45() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFBE0, 'M', 'ۅ'), + (0xFBE2, 'M', 'ۉ'), + (0xFBE4, 'M', 'ې'), + (0xFBE8, 'M', 'ى'), + (0xFBEA, 'M', 'ئا'), + (0xFBEC, 'M', 'ئە'), + (0xFBEE, 'M', 'ئو'), + (0xFBF0, 'M', 'ئۇ'), + (0xFBF2, 'M', 'ئۆ'), + (0xFBF4, 'M', 'ئۈ'), + (0xFBF6, 'M', 'ئې'), + (0xFBF9, 'M', 'ئى'), + (0xFBFC, 'M', 'ی'), + (0xFC00, 'M', 'ئج'), + (0xFC01, 'M', 'ئح'), + (0xFC02, 'M', 'ئم'), + (0xFC03, 'M', 'ئى'), + (0xFC04, 'M', 'ئي'), + (0xFC05, 'M', 'بج'), + (0xFC06, 'M', 'بح'), + (0xFC07, 'M', 'بخ'), + (0xFC08, 'M', 'بم'), + (0xFC09, 'M', 'بى'), + (0xFC0A, 'M', 'بي'), + (0xFC0B, 'M', 'تج'), + (0xFC0C, 'M', 'تح'), + (0xFC0D, 'M', 'تخ'), + (0xFC0E, 'M', 'تم'), + (0xFC0F, 'M', 'تى'), + (0xFC10, 'M', 'تي'), + (0xFC11, 'M', 'ثج'), + (0xFC12, 'M', 'ثم'), + (0xFC13, 'M', 'ثى'), + (0xFC14, 'M', 'ثي'), + (0xFC15, 'M', 'جح'), + (0xFC16, 'M', 'جم'), + (0xFC17, 'M', 'حج'), + (0xFC18, 'M', 'حم'), + (0xFC19, 'M', 'خج'), + (0xFC1A, 'M', 'خح'), + (0xFC1B, 'M', 'خم'), + (0xFC1C, 'M', 'سج'), + (0xFC1D, 'M', 'سح'), + (0xFC1E, 'M', 'سخ'), + (0xFC1F, 'M', 'سم'), + (0xFC20, 'M', 'صح'), + (0xFC21, 'M', 'صم'), + (0xFC22, 'M', 'ضج'), + (0xFC23, 'M', 'ضح'), + (0xFC24, 'M', 'ضخ'), + (0xFC25, 'M', 'ضم'), + (0xFC26, 'M', 'طح'), + (0xFC27, 'M', 'طم'), + (0xFC28, 'M', 'ظم'), + (0xFC29, 'M', 'عج'), + (0xFC2A, 'M', 'عم'), + (0xFC2B, 'M', 'غج'), + (0xFC2C, 'M', 'غم'), + (0xFC2D, 'M', 'فج'), + (0xFC2E, 'M', 'فح'), + (0xFC2F, 'M', 'فخ'), + (0xFC30, 'M', 'فم'), + (0xFC31, 'M', 'فى'), + (0xFC32, 'M', 'في'), + (0xFC33, 'M', 'قح'), + (0xFC34, 'M', 'قم'), + (0xFC35, 'M', 'قى'), + (0xFC36, 'M', 'قي'), + (0xFC37, 'M', 'كا'), + (0xFC38, 'M', 'كج'), + (0xFC39, 'M', 'كح'), + (0xFC3A, 'M', 'كخ'), + (0xFC3B, 'M', 'كل'), + (0xFC3C, 'M', 'كم'), + (0xFC3D, 'M', 'كى'), + (0xFC3E, 'M', 'كي'), + (0xFC3F, 'M', 'لج'), + (0xFC40, 'M', 'لح'), + (0xFC41, 'M', 'لخ'), + (0xFC42, 'M', 'لم'), + (0xFC43, 'M', 'لى'), + (0xFC44, 'M', 'لي'), + (0xFC45, 'M', 'مج'), + (0xFC46, 'M', 'مح'), + (0xFC47, 'M', 'مخ'), + (0xFC48, 'M', 'مم'), + (0xFC49, 'M', 'مى'), + (0xFC4A, 'M', 'مي'), + (0xFC4B, 'M', 'نج'), + (0xFC4C, 'M', 'نح'), + (0xFC4D, 'M', 'نخ'), + (0xFC4E, 'M', 'نم'), + (0xFC4F, 'M', 'نى'), + (0xFC50, 'M', 'ني'), + (0xFC51, 'M', 'هج'), + (0xFC52, 'M', 'هم'), + (0xFC53, 'M', 'هى'), + (0xFC54, 'M', 'هي'), + (0xFC55, 'M', 'يج'), + (0xFC56, 'M', 'يح'), + ] + +def _seg_46() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFC57, 'M', 'يخ'), + (0xFC58, 'M', 'يم'), + (0xFC59, 'M', 'يى'), + (0xFC5A, 'M', 'يي'), + (0xFC5B, 'M', 'ذٰ'), + (0xFC5C, 'M', 'رٰ'), + (0xFC5D, 'M', 'ىٰ'), + (0xFC5E, '3', ' ٌّ'), + (0xFC5F, '3', ' ٍّ'), + (0xFC60, '3', ' َّ'), + (0xFC61, '3', ' ُّ'), + (0xFC62, '3', ' ِّ'), + (0xFC63, '3', ' ّٰ'), + (0xFC64, 'M', 'ئر'), + (0xFC65, 'M', 'ئز'), + (0xFC66, 'M', 'ئم'), + (0xFC67, 'M', 'ئن'), + (0xFC68, 'M', 'ئى'), + (0xFC69, 'M', 'ئي'), + (0xFC6A, 'M', 'بر'), + (0xFC6B, 'M', 'بز'), + (0xFC6C, 'M', 'بم'), + (0xFC6D, 'M', 'بن'), + (0xFC6E, 'M', 'بى'), + (0xFC6F, 'M', 'بي'), + (0xFC70, 'M', 'تر'), + (0xFC71, 'M', 'تز'), + (0xFC72, 'M', 'تم'), + (0xFC73, 'M', 'تن'), + (0xFC74, 'M', 'تى'), + (0xFC75, 'M', 'تي'), + (0xFC76, 'M', 'ثر'), + (0xFC77, 'M', 'ثز'), + (0xFC78, 'M', 'ثم'), + (0xFC79, 'M', 'ثن'), + (0xFC7A, 'M', 'ثى'), + (0xFC7B, 'M', 'ثي'), + (0xFC7C, 'M', 'فى'), + (0xFC7D, 'M', 'في'), + (0xFC7E, 'M', 'قى'), + (0xFC7F, 'M', 'قي'), + (0xFC80, 'M', 'كا'), + (0xFC81, 'M', 'كل'), + (0xFC82, 'M', 'كم'), + (0xFC83, 'M', 'كى'), + (0xFC84, 'M', 'كي'), + (0xFC85, 'M', 'لم'), + (0xFC86, 'M', 'لى'), + (0xFC87, 'M', 'لي'), + (0xFC88, 'M', 'ما'), + (0xFC89, 'M', 'مم'), + (0xFC8A, 'M', 'نر'), + (0xFC8B, 'M', 'نز'), + (0xFC8C, 'M', 'نم'), + (0xFC8D, 'M', 'نن'), + (0xFC8E, 'M', 'نى'), + (0xFC8F, 'M', 'ني'), + (0xFC90, 'M', 'ىٰ'), + (0xFC91, 'M', 'ير'), + (0xFC92, 'M', 'يز'), + (0xFC93, 'M', 'يم'), + (0xFC94, 'M', 'ين'), + (0xFC95, 'M', 'يى'), + (0xFC96, 'M', 'يي'), + (0xFC97, 'M', 'ئج'), + (0xFC98, 'M', 'ئح'), + (0xFC99, 'M', 'ئخ'), + (0xFC9A, 'M', 'ئم'), + (0xFC9B, 'M', 'ئه'), + (0xFC9C, 'M', 'بج'), + (0xFC9D, 'M', 'بح'), + (0xFC9E, 'M', 'بخ'), + (0xFC9F, 'M', 'بم'), + (0xFCA0, 'M', 'به'), + (0xFCA1, 'M', 'تج'), + (0xFCA2, 'M', 'تح'), + (0xFCA3, 'M', 'تخ'), + (0xFCA4, 'M', 'تم'), + (0xFCA5, 'M', 'ته'), + (0xFCA6, 'M', 'ثم'), + (0xFCA7, 'M', 'جح'), + (0xFCA8, 'M', 'جم'), + (0xFCA9, 'M', 'حج'), + (0xFCAA, 'M', 'حم'), + (0xFCAB, 'M', 'خج'), + (0xFCAC, 'M', 'خم'), + (0xFCAD, 'M', 'سج'), + (0xFCAE, 'M', 'سح'), + (0xFCAF, 'M', 'سخ'), + (0xFCB0, 'M', 'سم'), + (0xFCB1, 'M', 'صح'), + (0xFCB2, 'M', 'صخ'), + (0xFCB3, 'M', 'صم'), + (0xFCB4, 'M', 'ضج'), + (0xFCB5, 'M', 'ضح'), + (0xFCB6, 'M', 'ضخ'), + (0xFCB7, 'M', 'ضم'), + (0xFCB8, 'M', 'طح'), + (0xFCB9, 'M', 'ظم'), + (0xFCBA, 'M', 'عج'), + ] + +def _seg_47() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFCBB, 'M', 'عم'), + (0xFCBC, 'M', 'غج'), + (0xFCBD, 'M', 'غم'), + (0xFCBE, 'M', 'فج'), + (0xFCBF, 'M', 'فح'), + (0xFCC0, 'M', 'فخ'), + (0xFCC1, 'M', 'فم'), + (0xFCC2, 'M', 'قح'), + (0xFCC3, 'M', 'قم'), + (0xFCC4, 'M', 'كج'), + (0xFCC5, 'M', 'كح'), + (0xFCC6, 'M', 'كخ'), + (0xFCC7, 'M', 'كل'), + (0xFCC8, 'M', 'كم'), + (0xFCC9, 'M', 'لج'), + (0xFCCA, 'M', 'لح'), + (0xFCCB, 'M', 'لخ'), + (0xFCCC, 'M', 'لم'), + (0xFCCD, 'M', 'له'), + (0xFCCE, 'M', 'مج'), + (0xFCCF, 'M', 'مح'), + (0xFCD0, 'M', 'مخ'), + (0xFCD1, 'M', 'مم'), + (0xFCD2, 'M', 'نج'), + (0xFCD3, 'M', 'نح'), + (0xFCD4, 'M', 'نخ'), + (0xFCD5, 'M', 'نم'), + (0xFCD6, 'M', 'نه'), + (0xFCD7, 'M', 'هج'), + (0xFCD8, 'M', 'هم'), + (0xFCD9, 'M', 'هٰ'), + (0xFCDA, 'M', 'يج'), + (0xFCDB, 'M', 'يح'), + (0xFCDC, 'M', 'يخ'), + (0xFCDD, 'M', 'يم'), + (0xFCDE, 'M', 'يه'), + (0xFCDF, 'M', 'ئم'), + (0xFCE0, 'M', 'ئه'), + (0xFCE1, 'M', 'بم'), + (0xFCE2, 'M', 'به'), + (0xFCE3, 'M', 'تم'), + (0xFCE4, 'M', 'ته'), + (0xFCE5, 'M', 'ثم'), + (0xFCE6, 'M', 'ثه'), + (0xFCE7, 'M', 'سم'), + (0xFCE8, 'M', 'سه'), + (0xFCE9, 'M', 'شم'), + (0xFCEA, 'M', 'شه'), + (0xFCEB, 'M', 'كل'), + (0xFCEC, 'M', 'كم'), + (0xFCED, 'M', 'لم'), + (0xFCEE, 'M', 'نم'), + (0xFCEF, 'M', 'نه'), + (0xFCF0, 'M', 'يم'), + (0xFCF1, 'M', 'يه'), + (0xFCF2, 'M', 'ـَّ'), + (0xFCF3, 'M', 'ـُّ'), + (0xFCF4, 'M', 'ـِّ'), + (0xFCF5, 'M', 'طى'), + (0xFCF6, 'M', 'طي'), + (0xFCF7, 'M', 'عى'), + (0xFCF8, 'M', 'عي'), + (0xFCF9, 'M', 'غى'), + (0xFCFA, 'M', 'غي'), + (0xFCFB, 'M', 'سى'), + (0xFCFC, 'M', 'سي'), + (0xFCFD, 'M', 'شى'), + (0xFCFE, 'M', 'شي'), + (0xFCFF, 'M', 'حى'), + (0xFD00, 'M', 'حي'), + (0xFD01, 'M', 'جى'), + (0xFD02, 'M', 'جي'), + (0xFD03, 'M', 'خى'), + (0xFD04, 'M', 'خي'), + (0xFD05, 'M', 'صى'), + (0xFD06, 'M', 'صي'), + (0xFD07, 'M', 'ضى'), + (0xFD08, 'M', 'ضي'), + (0xFD09, 'M', 'شج'), + (0xFD0A, 'M', 'شح'), + (0xFD0B, 'M', 'شخ'), + (0xFD0C, 'M', 'شم'), + (0xFD0D, 'M', 'شر'), + (0xFD0E, 'M', 'سر'), + (0xFD0F, 'M', 'صر'), + (0xFD10, 'M', 'ضر'), + (0xFD11, 'M', 'طى'), + (0xFD12, 'M', 'طي'), + (0xFD13, 'M', 'عى'), + (0xFD14, 'M', 'عي'), + (0xFD15, 'M', 'غى'), + (0xFD16, 'M', 'غي'), + (0xFD17, 'M', 'سى'), + (0xFD18, 'M', 'سي'), + (0xFD19, 'M', 'شى'), + (0xFD1A, 'M', 'شي'), + (0xFD1B, 'M', 'حى'), + (0xFD1C, 'M', 'حي'), + (0xFD1D, 'M', 'جى'), + (0xFD1E, 'M', 'جي'), + ] + +def _seg_48() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFD1F, 'M', 'خى'), + (0xFD20, 'M', 'خي'), + (0xFD21, 'M', 'صى'), + (0xFD22, 'M', 'صي'), + (0xFD23, 'M', 'ضى'), + (0xFD24, 'M', 'ضي'), + (0xFD25, 'M', 'شج'), + (0xFD26, 'M', 'شح'), + (0xFD27, 'M', 'شخ'), + (0xFD28, 'M', 'شم'), + (0xFD29, 'M', 'شر'), + (0xFD2A, 'M', 'سر'), + (0xFD2B, 'M', 'صر'), + (0xFD2C, 'M', 'ضر'), + (0xFD2D, 'M', 'شج'), + (0xFD2E, 'M', 'شح'), + (0xFD2F, 'M', 'شخ'), + (0xFD30, 'M', 'شم'), + (0xFD31, 'M', 'سه'), + (0xFD32, 'M', 'شه'), + (0xFD33, 'M', 'طم'), + (0xFD34, 'M', 'سج'), + (0xFD35, 'M', 'سح'), + (0xFD36, 'M', 'سخ'), + (0xFD37, 'M', 'شج'), + (0xFD38, 'M', 'شح'), + (0xFD39, 'M', 'شخ'), + (0xFD3A, 'M', 'طم'), + (0xFD3B, 'M', 'ظم'), + (0xFD3C, 'M', 'اً'), + (0xFD3E, 'V'), + (0xFD50, 'M', 'تجم'), + (0xFD51, 'M', 'تحج'), + (0xFD53, 'M', 'تحم'), + (0xFD54, 'M', 'تخم'), + (0xFD55, 'M', 'تمج'), + (0xFD56, 'M', 'تمح'), + (0xFD57, 'M', 'تمخ'), + (0xFD58, 'M', 'جمح'), + (0xFD5A, 'M', 'حمي'), + (0xFD5B, 'M', 'حمى'), + (0xFD5C, 'M', 'سحج'), + (0xFD5D, 'M', 'سجح'), + (0xFD5E, 'M', 'سجى'), + (0xFD5F, 'M', 'سمح'), + (0xFD61, 'M', 'سمج'), + (0xFD62, 'M', 'سمم'), + (0xFD64, 'M', 'صحح'), + (0xFD66, 'M', 'صمم'), + (0xFD67, 'M', 'شحم'), + (0xFD69, 'M', 'شجي'), + (0xFD6A, 'M', 'شمخ'), + (0xFD6C, 'M', 'شمم'), + (0xFD6E, 'M', 'ضحى'), + (0xFD6F, 'M', 'ضخم'), + (0xFD71, 'M', 'طمح'), + (0xFD73, 'M', 'طمم'), + (0xFD74, 'M', 'طمي'), + (0xFD75, 'M', 'عجم'), + (0xFD76, 'M', 'عمم'), + (0xFD78, 'M', 'عمى'), + (0xFD79, 'M', 'غمم'), + (0xFD7A, 'M', 'غمي'), + (0xFD7B, 'M', 'غمى'), + (0xFD7C, 'M', 'فخم'), + (0xFD7E, 'M', 'قمح'), + (0xFD7F, 'M', 'قمم'), + (0xFD80, 'M', 'لحم'), + (0xFD81, 'M', 'لحي'), + (0xFD82, 'M', 'لحى'), + (0xFD83, 'M', 'لجج'), + (0xFD85, 'M', 'لخم'), + (0xFD87, 'M', 'لمح'), + (0xFD89, 'M', 'محج'), + (0xFD8A, 'M', 'محم'), + (0xFD8B, 'M', 'محي'), + (0xFD8C, 'M', 'مجح'), + (0xFD8D, 'M', 'مجم'), + (0xFD8E, 'M', 'مخج'), + (0xFD8F, 'M', 'مخم'), + (0xFD90, 'X'), + (0xFD92, 'M', 'مجخ'), + (0xFD93, 'M', 'همج'), + (0xFD94, 'M', 'همم'), + (0xFD95, 'M', 'نحم'), + (0xFD96, 'M', 'نحى'), + (0xFD97, 'M', 'نجم'), + (0xFD99, 'M', 'نجى'), + (0xFD9A, 'M', 'نمي'), + (0xFD9B, 'M', 'نمى'), + (0xFD9C, 'M', 'يمم'), + (0xFD9E, 'M', 'بخي'), + (0xFD9F, 'M', 'تجي'), + (0xFDA0, 'M', 'تجى'), + (0xFDA1, 'M', 'تخي'), + (0xFDA2, 'M', 'تخى'), + (0xFDA3, 'M', 'تمي'), + (0xFDA4, 'M', 'تمى'), + (0xFDA5, 'M', 'جمي'), + (0xFDA6, 'M', 'جحى'), + ] + +def _seg_49() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFDA7, 'M', 'جمى'), + (0xFDA8, 'M', 'سخى'), + (0xFDA9, 'M', 'صحي'), + (0xFDAA, 'M', 'شحي'), + (0xFDAB, 'M', 'ضحي'), + (0xFDAC, 'M', 'لجي'), + (0xFDAD, 'M', 'لمي'), + (0xFDAE, 'M', 'يحي'), + (0xFDAF, 'M', 'يجي'), + (0xFDB0, 'M', 'يمي'), + (0xFDB1, 'M', 'ممي'), + (0xFDB2, 'M', 'قمي'), + (0xFDB3, 'M', 'نحي'), + (0xFDB4, 'M', 'قمح'), + (0xFDB5, 'M', 'لحم'), + (0xFDB6, 'M', 'عمي'), + (0xFDB7, 'M', 'كمي'), + (0xFDB8, 'M', 'نجح'), + (0xFDB9, 'M', 'مخي'), + (0xFDBA, 'M', 'لجم'), + (0xFDBB, 'M', 'كمم'), + (0xFDBC, 'M', 'لجم'), + (0xFDBD, 'M', 'نجح'), + (0xFDBE, 'M', 'جحي'), + (0xFDBF, 'M', 'حجي'), + (0xFDC0, 'M', 'مجي'), + (0xFDC1, 'M', 'فمي'), + (0xFDC2, 'M', 'بحي'), + (0xFDC3, 'M', 'كمم'), + (0xFDC4, 'M', 'عجم'), + (0xFDC5, 'M', 'صمم'), + (0xFDC6, 'M', 'سخي'), + (0xFDC7, 'M', 'نجي'), + (0xFDC8, 'X'), + (0xFDCF, 'V'), + (0xFDD0, 'X'), + (0xFDF0, 'M', 'صلے'), + (0xFDF1, 'M', 'قلے'), + (0xFDF2, 'M', 'الله'), + (0xFDF3, 'M', 'اكبر'), + (0xFDF4, 'M', 'محمد'), + (0xFDF5, 'M', 'صلعم'), + (0xFDF6, 'M', 'رسول'), + (0xFDF7, 'M', 'عليه'), + (0xFDF8, 'M', 'وسلم'), + (0xFDF9, 'M', 'صلى'), + (0xFDFA, '3', 'صلى الله عليه وسلم'), + (0xFDFB, '3', 'جل جلاله'), + (0xFDFC, 'M', 'ریال'), + (0xFDFD, 'V'), + (0xFE00, 'I'), + (0xFE10, '3', ','), + (0xFE11, 'M', '、'), + (0xFE12, 'X'), + (0xFE13, '3', ':'), + (0xFE14, '3', ';'), + (0xFE15, '3', '!'), + (0xFE16, '3', '?'), + (0xFE17, 'M', '〖'), + (0xFE18, 'M', '〗'), + (0xFE19, 'X'), + (0xFE20, 'V'), + (0xFE30, 'X'), + (0xFE31, 'M', '—'), + (0xFE32, 'M', '–'), + (0xFE33, '3', '_'), + (0xFE35, '3', '('), + (0xFE36, '3', ')'), + (0xFE37, '3', '{'), + (0xFE38, '3', '}'), + (0xFE39, 'M', '〔'), + (0xFE3A, 'M', '〕'), + (0xFE3B, 'M', '【'), + (0xFE3C, 'M', '】'), + (0xFE3D, 'M', '《'), + (0xFE3E, 'M', '》'), + (0xFE3F, 'M', '〈'), + (0xFE40, 'M', '〉'), + (0xFE41, 'M', '「'), + (0xFE42, 'M', '」'), + (0xFE43, 'M', '『'), + (0xFE44, 'M', '』'), + (0xFE45, 'V'), + (0xFE47, '3', '['), + (0xFE48, '3', ']'), + (0xFE49, '3', ' ̅'), + (0xFE4D, '3', '_'), + (0xFE50, '3', ','), + (0xFE51, 'M', '、'), + (0xFE52, 'X'), + (0xFE54, '3', ';'), + (0xFE55, '3', ':'), + (0xFE56, '3', '?'), + (0xFE57, '3', '!'), + (0xFE58, 'M', '—'), + (0xFE59, '3', '('), + (0xFE5A, '3', ')'), + (0xFE5B, '3', '{'), + (0xFE5C, '3', '}'), + (0xFE5D, 'M', '〔'), + ] + +def _seg_50() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFE5E, 'M', '〕'), + (0xFE5F, '3', '#'), + (0xFE60, '3', '&'), + (0xFE61, '3', '*'), + (0xFE62, '3', '+'), + (0xFE63, 'M', '-'), + (0xFE64, '3', '<'), + (0xFE65, '3', '>'), + (0xFE66, '3', '='), + (0xFE67, 'X'), + (0xFE68, '3', '\\'), + (0xFE69, '3', '$'), + (0xFE6A, '3', '%'), + (0xFE6B, '3', '@'), + (0xFE6C, 'X'), + (0xFE70, '3', ' ً'), + (0xFE71, 'M', 'ـً'), + (0xFE72, '3', ' ٌ'), + (0xFE73, 'V'), + (0xFE74, '3', ' ٍ'), + (0xFE75, 'X'), + (0xFE76, '3', ' َ'), + (0xFE77, 'M', 'ـَ'), + (0xFE78, '3', ' ُ'), + (0xFE79, 'M', 'ـُ'), + (0xFE7A, '3', ' ِ'), + (0xFE7B, 'M', 'ـِ'), + (0xFE7C, '3', ' ّ'), + (0xFE7D, 'M', 'ـّ'), + (0xFE7E, '3', ' ْ'), + (0xFE7F, 'M', 'ـْ'), + (0xFE80, 'M', 'ء'), + (0xFE81, 'M', 'آ'), + (0xFE83, 'M', 'أ'), + (0xFE85, 'M', 'ؤ'), + (0xFE87, 'M', 'إ'), + (0xFE89, 'M', 'ئ'), + (0xFE8D, 'M', 'ا'), + (0xFE8F, 'M', 'ب'), + (0xFE93, 'M', 'ة'), + (0xFE95, 'M', 'ت'), + (0xFE99, 'M', 'ث'), + (0xFE9D, 'M', 'ج'), + (0xFEA1, 'M', 'ح'), + (0xFEA5, 'M', 'خ'), + (0xFEA9, 'M', 'د'), + (0xFEAB, 'M', 'ذ'), + (0xFEAD, 'M', 'ر'), + (0xFEAF, 'M', 'ز'), + (0xFEB1, 'M', 'س'), + (0xFEB5, 'M', 'ش'), + (0xFEB9, 'M', 'ص'), + (0xFEBD, 'M', 'ض'), + (0xFEC1, 'M', 'ط'), + (0xFEC5, 'M', 'ظ'), + (0xFEC9, 'M', 'ع'), + (0xFECD, 'M', 'غ'), + (0xFED1, 'M', 'ف'), + (0xFED5, 'M', 'ق'), + (0xFED9, 'M', 'ك'), + (0xFEDD, 'M', 'ل'), + (0xFEE1, 'M', 'م'), + (0xFEE5, 'M', 'ن'), + (0xFEE9, 'M', 'ه'), + (0xFEED, 'M', 'و'), + (0xFEEF, 'M', 'ى'), + (0xFEF1, 'M', 'ي'), + (0xFEF5, 'M', 'لآ'), + (0xFEF7, 'M', 'لأ'), + (0xFEF9, 'M', 'لإ'), + (0xFEFB, 'M', 'لا'), + (0xFEFD, 'X'), + (0xFEFF, 'I'), + (0xFF00, 'X'), + (0xFF01, '3', '!'), + (0xFF02, '3', '"'), + (0xFF03, '3', '#'), + (0xFF04, '3', '$'), + (0xFF05, '3', '%'), + (0xFF06, '3', '&'), + (0xFF07, '3', '\''), + (0xFF08, '3', '('), + (0xFF09, '3', ')'), + (0xFF0A, '3', '*'), + (0xFF0B, '3', '+'), + (0xFF0C, '3', ','), + (0xFF0D, 'M', '-'), + (0xFF0E, 'M', '.'), + (0xFF0F, '3', '/'), + (0xFF10, 'M', '0'), + (0xFF11, 'M', '1'), + (0xFF12, 'M', '2'), + (0xFF13, 'M', '3'), + (0xFF14, 'M', '4'), + (0xFF15, 'M', '5'), + (0xFF16, 'M', '6'), + (0xFF17, 'M', '7'), + (0xFF18, 'M', '8'), + (0xFF19, 'M', '9'), + (0xFF1A, '3', ':'), + ] + +def _seg_51() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFF1B, '3', ';'), + (0xFF1C, '3', '<'), + (0xFF1D, '3', '='), + (0xFF1E, '3', '>'), + (0xFF1F, '3', '?'), + (0xFF20, '3', '@'), + (0xFF21, 'M', 'a'), + (0xFF22, 'M', 'b'), + (0xFF23, 'M', 'c'), + (0xFF24, 'M', 'd'), + (0xFF25, 'M', 'e'), + (0xFF26, 'M', 'f'), + (0xFF27, 'M', 'g'), + (0xFF28, 'M', 'h'), + (0xFF29, 'M', 'i'), + (0xFF2A, 'M', 'j'), + (0xFF2B, 'M', 'k'), + (0xFF2C, 'M', 'l'), + (0xFF2D, 'M', 'm'), + (0xFF2E, 'M', 'n'), + (0xFF2F, 'M', 'o'), + (0xFF30, 'M', 'p'), + (0xFF31, 'M', 'q'), + (0xFF32, 'M', 'r'), + (0xFF33, 'M', 's'), + (0xFF34, 'M', 't'), + (0xFF35, 'M', 'u'), + (0xFF36, 'M', 'v'), + (0xFF37, 'M', 'w'), + (0xFF38, 'M', 'x'), + (0xFF39, 'M', 'y'), + (0xFF3A, 'M', 'z'), + (0xFF3B, '3', '['), + (0xFF3C, '3', '\\'), + (0xFF3D, '3', ']'), + (0xFF3E, '3', '^'), + (0xFF3F, '3', '_'), + (0xFF40, '3', '`'), + (0xFF41, 'M', 'a'), + (0xFF42, 'M', 'b'), + (0xFF43, 'M', 'c'), + (0xFF44, 'M', 'd'), + (0xFF45, 'M', 'e'), + (0xFF46, 'M', 'f'), + (0xFF47, 'M', 'g'), + (0xFF48, 'M', 'h'), + (0xFF49, 'M', 'i'), + (0xFF4A, 'M', 'j'), + (0xFF4B, 'M', 'k'), + (0xFF4C, 'M', 'l'), + (0xFF4D, 'M', 'm'), + (0xFF4E, 'M', 'n'), + (0xFF4F, 'M', 'o'), + (0xFF50, 'M', 'p'), + (0xFF51, 'M', 'q'), + (0xFF52, 'M', 'r'), + (0xFF53, 'M', 's'), + (0xFF54, 'M', 't'), + (0xFF55, 'M', 'u'), + (0xFF56, 'M', 'v'), + (0xFF57, 'M', 'w'), + (0xFF58, 'M', 'x'), + (0xFF59, 'M', 'y'), + (0xFF5A, 'M', 'z'), + (0xFF5B, '3', '{'), + (0xFF5C, '3', '|'), + (0xFF5D, '3', '}'), + (0xFF5E, '3', '~'), + (0xFF5F, 'M', '⦅'), + (0xFF60, 'M', '⦆'), + (0xFF61, 'M', '.'), + (0xFF62, 'M', '「'), + (0xFF63, 'M', '」'), + (0xFF64, 'M', '、'), + (0xFF65, 'M', '・'), + (0xFF66, 'M', 'ヲ'), + (0xFF67, 'M', 'ァ'), + (0xFF68, 'M', 'ィ'), + (0xFF69, 'M', 'ゥ'), + (0xFF6A, 'M', 'ェ'), + (0xFF6B, 'M', 'ォ'), + (0xFF6C, 'M', 'ャ'), + (0xFF6D, 'M', 'ュ'), + (0xFF6E, 'M', 'ョ'), + (0xFF6F, 'M', 'ッ'), + (0xFF70, 'M', 'ー'), + (0xFF71, 'M', 'ア'), + (0xFF72, 'M', 'イ'), + (0xFF73, 'M', 'ウ'), + (0xFF74, 'M', 'エ'), + (0xFF75, 'M', 'オ'), + (0xFF76, 'M', 'カ'), + (0xFF77, 'M', 'キ'), + (0xFF78, 'M', 'ク'), + (0xFF79, 'M', 'ケ'), + (0xFF7A, 'M', 'コ'), + (0xFF7B, 'M', 'サ'), + (0xFF7C, 'M', 'シ'), + (0xFF7D, 'M', 'ス'), + (0xFF7E, 'M', 'セ'), + ] + +def _seg_52() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFF7F, 'M', 'ソ'), + (0xFF80, 'M', 'タ'), + (0xFF81, 'M', 'チ'), + (0xFF82, 'M', 'ツ'), + (0xFF83, 'M', 'テ'), + (0xFF84, 'M', 'ト'), + (0xFF85, 'M', 'ナ'), + (0xFF86, 'M', 'ニ'), + (0xFF87, 'M', 'ヌ'), + (0xFF88, 'M', 'ネ'), + (0xFF89, 'M', 'ノ'), + (0xFF8A, 'M', 'ハ'), + (0xFF8B, 'M', 'ヒ'), + (0xFF8C, 'M', 'フ'), + (0xFF8D, 'M', 'ヘ'), + (0xFF8E, 'M', 'ホ'), + (0xFF8F, 'M', 'マ'), + (0xFF90, 'M', 'ミ'), + (0xFF91, 'M', 'ム'), + (0xFF92, 'M', 'メ'), + (0xFF93, 'M', 'モ'), + (0xFF94, 'M', 'ヤ'), + (0xFF95, 'M', 'ユ'), + (0xFF96, 'M', 'ヨ'), + (0xFF97, 'M', 'ラ'), + (0xFF98, 'M', 'リ'), + (0xFF99, 'M', 'ル'), + (0xFF9A, 'M', 'レ'), + (0xFF9B, 'M', 'ロ'), + (0xFF9C, 'M', 'ワ'), + (0xFF9D, 'M', 'ン'), + (0xFF9E, 'M', '゙'), + (0xFF9F, 'M', '゚'), + (0xFFA0, 'X'), + (0xFFA1, 'M', 'ᄀ'), + (0xFFA2, 'M', 'ᄁ'), + (0xFFA3, 'M', 'ᆪ'), + (0xFFA4, 'M', 'ᄂ'), + (0xFFA5, 'M', 'ᆬ'), + (0xFFA6, 'M', 'ᆭ'), + (0xFFA7, 'M', 'ᄃ'), + (0xFFA8, 'M', 'ᄄ'), + (0xFFA9, 'M', 'ᄅ'), + (0xFFAA, 'M', 'ᆰ'), + (0xFFAB, 'M', 'ᆱ'), + (0xFFAC, 'M', 'ᆲ'), + (0xFFAD, 'M', 'ᆳ'), + (0xFFAE, 'M', 'ᆴ'), + (0xFFAF, 'M', 'ᆵ'), + (0xFFB0, 'M', 'ᄚ'), + (0xFFB1, 'M', 'ᄆ'), + (0xFFB2, 'M', 'ᄇ'), + (0xFFB3, 'M', 'ᄈ'), + (0xFFB4, 'M', 'ᄡ'), + (0xFFB5, 'M', 'ᄉ'), + (0xFFB6, 'M', 'ᄊ'), + (0xFFB7, 'M', 'ᄋ'), + (0xFFB8, 'M', 'ᄌ'), + (0xFFB9, 'M', 'ᄍ'), + (0xFFBA, 'M', 'ᄎ'), + (0xFFBB, 'M', 'ᄏ'), + (0xFFBC, 'M', 'ᄐ'), + (0xFFBD, 'M', 'ᄑ'), + (0xFFBE, 'M', 'ᄒ'), + (0xFFBF, 'X'), + (0xFFC2, 'M', 'ᅡ'), + (0xFFC3, 'M', 'ᅢ'), + (0xFFC4, 'M', 'ᅣ'), + (0xFFC5, 'M', 'ᅤ'), + (0xFFC6, 'M', 'ᅥ'), + (0xFFC7, 'M', 'ᅦ'), + (0xFFC8, 'X'), + (0xFFCA, 'M', 'ᅧ'), + (0xFFCB, 'M', 'ᅨ'), + (0xFFCC, 'M', 'ᅩ'), + (0xFFCD, 'M', 'ᅪ'), + (0xFFCE, 'M', 'ᅫ'), + (0xFFCF, 'M', 'ᅬ'), + (0xFFD0, 'X'), + (0xFFD2, 'M', 'ᅭ'), + (0xFFD3, 'M', 'ᅮ'), + (0xFFD4, 'M', 'ᅯ'), + (0xFFD5, 'M', 'ᅰ'), + (0xFFD6, 'M', 'ᅱ'), + (0xFFD7, 'M', 'ᅲ'), + (0xFFD8, 'X'), + (0xFFDA, 'M', 'ᅳ'), + (0xFFDB, 'M', 'ᅴ'), + (0xFFDC, 'M', 'ᅵ'), + (0xFFDD, 'X'), + (0xFFE0, 'M', '¢'), + (0xFFE1, 'M', '£'), + (0xFFE2, 'M', '¬'), + (0xFFE3, '3', ' ̄'), + (0xFFE4, 'M', '¦'), + (0xFFE5, 'M', '¥'), + (0xFFE6, 'M', '₩'), + (0xFFE7, 'X'), + (0xFFE8, 'M', '│'), + (0xFFE9, 'M', '←'), + ] + +def _seg_53() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFFEA, 'M', '↑'), + (0xFFEB, 'M', '→'), + (0xFFEC, 'M', '↓'), + (0xFFED, 'M', '■'), + (0xFFEE, 'M', '○'), + (0xFFEF, 'X'), + (0x10000, 'V'), + (0x1000C, 'X'), + (0x1000D, 'V'), + (0x10027, 'X'), + (0x10028, 'V'), + (0x1003B, 'X'), + (0x1003C, 'V'), + (0x1003E, 'X'), + (0x1003F, 'V'), + (0x1004E, 'X'), + (0x10050, 'V'), + (0x1005E, 'X'), + (0x10080, 'V'), + (0x100FB, 'X'), + (0x10100, 'V'), + (0x10103, 'X'), + (0x10107, 'V'), + (0x10134, 'X'), + (0x10137, 'V'), + (0x1018F, 'X'), + (0x10190, 'V'), + (0x1019D, 'X'), + (0x101A0, 'V'), + (0x101A1, 'X'), + (0x101D0, 'V'), + (0x101FE, 'X'), + (0x10280, 'V'), + (0x1029D, 'X'), + (0x102A0, 'V'), + (0x102D1, 'X'), + (0x102E0, 'V'), + (0x102FC, 'X'), + (0x10300, 'V'), + (0x10324, 'X'), + (0x1032D, 'V'), + (0x1034B, 'X'), + (0x10350, 'V'), + (0x1037B, 'X'), + (0x10380, 'V'), + (0x1039E, 'X'), + (0x1039F, 'V'), + (0x103C4, 'X'), + (0x103C8, 'V'), + (0x103D6, 'X'), + (0x10400, 'M', '𐐨'), + (0x10401, 'M', '𐐩'), + (0x10402, 'M', '𐐪'), + (0x10403, 'M', '𐐫'), + (0x10404, 'M', '𐐬'), + (0x10405, 'M', '𐐭'), + (0x10406, 'M', '𐐮'), + (0x10407, 'M', '𐐯'), + (0x10408, 'M', '𐐰'), + (0x10409, 'M', '𐐱'), + (0x1040A, 'M', '𐐲'), + (0x1040B, 'M', '𐐳'), + (0x1040C, 'M', '𐐴'), + (0x1040D, 'M', '𐐵'), + (0x1040E, 'M', '𐐶'), + (0x1040F, 'M', '𐐷'), + (0x10410, 'M', '𐐸'), + (0x10411, 'M', '𐐹'), + (0x10412, 'M', '𐐺'), + (0x10413, 'M', '𐐻'), + (0x10414, 'M', '𐐼'), + (0x10415, 'M', '𐐽'), + (0x10416, 'M', '𐐾'), + (0x10417, 'M', '𐐿'), + (0x10418, 'M', '𐑀'), + (0x10419, 'M', '𐑁'), + (0x1041A, 'M', '𐑂'), + (0x1041B, 'M', '𐑃'), + (0x1041C, 'M', '𐑄'), + (0x1041D, 'M', '𐑅'), + (0x1041E, 'M', '𐑆'), + (0x1041F, 'M', '𐑇'), + (0x10420, 'M', '𐑈'), + (0x10421, 'M', '𐑉'), + (0x10422, 'M', '𐑊'), + (0x10423, 'M', '𐑋'), + (0x10424, 'M', '𐑌'), + (0x10425, 'M', '𐑍'), + (0x10426, 'M', '𐑎'), + (0x10427, 'M', '𐑏'), + (0x10428, 'V'), + (0x1049E, 'X'), + (0x104A0, 'V'), + (0x104AA, 'X'), + (0x104B0, 'M', '𐓘'), + (0x104B1, 'M', '𐓙'), + (0x104B2, 'M', '𐓚'), + (0x104B3, 'M', '𐓛'), + (0x104B4, 'M', '𐓜'), + (0x104B5, 'M', '𐓝'), + ] + +def _seg_54() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x104B6, 'M', '𐓞'), + (0x104B7, 'M', '𐓟'), + (0x104B8, 'M', '𐓠'), + (0x104B9, 'M', '𐓡'), + (0x104BA, 'M', '𐓢'), + (0x104BB, 'M', '𐓣'), + (0x104BC, 'M', '𐓤'), + (0x104BD, 'M', '𐓥'), + (0x104BE, 'M', '𐓦'), + (0x104BF, 'M', '𐓧'), + (0x104C0, 'M', '𐓨'), + (0x104C1, 'M', '𐓩'), + (0x104C2, 'M', '𐓪'), + (0x104C3, 'M', '𐓫'), + (0x104C4, 'M', '𐓬'), + (0x104C5, 'M', '𐓭'), + (0x104C6, 'M', '𐓮'), + (0x104C7, 'M', '𐓯'), + (0x104C8, 'M', '𐓰'), + (0x104C9, 'M', '𐓱'), + (0x104CA, 'M', '𐓲'), + (0x104CB, 'M', '𐓳'), + (0x104CC, 'M', '𐓴'), + (0x104CD, 'M', '𐓵'), + (0x104CE, 'M', '𐓶'), + (0x104CF, 'M', '𐓷'), + (0x104D0, 'M', '𐓸'), + (0x104D1, 'M', '𐓹'), + (0x104D2, 'M', '𐓺'), + (0x104D3, 'M', '𐓻'), + (0x104D4, 'X'), + (0x104D8, 'V'), + (0x104FC, 'X'), + (0x10500, 'V'), + (0x10528, 'X'), + (0x10530, 'V'), + (0x10564, 'X'), + (0x1056F, 'V'), + (0x10570, 'M', '𐖗'), + (0x10571, 'M', '𐖘'), + (0x10572, 'M', '𐖙'), + (0x10573, 'M', '𐖚'), + (0x10574, 'M', '𐖛'), + (0x10575, 'M', '𐖜'), + (0x10576, 'M', '𐖝'), + (0x10577, 'M', '𐖞'), + (0x10578, 'M', '𐖟'), + (0x10579, 'M', '𐖠'), + (0x1057A, 'M', '𐖡'), + (0x1057B, 'X'), + (0x1057C, 'M', '𐖣'), + (0x1057D, 'M', '𐖤'), + (0x1057E, 'M', '𐖥'), + (0x1057F, 'M', '𐖦'), + (0x10580, 'M', '𐖧'), + (0x10581, 'M', '𐖨'), + (0x10582, 'M', '𐖩'), + (0x10583, 'M', '𐖪'), + (0x10584, 'M', '𐖫'), + (0x10585, 'M', '𐖬'), + (0x10586, 'M', '𐖭'), + (0x10587, 'M', '𐖮'), + (0x10588, 'M', '𐖯'), + (0x10589, 'M', '𐖰'), + (0x1058A, 'M', '𐖱'), + (0x1058B, 'X'), + (0x1058C, 'M', '𐖳'), + (0x1058D, 'M', '𐖴'), + (0x1058E, 'M', '𐖵'), + (0x1058F, 'M', '𐖶'), + (0x10590, 'M', '𐖷'), + (0x10591, 'M', '𐖸'), + (0x10592, 'M', '𐖹'), + (0x10593, 'X'), + (0x10594, 'M', '𐖻'), + (0x10595, 'M', '𐖼'), + (0x10596, 'X'), + (0x10597, 'V'), + (0x105A2, 'X'), + (0x105A3, 'V'), + (0x105B2, 'X'), + (0x105B3, 'V'), + (0x105BA, 'X'), + (0x105BB, 'V'), + (0x105BD, 'X'), + (0x10600, 'V'), + (0x10737, 'X'), + (0x10740, 'V'), + (0x10756, 'X'), + (0x10760, 'V'), + (0x10768, 'X'), + (0x10780, 'V'), + (0x10781, 'M', 'ː'), + (0x10782, 'M', 'ˑ'), + (0x10783, 'M', 'æ'), + (0x10784, 'M', 'ʙ'), + (0x10785, 'M', 'ɓ'), + (0x10786, 'X'), + (0x10787, 'M', 'ʣ'), + (0x10788, 'M', 'ꭦ'), + ] + +def _seg_55() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x10789, 'M', 'ʥ'), + (0x1078A, 'M', 'ʤ'), + (0x1078B, 'M', 'ɖ'), + (0x1078C, 'M', 'ɗ'), + (0x1078D, 'M', 'ᶑ'), + (0x1078E, 'M', 'ɘ'), + (0x1078F, 'M', 'ɞ'), + (0x10790, 'M', 'ʩ'), + (0x10791, 'M', 'ɤ'), + (0x10792, 'M', 'ɢ'), + (0x10793, 'M', 'ɠ'), + (0x10794, 'M', 'ʛ'), + (0x10795, 'M', 'ħ'), + (0x10796, 'M', 'ʜ'), + (0x10797, 'M', 'ɧ'), + (0x10798, 'M', 'ʄ'), + (0x10799, 'M', 'ʪ'), + (0x1079A, 'M', 'ʫ'), + (0x1079B, 'M', 'ɬ'), + (0x1079C, 'M', '𝼄'), + (0x1079D, 'M', 'ꞎ'), + (0x1079E, 'M', 'ɮ'), + (0x1079F, 'M', '𝼅'), + (0x107A0, 'M', 'ʎ'), + (0x107A1, 'M', '𝼆'), + (0x107A2, 'M', 'ø'), + (0x107A3, 'M', 'ɶ'), + (0x107A4, 'M', 'ɷ'), + (0x107A5, 'M', 'q'), + (0x107A6, 'M', 'ɺ'), + (0x107A7, 'M', '𝼈'), + (0x107A8, 'M', 'ɽ'), + (0x107A9, 'M', 'ɾ'), + (0x107AA, 'M', 'ʀ'), + (0x107AB, 'M', 'ʨ'), + (0x107AC, 'M', 'ʦ'), + (0x107AD, 'M', 'ꭧ'), + (0x107AE, 'M', 'ʧ'), + (0x107AF, 'M', 'ʈ'), + (0x107B0, 'M', 'ⱱ'), + (0x107B1, 'X'), + (0x107B2, 'M', 'ʏ'), + (0x107B3, 'M', 'ʡ'), + (0x107B4, 'M', 'ʢ'), + (0x107B5, 'M', 'ʘ'), + (0x107B6, 'M', 'ǀ'), + (0x107B7, 'M', 'ǁ'), + (0x107B8, 'M', 'ǂ'), + (0x107B9, 'M', '𝼊'), + (0x107BA, 'M', '𝼞'), + (0x107BB, 'X'), + (0x10800, 'V'), + (0x10806, 'X'), + (0x10808, 'V'), + (0x10809, 'X'), + (0x1080A, 'V'), + (0x10836, 'X'), + (0x10837, 'V'), + (0x10839, 'X'), + (0x1083C, 'V'), + (0x1083D, 'X'), + (0x1083F, 'V'), + (0x10856, 'X'), + (0x10857, 'V'), + (0x1089F, 'X'), + (0x108A7, 'V'), + (0x108B0, 'X'), + (0x108E0, 'V'), + (0x108F3, 'X'), + (0x108F4, 'V'), + (0x108F6, 'X'), + (0x108FB, 'V'), + (0x1091C, 'X'), + (0x1091F, 'V'), + (0x1093A, 'X'), + (0x1093F, 'V'), + (0x10940, 'X'), + (0x10980, 'V'), + (0x109B8, 'X'), + (0x109BC, 'V'), + (0x109D0, 'X'), + (0x109D2, 'V'), + (0x10A04, 'X'), + (0x10A05, 'V'), + (0x10A07, 'X'), + (0x10A0C, 'V'), + (0x10A14, 'X'), + (0x10A15, 'V'), + (0x10A18, 'X'), + (0x10A19, 'V'), + (0x10A36, 'X'), + (0x10A38, 'V'), + (0x10A3B, 'X'), + (0x10A3F, 'V'), + (0x10A49, 'X'), + (0x10A50, 'V'), + (0x10A59, 'X'), + (0x10A60, 'V'), + (0x10AA0, 'X'), + (0x10AC0, 'V'), + ] + +def _seg_56() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x10AE7, 'X'), + (0x10AEB, 'V'), + (0x10AF7, 'X'), + (0x10B00, 'V'), + (0x10B36, 'X'), + (0x10B39, 'V'), + (0x10B56, 'X'), + (0x10B58, 'V'), + (0x10B73, 'X'), + (0x10B78, 'V'), + (0x10B92, 'X'), + (0x10B99, 'V'), + (0x10B9D, 'X'), + (0x10BA9, 'V'), + (0x10BB0, 'X'), + (0x10C00, 'V'), + (0x10C49, 'X'), + (0x10C80, 'M', '𐳀'), + (0x10C81, 'M', '𐳁'), + (0x10C82, 'M', '𐳂'), + (0x10C83, 'M', '𐳃'), + (0x10C84, 'M', '𐳄'), + (0x10C85, 'M', '𐳅'), + (0x10C86, 'M', '𐳆'), + (0x10C87, 'M', '𐳇'), + (0x10C88, 'M', '𐳈'), + (0x10C89, 'M', '𐳉'), + (0x10C8A, 'M', '𐳊'), + (0x10C8B, 'M', '𐳋'), + (0x10C8C, 'M', '𐳌'), + (0x10C8D, 'M', '𐳍'), + (0x10C8E, 'M', '𐳎'), + (0x10C8F, 'M', '𐳏'), + (0x10C90, 'M', '𐳐'), + (0x10C91, 'M', '𐳑'), + (0x10C92, 'M', '𐳒'), + (0x10C93, 'M', '𐳓'), + (0x10C94, 'M', '𐳔'), + (0x10C95, 'M', '𐳕'), + (0x10C96, 'M', '𐳖'), + (0x10C97, 'M', '𐳗'), + (0x10C98, 'M', '𐳘'), + (0x10C99, 'M', '𐳙'), + (0x10C9A, 'M', '𐳚'), + (0x10C9B, 'M', '𐳛'), + (0x10C9C, 'M', '𐳜'), + (0x10C9D, 'M', '𐳝'), + (0x10C9E, 'M', '𐳞'), + (0x10C9F, 'M', '𐳟'), + (0x10CA0, 'M', '𐳠'), + (0x10CA1, 'M', '𐳡'), + (0x10CA2, 'M', '𐳢'), + (0x10CA3, 'M', '𐳣'), + (0x10CA4, 'M', '𐳤'), + (0x10CA5, 'M', '𐳥'), + (0x10CA6, 'M', '𐳦'), + (0x10CA7, 'M', '𐳧'), + (0x10CA8, 'M', '𐳨'), + (0x10CA9, 'M', '𐳩'), + (0x10CAA, 'M', '𐳪'), + (0x10CAB, 'M', '𐳫'), + (0x10CAC, 'M', '𐳬'), + (0x10CAD, 'M', '𐳭'), + (0x10CAE, 'M', '𐳮'), + (0x10CAF, 'M', '𐳯'), + (0x10CB0, 'M', '𐳰'), + (0x10CB1, 'M', '𐳱'), + (0x10CB2, 'M', '𐳲'), + (0x10CB3, 'X'), + (0x10CC0, 'V'), + (0x10CF3, 'X'), + (0x10CFA, 'V'), + (0x10D28, 'X'), + (0x10D30, 'V'), + (0x10D3A, 'X'), + (0x10E60, 'V'), + (0x10E7F, 'X'), + (0x10E80, 'V'), + (0x10EAA, 'X'), + (0x10EAB, 'V'), + (0x10EAE, 'X'), + (0x10EB0, 'V'), + (0x10EB2, 'X'), + (0x10EFD, 'V'), + (0x10F28, 'X'), + (0x10F30, 'V'), + (0x10F5A, 'X'), + (0x10F70, 'V'), + (0x10F8A, 'X'), + (0x10FB0, 'V'), + (0x10FCC, 'X'), + (0x10FE0, 'V'), + (0x10FF7, 'X'), + (0x11000, 'V'), + (0x1104E, 'X'), + (0x11052, 'V'), + (0x11076, 'X'), + (0x1107F, 'V'), + (0x110BD, 'X'), + (0x110BE, 'V'), + ] + +def _seg_57() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x110C3, 'X'), + (0x110D0, 'V'), + (0x110E9, 'X'), + (0x110F0, 'V'), + (0x110FA, 'X'), + (0x11100, 'V'), + (0x11135, 'X'), + (0x11136, 'V'), + (0x11148, 'X'), + (0x11150, 'V'), + (0x11177, 'X'), + (0x11180, 'V'), + (0x111E0, 'X'), + (0x111E1, 'V'), + (0x111F5, 'X'), + (0x11200, 'V'), + (0x11212, 'X'), + (0x11213, 'V'), + (0x11242, 'X'), + (0x11280, 'V'), + (0x11287, 'X'), + (0x11288, 'V'), + (0x11289, 'X'), + (0x1128A, 'V'), + (0x1128E, 'X'), + (0x1128F, 'V'), + (0x1129E, 'X'), + (0x1129F, 'V'), + (0x112AA, 'X'), + (0x112B0, 'V'), + (0x112EB, 'X'), + (0x112F0, 'V'), + (0x112FA, 'X'), + (0x11300, 'V'), + (0x11304, 'X'), + (0x11305, 'V'), + (0x1130D, 'X'), + (0x1130F, 'V'), + (0x11311, 'X'), + (0x11313, 'V'), + (0x11329, 'X'), + (0x1132A, 'V'), + (0x11331, 'X'), + (0x11332, 'V'), + (0x11334, 'X'), + (0x11335, 'V'), + (0x1133A, 'X'), + (0x1133B, 'V'), + (0x11345, 'X'), + (0x11347, 'V'), + (0x11349, 'X'), + (0x1134B, 'V'), + (0x1134E, 'X'), + (0x11350, 'V'), + (0x11351, 'X'), + (0x11357, 'V'), + (0x11358, 'X'), + (0x1135D, 'V'), + (0x11364, 'X'), + (0x11366, 'V'), + (0x1136D, 'X'), + (0x11370, 'V'), + (0x11375, 'X'), + (0x11400, 'V'), + (0x1145C, 'X'), + (0x1145D, 'V'), + (0x11462, 'X'), + (0x11480, 'V'), + (0x114C8, 'X'), + (0x114D0, 'V'), + (0x114DA, 'X'), + (0x11580, 'V'), + (0x115B6, 'X'), + (0x115B8, 'V'), + (0x115DE, 'X'), + (0x11600, 'V'), + (0x11645, 'X'), + (0x11650, 'V'), + (0x1165A, 'X'), + (0x11660, 'V'), + (0x1166D, 'X'), + (0x11680, 'V'), + (0x116BA, 'X'), + (0x116C0, 'V'), + (0x116CA, 'X'), + (0x11700, 'V'), + (0x1171B, 'X'), + (0x1171D, 'V'), + (0x1172C, 'X'), + (0x11730, 'V'), + (0x11747, 'X'), + (0x11800, 'V'), + (0x1183C, 'X'), + (0x118A0, 'M', '𑣀'), + (0x118A1, 'M', '𑣁'), + (0x118A2, 'M', '𑣂'), + (0x118A3, 'M', '𑣃'), + (0x118A4, 'M', '𑣄'), + (0x118A5, 'M', '𑣅'), + (0x118A6, 'M', '𑣆'), + ] + +def _seg_58() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x118A7, 'M', '𑣇'), + (0x118A8, 'M', '𑣈'), + (0x118A9, 'M', '𑣉'), + (0x118AA, 'M', '𑣊'), + (0x118AB, 'M', '𑣋'), + (0x118AC, 'M', '𑣌'), + (0x118AD, 'M', '𑣍'), + (0x118AE, 'M', '𑣎'), + (0x118AF, 'M', '𑣏'), + (0x118B0, 'M', '𑣐'), + (0x118B1, 'M', '𑣑'), + (0x118B2, 'M', '𑣒'), + (0x118B3, 'M', '𑣓'), + (0x118B4, 'M', '𑣔'), + (0x118B5, 'M', '𑣕'), + (0x118B6, 'M', '𑣖'), + (0x118B7, 'M', '𑣗'), + (0x118B8, 'M', '𑣘'), + (0x118B9, 'M', '𑣙'), + (0x118BA, 'M', '𑣚'), + (0x118BB, 'M', '𑣛'), + (0x118BC, 'M', '𑣜'), + (0x118BD, 'M', '𑣝'), + (0x118BE, 'M', '𑣞'), + (0x118BF, 'M', '𑣟'), + (0x118C0, 'V'), + (0x118F3, 'X'), + (0x118FF, 'V'), + (0x11907, 'X'), + (0x11909, 'V'), + (0x1190A, 'X'), + (0x1190C, 'V'), + (0x11914, 'X'), + (0x11915, 'V'), + (0x11917, 'X'), + (0x11918, 'V'), + (0x11936, 'X'), + (0x11937, 'V'), + (0x11939, 'X'), + (0x1193B, 'V'), + (0x11947, 'X'), + (0x11950, 'V'), + (0x1195A, 'X'), + (0x119A0, 'V'), + (0x119A8, 'X'), + (0x119AA, 'V'), + (0x119D8, 'X'), + (0x119DA, 'V'), + (0x119E5, 'X'), + (0x11A00, 'V'), + (0x11A48, 'X'), + (0x11A50, 'V'), + (0x11AA3, 'X'), + (0x11AB0, 'V'), + (0x11AF9, 'X'), + (0x11B00, 'V'), + (0x11B0A, 'X'), + (0x11C00, 'V'), + (0x11C09, 'X'), + (0x11C0A, 'V'), + (0x11C37, 'X'), + (0x11C38, 'V'), + (0x11C46, 'X'), + (0x11C50, 'V'), + (0x11C6D, 'X'), + (0x11C70, 'V'), + (0x11C90, 'X'), + (0x11C92, 'V'), + (0x11CA8, 'X'), + (0x11CA9, 'V'), + (0x11CB7, 'X'), + (0x11D00, 'V'), + (0x11D07, 'X'), + (0x11D08, 'V'), + (0x11D0A, 'X'), + (0x11D0B, 'V'), + (0x11D37, 'X'), + (0x11D3A, 'V'), + (0x11D3B, 'X'), + (0x11D3C, 'V'), + (0x11D3E, 'X'), + (0x11D3F, 'V'), + (0x11D48, 'X'), + (0x11D50, 'V'), + (0x11D5A, 'X'), + (0x11D60, 'V'), + (0x11D66, 'X'), + (0x11D67, 'V'), + (0x11D69, 'X'), + (0x11D6A, 'V'), + (0x11D8F, 'X'), + (0x11D90, 'V'), + (0x11D92, 'X'), + (0x11D93, 'V'), + (0x11D99, 'X'), + (0x11DA0, 'V'), + (0x11DAA, 'X'), + (0x11EE0, 'V'), + (0x11EF9, 'X'), + (0x11F00, 'V'), + ] + +def _seg_59() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x11F11, 'X'), + (0x11F12, 'V'), + (0x11F3B, 'X'), + (0x11F3E, 'V'), + (0x11F5A, 'X'), + (0x11FB0, 'V'), + (0x11FB1, 'X'), + (0x11FC0, 'V'), + (0x11FF2, 'X'), + (0x11FFF, 'V'), + (0x1239A, 'X'), + (0x12400, 'V'), + (0x1246F, 'X'), + (0x12470, 'V'), + (0x12475, 'X'), + (0x12480, 'V'), + (0x12544, 'X'), + (0x12F90, 'V'), + (0x12FF3, 'X'), + (0x13000, 'V'), + (0x13430, 'X'), + (0x13440, 'V'), + (0x13456, 'X'), + (0x14400, 'V'), + (0x14647, 'X'), + (0x16800, 'V'), + (0x16A39, 'X'), + (0x16A40, 'V'), + (0x16A5F, 'X'), + (0x16A60, 'V'), + (0x16A6A, 'X'), + (0x16A6E, 'V'), + (0x16ABF, 'X'), + (0x16AC0, 'V'), + (0x16ACA, 'X'), + (0x16AD0, 'V'), + (0x16AEE, 'X'), + (0x16AF0, 'V'), + (0x16AF6, 'X'), + (0x16B00, 'V'), + (0x16B46, 'X'), + (0x16B50, 'V'), + (0x16B5A, 'X'), + (0x16B5B, 'V'), + (0x16B62, 'X'), + (0x16B63, 'V'), + (0x16B78, 'X'), + (0x16B7D, 'V'), + (0x16B90, 'X'), + (0x16E40, 'M', '𖹠'), + (0x16E41, 'M', '𖹡'), + (0x16E42, 'M', '𖹢'), + (0x16E43, 'M', '𖹣'), + (0x16E44, 'M', '𖹤'), + (0x16E45, 'M', '𖹥'), + (0x16E46, 'M', '𖹦'), + (0x16E47, 'M', '𖹧'), + (0x16E48, 'M', '𖹨'), + (0x16E49, 'M', '𖹩'), + (0x16E4A, 'M', '𖹪'), + (0x16E4B, 'M', '𖹫'), + (0x16E4C, 'M', '𖹬'), + (0x16E4D, 'M', '𖹭'), + (0x16E4E, 'M', '𖹮'), + (0x16E4F, 'M', '𖹯'), + (0x16E50, 'M', '𖹰'), + (0x16E51, 'M', '𖹱'), + (0x16E52, 'M', '𖹲'), + (0x16E53, 'M', '𖹳'), + (0x16E54, 'M', '𖹴'), + (0x16E55, 'M', '𖹵'), + (0x16E56, 'M', '𖹶'), + (0x16E57, 'M', '𖹷'), + (0x16E58, 'M', '𖹸'), + (0x16E59, 'M', '𖹹'), + (0x16E5A, 'M', '𖹺'), + (0x16E5B, 'M', '𖹻'), + (0x16E5C, 'M', '𖹼'), + (0x16E5D, 'M', '𖹽'), + (0x16E5E, 'M', '𖹾'), + (0x16E5F, 'M', '𖹿'), + (0x16E60, 'V'), + (0x16E9B, 'X'), + (0x16F00, 'V'), + (0x16F4B, 'X'), + (0x16F4F, 'V'), + (0x16F88, 'X'), + (0x16F8F, 'V'), + (0x16FA0, 'X'), + (0x16FE0, 'V'), + (0x16FE5, 'X'), + (0x16FF0, 'V'), + (0x16FF2, 'X'), + (0x17000, 'V'), + (0x187F8, 'X'), + (0x18800, 'V'), + (0x18CD6, 'X'), + (0x18D00, 'V'), + (0x18D09, 'X'), + (0x1AFF0, 'V'), + ] + +def _seg_60() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1AFF4, 'X'), + (0x1AFF5, 'V'), + (0x1AFFC, 'X'), + (0x1AFFD, 'V'), + (0x1AFFF, 'X'), + (0x1B000, 'V'), + (0x1B123, 'X'), + (0x1B132, 'V'), + (0x1B133, 'X'), + (0x1B150, 'V'), + (0x1B153, 'X'), + (0x1B155, 'V'), + (0x1B156, 'X'), + (0x1B164, 'V'), + (0x1B168, 'X'), + (0x1B170, 'V'), + (0x1B2FC, 'X'), + (0x1BC00, 'V'), + (0x1BC6B, 'X'), + (0x1BC70, 'V'), + (0x1BC7D, 'X'), + (0x1BC80, 'V'), + (0x1BC89, 'X'), + (0x1BC90, 'V'), + (0x1BC9A, 'X'), + (0x1BC9C, 'V'), + (0x1BCA0, 'I'), + (0x1BCA4, 'X'), + (0x1CF00, 'V'), + (0x1CF2E, 'X'), + (0x1CF30, 'V'), + (0x1CF47, 'X'), + (0x1CF50, 'V'), + (0x1CFC4, 'X'), + (0x1D000, 'V'), + (0x1D0F6, 'X'), + (0x1D100, 'V'), + (0x1D127, 'X'), + (0x1D129, 'V'), + (0x1D15E, 'M', '𝅗𝅥'), + (0x1D15F, 'M', '𝅘𝅥'), + (0x1D160, 'M', '𝅘𝅥𝅮'), + (0x1D161, 'M', '𝅘𝅥𝅯'), + (0x1D162, 'M', '𝅘𝅥𝅰'), + (0x1D163, 'M', '𝅘𝅥𝅱'), + (0x1D164, 'M', '𝅘𝅥𝅲'), + (0x1D165, 'V'), + (0x1D173, 'X'), + (0x1D17B, 'V'), + (0x1D1BB, 'M', '𝆹𝅥'), + (0x1D1BC, 'M', '𝆺𝅥'), + (0x1D1BD, 'M', '𝆹𝅥𝅮'), + (0x1D1BE, 'M', '𝆺𝅥𝅮'), + (0x1D1BF, 'M', '𝆹𝅥𝅯'), + (0x1D1C0, 'M', '𝆺𝅥𝅯'), + (0x1D1C1, 'V'), + (0x1D1EB, 'X'), + (0x1D200, 'V'), + (0x1D246, 'X'), + (0x1D2C0, 'V'), + (0x1D2D4, 'X'), + (0x1D2E0, 'V'), + (0x1D2F4, 'X'), + (0x1D300, 'V'), + (0x1D357, 'X'), + (0x1D360, 'V'), + (0x1D379, 'X'), + (0x1D400, 'M', 'a'), + (0x1D401, 'M', 'b'), + (0x1D402, 'M', 'c'), + (0x1D403, 'M', 'd'), + (0x1D404, 'M', 'e'), + (0x1D405, 'M', 'f'), + (0x1D406, 'M', 'g'), + (0x1D407, 'M', 'h'), + (0x1D408, 'M', 'i'), + (0x1D409, 'M', 'j'), + (0x1D40A, 'M', 'k'), + (0x1D40B, 'M', 'l'), + (0x1D40C, 'M', 'm'), + (0x1D40D, 'M', 'n'), + (0x1D40E, 'M', 'o'), + (0x1D40F, 'M', 'p'), + (0x1D410, 'M', 'q'), + (0x1D411, 'M', 'r'), + (0x1D412, 'M', 's'), + (0x1D413, 'M', 't'), + (0x1D414, 'M', 'u'), + (0x1D415, 'M', 'v'), + (0x1D416, 'M', 'w'), + (0x1D417, 'M', 'x'), + (0x1D418, 'M', 'y'), + (0x1D419, 'M', 'z'), + (0x1D41A, 'M', 'a'), + (0x1D41B, 'M', 'b'), + (0x1D41C, 'M', 'c'), + (0x1D41D, 'M', 'd'), + (0x1D41E, 'M', 'e'), + (0x1D41F, 'M', 'f'), + (0x1D420, 'M', 'g'), + ] + +def _seg_61() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D421, 'M', 'h'), + (0x1D422, 'M', 'i'), + (0x1D423, 'M', 'j'), + (0x1D424, 'M', 'k'), + (0x1D425, 'M', 'l'), + (0x1D426, 'M', 'm'), + (0x1D427, 'M', 'n'), + (0x1D428, 'M', 'o'), + (0x1D429, 'M', 'p'), + (0x1D42A, 'M', 'q'), + (0x1D42B, 'M', 'r'), + (0x1D42C, 'M', 's'), + (0x1D42D, 'M', 't'), + (0x1D42E, 'M', 'u'), + (0x1D42F, 'M', 'v'), + (0x1D430, 'M', 'w'), + (0x1D431, 'M', 'x'), + (0x1D432, 'M', 'y'), + (0x1D433, 'M', 'z'), + (0x1D434, 'M', 'a'), + (0x1D435, 'M', 'b'), + (0x1D436, 'M', 'c'), + (0x1D437, 'M', 'd'), + (0x1D438, 'M', 'e'), + (0x1D439, 'M', 'f'), + (0x1D43A, 'M', 'g'), + (0x1D43B, 'M', 'h'), + (0x1D43C, 'M', 'i'), + (0x1D43D, 'M', 'j'), + (0x1D43E, 'M', 'k'), + (0x1D43F, 'M', 'l'), + (0x1D440, 'M', 'm'), + (0x1D441, 'M', 'n'), + (0x1D442, 'M', 'o'), + (0x1D443, 'M', 'p'), + (0x1D444, 'M', 'q'), + (0x1D445, 'M', 'r'), + (0x1D446, 'M', 's'), + (0x1D447, 'M', 't'), + (0x1D448, 'M', 'u'), + (0x1D449, 'M', 'v'), + (0x1D44A, 'M', 'w'), + (0x1D44B, 'M', 'x'), + (0x1D44C, 'M', 'y'), + (0x1D44D, 'M', 'z'), + (0x1D44E, 'M', 'a'), + (0x1D44F, 'M', 'b'), + (0x1D450, 'M', 'c'), + (0x1D451, 'M', 'd'), + (0x1D452, 'M', 'e'), + (0x1D453, 'M', 'f'), + (0x1D454, 'M', 'g'), + (0x1D455, 'X'), + (0x1D456, 'M', 'i'), + (0x1D457, 'M', 'j'), + (0x1D458, 'M', 'k'), + (0x1D459, 'M', 'l'), + (0x1D45A, 'M', 'm'), + (0x1D45B, 'M', 'n'), + (0x1D45C, 'M', 'o'), + (0x1D45D, 'M', 'p'), + (0x1D45E, 'M', 'q'), + (0x1D45F, 'M', 'r'), + (0x1D460, 'M', 's'), + (0x1D461, 'M', 't'), + (0x1D462, 'M', 'u'), + (0x1D463, 'M', 'v'), + (0x1D464, 'M', 'w'), + (0x1D465, 'M', 'x'), + (0x1D466, 'M', 'y'), + (0x1D467, 'M', 'z'), + (0x1D468, 'M', 'a'), + (0x1D469, 'M', 'b'), + (0x1D46A, 'M', 'c'), + (0x1D46B, 'M', 'd'), + (0x1D46C, 'M', 'e'), + (0x1D46D, 'M', 'f'), + (0x1D46E, 'M', 'g'), + (0x1D46F, 'M', 'h'), + (0x1D470, 'M', 'i'), + (0x1D471, 'M', 'j'), + (0x1D472, 'M', 'k'), + (0x1D473, 'M', 'l'), + (0x1D474, 'M', 'm'), + (0x1D475, 'M', 'n'), + (0x1D476, 'M', 'o'), + (0x1D477, 'M', 'p'), + (0x1D478, 'M', 'q'), + (0x1D479, 'M', 'r'), + (0x1D47A, 'M', 's'), + (0x1D47B, 'M', 't'), + (0x1D47C, 'M', 'u'), + (0x1D47D, 'M', 'v'), + (0x1D47E, 'M', 'w'), + (0x1D47F, 'M', 'x'), + (0x1D480, 'M', 'y'), + (0x1D481, 'M', 'z'), + (0x1D482, 'M', 'a'), + (0x1D483, 'M', 'b'), + (0x1D484, 'M', 'c'), + ] + +def _seg_62() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D485, 'M', 'd'), + (0x1D486, 'M', 'e'), + (0x1D487, 'M', 'f'), + (0x1D488, 'M', 'g'), + (0x1D489, 'M', 'h'), + (0x1D48A, 'M', 'i'), + (0x1D48B, 'M', 'j'), + (0x1D48C, 'M', 'k'), + (0x1D48D, 'M', 'l'), + (0x1D48E, 'M', 'm'), + (0x1D48F, 'M', 'n'), + (0x1D490, 'M', 'o'), + (0x1D491, 'M', 'p'), + (0x1D492, 'M', 'q'), + (0x1D493, 'M', 'r'), + (0x1D494, 'M', 's'), + (0x1D495, 'M', 't'), + (0x1D496, 'M', 'u'), + (0x1D497, 'M', 'v'), + (0x1D498, 'M', 'w'), + (0x1D499, 'M', 'x'), + (0x1D49A, 'M', 'y'), + (0x1D49B, 'M', 'z'), + (0x1D49C, 'M', 'a'), + (0x1D49D, 'X'), + (0x1D49E, 'M', 'c'), + (0x1D49F, 'M', 'd'), + (0x1D4A0, 'X'), + (0x1D4A2, 'M', 'g'), + (0x1D4A3, 'X'), + (0x1D4A5, 'M', 'j'), + (0x1D4A6, 'M', 'k'), + (0x1D4A7, 'X'), + (0x1D4A9, 'M', 'n'), + (0x1D4AA, 'M', 'o'), + (0x1D4AB, 'M', 'p'), + (0x1D4AC, 'M', 'q'), + (0x1D4AD, 'X'), + (0x1D4AE, 'M', 's'), + (0x1D4AF, 'M', 't'), + (0x1D4B0, 'M', 'u'), + (0x1D4B1, 'M', 'v'), + (0x1D4B2, 'M', 'w'), + (0x1D4B3, 'M', 'x'), + (0x1D4B4, 'M', 'y'), + (0x1D4B5, 'M', 'z'), + (0x1D4B6, 'M', 'a'), + (0x1D4B7, 'M', 'b'), + (0x1D4B8, 'M', 'c'), + (0x1D4B9, 'M', 'd'), + (0x1D4BA, 'X'), + (0x1D4BB, 'M', 'f'), + (0x1D4BC, 'X'), + (0x1D4BD, 'M', 'h'), + (0x1D4BE, 'M', 'i'), + (0x1D4BF, 'M', 'j'), + (0x1D4C0, 'M', 'k'), + (0x1D4C1, 'M', 'l'), + (0x1D4C2, 'M', 'm'), + (0x1D4C3, 'M', 'n'), + (0x1D4C4, 'X'), + (0x1D4C5, 'M', 'p'), + (0x1D4C6, 'M', 'q'), + (0x1D4C7, 'M', 'r'), + (0x1D4C8, 'M', 's'), + (0x1D4C9, 'M', 't'), + (0x1D4CA, 'M', 'u'), + (0x1D4CB, 'M', 'v'), + (0x1D4CC, 'M', 'w'), + (0x1D4CD, 'M', 'x'), + (0x1D4CE, 'M', 'y'), + (0x1D4CF, 'M', 'z'), + (0x1D4D0, 'M', 'a'), + (0x1D4D1, 'M', 'b'), + (0x1D4D2, 'M', 'c'), + (0x1D4D3, 'M', 'd'), + (0x1D4D4, 'M', 'e'), + (0x1D4D5, 'M', 'f'), + (0x1D4D6, 'M', 'g'), + (0x1D4D7, 'M', 'h'), + (0x1D4D8, 'M', 'i'), + (0x1D4D9, 'M', 'j'), + (0x1D4DA, 'M', 'k'), + (0x1D4DB, 'M', 'l'), + (0x1D4DC, 'M', 'm'), + (0x1D4DD, 'M', 'n'), + (0x1D4DE, 'M', 'o'), + (0x1D4DF, 'M', 'p'), + (0x1D4E0, 'M', 'q'), + (0x1D4E1, 'M', 'r'), + (0x1D4E2, 'M', 's'), + (0x1D4E3, 'M', 't'), + (0x1D4E4, 'M', 'u'), + (0x1D4E5, 'M', 'v'), + (0x1D4E6, 'M', 'w'), + (0x1D4E7, 'M', 'x'), + (0x1D4E8, 'M', 'y'), + (0x1D4E9, 'M', 'z'), + (0x1D4EA, 'M', 'a'), + (0x1D4EB, 'M', 'b'), + ] + +def _seg_63() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D4EC, 'M', 'c'), + (0x1D4ED, 'M', 'd'), + (0x1D4EE, 'M', 'e'), + (0x1D4EF, 'M', 'f'), + (0x1D4F0, 'M', 'g'), + (0x1D4F1, 'M', 'h'), + (0x1D4F2, 'M', 'i'), + (0x1D4F3, 'M', 'j'), + (0x1D4F4, 'M', 'k'), + (0x1D4F5, 'M', 'l'), + (0x1D4F6, 'M', 'm'), + (0x1D4F7, 'M', 'n'), + (0x1D4F8, 'M', 'o'), + (0x1D4F9, 'M', 'p'), + (0x1D4FA, 'M', 'q'), + (0x1D4FB, 'M', 'r'), + (0x1D4FC, 'M', 's'), + (0x1D4FD, 'M', 't'), + (0x1D4FE, 'M', 'u'), + (0x1D4FF, 'M', 'v'), + (0x1D500, 'M', 'w'), + (0x1D501, 'M', 'x'), + (0x1D502, 'M', 'y'), + (0x1D503, 'M', 'z'), + (0x1D504, 'M', 'a'), + (0x1D505, 'M', 'b'), + (0x1D506, 'X'), + (0x1D507, 'M', 'd'), + (0x1D508, 'M', 'e'), + (0x1D509, 'M', 'f'), + (0x1D50A, 'M', 'g'), + (0x1D50B, 'X'), + (0x1D50D, 'M', 'j'), + (0x1D50E, 'M', 'k'), + (0x1D50F, 'M', 'l'), + (0x1D510, 'M', 'm'), + (0x1D511, 'M', 'n'), + (0x1D512, 'M', 'o'), + (0x1D513, 'M', 'p'), + (0x1D514, 'M', 'q'), + (0x1D515, 'X'), + (0x1D516, 'M', 's'), + (0x1D517, 'M', 't'), + (0x1D518, 'M', 'u'), + (0x1D519, 'M', 'v'), + (0x1D51A, 'M', 'w'), + (0x1D51B, 'M', 'x'), + (0x1D51C, 'M', 'y'), + (0x1D51D, 'X'), + (0x1D51E, 'M', 'a'), + (0x1D51F, 'M', 'b'), + (0x1D520, 'M', 'c'), + (0x1D521, 'M', 'd'), + (0x1D522, 'M', 'e'), + (0x1D523, 'M', 'f'), + (0x1D524, 'M', 'g'), + (0x1D525, 'M', 'h'), + (0x1D526, 'M', 'i'), + (0x1D527, 'M', 'j'), + (0x1D528, 'M', 'k'), + (0x1D529, 'M', 'l'), + (0x1D52A, 'M', 'm'), + (0x1D52B, 'M', 'n'), + (0x1D52C, 'M', 'o'), + (0x1D52D, 'M', 'p'), + (0x1D52E, 'M', 'q'), + (0x1D52F, 'M', 'r'), + (0x1D530, 'M', 's'), + (0x1D531, 'M', 't'), + (0x1D532, 'M', 'u'), + (0x1D533, 'M', 'v'), + (0x1D534, 'M', 'w'), + (0x1D535, 'M', 'x'), + (0x1D536, 'M', 'y'), + (0x1D537, 'M', 'z'), + (0x1D538, 'M', 'a'), + (0x1D539, 'M', 'b'), + (0x1D53A, 'X'), + (0x1D53B, 'M', 'd'), + (0x1D53C, 'M', 'e'), + (0x1D53D, 'M', 'f'), + (0x1D53E, 'M', 'g'), + (0x1D53F, 'X'), + (0x1D540, 'M', 'i'), + (0x1D541, 'M', 'j'), + (0x1D542, 'M', 'k'), + (0x1D543, 'M', 'l'), + (0x1D544, 'M', 'm'), + (0x1D545, 'X'), + (0x1D546, 'M', 'o'), + (0x1D547, 'X'), + (0x1D54A, 'M', 's'), + (0x1D54B, 'M', 't'), + (0x1D54C, 'M', 'u'), + (0x1D54D, 'M', 'v'), + (0x1D54E, 'M', 'w'), + (0x1D54F, 'M', 'x'), + (0x1D550, 'M', 'y'), + (0x1D551, 'X'), + (0x1D552, 'M', 'a'), + ] + +def _seg_64() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D553, 'M', 'b'), + (0x1D554, 'M', 'c'), + (0x1D555, 'M', 'd'), + (0x1D556, 'M', 'e'), + (0x1D557, 'M', 'f'), + (0x1D558, 'M', 'g'), + (0x1D559, 'M', 'h'), + (0x1D55A, 'M', 'i'), + (0x1D55B, 'M', 'j'), + (0x1D55C, 'M', 'k'), + (0x1D55D, 'M', 'l'), + (0x1D55E, 'M', 'm'), + (0x1D55F, 'M', 'n'), + (0x1D560, 'M', 'o'), + (0x1D561, 'M', 'p'), + (0x1D562, 'M', 'q'), + (0x1D563, 'M', 'r'), + (0x1D564, 'M', 's'), + (0x1D565, 'M', 't'), + (0x1D566, 'M', 'u'), + (0x1D567, 'M', 'v'), + (0x1D568, 'M', 'w'), + (0x1D569, 'M', 'x'), + (0x1D56A, 'M', 'y'), + (0x1D56B, 'M', 'z'), + (0x1D56C, 'M', 'a'), + (0x1D56D, 'M', 'b'), + (0x1D56E, 'M', 'c'), + (0x1D56F, 'M', 'd'), + (0x1D570, 'M', 'e'), + (0x1D571, 'M', 'f'), + (0x1D572, 'M', 'g'), + (0x1D573, 'M', 'h'), + (0x1D574, 'M', 'i'), + (0x1D575, 'M', 'j'), + (0x1D576, 'M', 'k'), + (0x1D577, 'M', 'l'), + (0x1D578, 'M', 'm'), + (0x1D579, 'M', 'n'), + (0x1D57A, 'M', 'o'), + (0x1D57B, 'M', 'p'), + (0x1D57C, 'M', 'q'), + (0x1D57D, 'M', 'r'), + (0x1D57E, 'M', 's'), + (0x1D57F, 'M', 't'), + (0x1D580, 'M', 'u'), + (0x1D581, 'M', 'v'), + (0x1D582, 'M', 'w'), + (0x1D583, 'M', 'x'), + (0x1D584, 'M', 'y'), + (0x1D585, 'M', 'z'), + (0x1D586, 'M', 'a'), + (0x1D587, 'M', 'b'), + (0x1D588, 'M', 'c'), + (0x1D589, 'M', 'd'), + (0x1D58A, 'M', 'e'), + (0x1D58B, 'M', 'f'), + (0x1D58C, 'M', 'g'), + (0x1D58D, 'M', 'h'), + (0x1D58E, 'M', 'i'), + (0x1D58F, 'M', 'j'), + (0x1D590, 'M', 'k'), + (0x1D591, 'M', 'l'), + (0x1D592, 'M', 'm'), + (0x1D593, 'M', 'n'), + (0x1D594, 'M', 'o'), + (0x1D595, 'M', 'p'), + (0x1D596, 'M', 'q'), + (0x1D597, 'M', 'r'), + (0x1D598, 'M', 's'), + (0x1D599, 'M', 't'), + (0x1D59A, 'M', 'u'), + (0x1D59B, 'M', 'v'), + (0x1D59C, 'M', 'w'), + (0x1D59D, 'M', 'x'), + (0x1D59E, 'M', 'y'), + (0x1D59F, 'M', 'z'), + (0x1D5A0, 'M', 'a'), + (0x1D5A1, 'M', 'b'), + (0x1D5A2, 'M', 'c'), + (0x1D5A3, 'M', 'd'), + (0x1D5A4, 'M', 'e'), + (0x1D5A5, 'M', 'f'), + (0x1D5A6, 'M', 'g'), + (0x1D5A7, 'M', 'h'), + (0x1D5A8, 'M', 'i'), + (0x1D5A9, 'M', 'j'), + (0x1D5AA, 'M', 'k'), + (0x1D5AB, 'M', 'l'), + (0x1D5AC, 'M', 'm'), + (0x1D5AD, 'M', 'n'), + (0x1D5AE, 'M', 'o'), + (0x1D5AF, 'M', 'p'), + (0x1D5B0, 'M', 'q'), + (0x1D5B1, 'M', 'r'), + (0x1D5B2, 'M', 's'), + (0x1D5B3, 'M', 't'), + (0x1D5B4, 'M', 'u'), + (0x1D5B5, 'M', 'v'), + (0x1D5B6, 'M', 'w'), + ] + +def _seg_65() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D5B7, 'M', 'x'), + (0x1D5B8, 'M', 'y'), + (0x1D5B9, 'M', 'z'), + (0x1D5BA, 'M', 'a'), + (0x1D5BB, 'M', 'b'), + (0x1D5BC, 'M', 'c'), + (0x1D5BD, 'M', 'd'), + (0x1D5BE, 'M', 'e'), + (0x1D5BF, 'M', 'f'), + (0x1D5C0, 'M', 'g'), + (0x1D5C1, 'M', 'h'), + (0x1D5C2, 'M', 'i'), + (0x1D5C3, 'M', 'j'), + (0x1D5C4, 'M', 'k'), + (0x1D5C5, 'M', 'l'), + (0x1D5C6, 'M', 'm'), + (0x1D5C7, 'M', 'n'), + (0x1D5C8, 'M', 'o'), + (0x1D5C9, 'M', 'p'), + (0x1D5CA, 'M', 'q'), + (0x1D5CB, 'M', 'r'), + (0x1D5CC, 'M', 's'), + (0x1D5CD, 'M', 't'), + (0x1D5CE, 'M', 'u'), + (0x1D5CF, 'M', 'v'), + (0x1D5D0, 'M', 'w'), + (0x1D5D1, 'M', 'x'), + (0x1D5D2, 'M', 'y'), + (0x1D5D3, 'M', 'z'), + (0x1D5D4, 'M', 'a'), + (0x1D5D5, 'M', 'b'), + (0x1D5D6, 'M', 'c'), + (0x1D5D7, 'M', 'd'), + (0x1D5D8, 'M', 'e'), + (0x1D5D9, 'M', 'f'), + (0x1D5DA, 'M', 'g'), + (0x1D5DB, 'M', 'h'), + (0x1D5DC, 'M', 'i'), + (0x1D5DD, 'M', 'j'), + (0x1D5DE, 'M', 'k'), + (0x1D5DF, 'M', 'l'), + (0x1D5E0, 'M', 'm'), + (0x1D5E1, 'M', 'n'), + (0x1D5E2, 'M', 'o'), + (0x1D5E3, 'M', 'p'), + (0x1D5E4, 'M', 'q'), + (0x1D5E5, 'M', 'r'), + (0x1D5E6, 'M', 's'), + (0x1D5E7, 'M', 't'), + (0x1D5E8, 'M', 'u'), + (0x1D5E9, 'M', 'v'), + (0x1D5EA, 'M', 'w'), + (0x1D5EB, 'M', 'x'), + (0x1D5EC, 'M', 'y'), + (0x1D5ED, 'M', 'z'), + (0x1D5EE, 'M', 'a'), + (0x1D5EF, 'M', 'b'), + (0x1D5F0, 'M', 'c'), + (0x1D5F1, 'M', 'd'), + (0x1D5F2, 'M', 'e'), + (0x1D5F3, 'M', 'f'), + (0x1D5F4, 'M', 'g'), + (0x1D5F5, 'M', 'h'), + (0x1D5F6, 'M', 'i'), + (0x1D5F7, 'M', 'j'), + (0x1D5F8, 'M', 'k'), + (0x1D5F9, 'M', 'l'), + (0x1D5FA, 'M', 'm'), + (0x1D5FB, 'M', 'n'), + (0x1D5FC, 'M', 'o'), + (0x1D5FD, 'M', 'p'), + (0x1D5FE, 'M', 'q'), + (0x1D5FF, 'M', 'r'), + (0x1D600, 'M', 's'), + (0x1D601, 'M', 't'), + (0x1D602, 'M', 'u'), + (0x1D603, 'M', 'v'), + (0x1D604, 'M', 'w'), + (0x1D605, 'M', 'x'), + (0x1D606, 'M', 'y'), + (0x1D607, 'M', 'z'), + (0x1D608, 'M', 'a'), + (0x1D609, 'M', 'b'), + (0x1D60A, 'M', 'c'), + (0x1D60B, 'M', 'd'), + (0x1D60C, 'M', 'e'), + (0x1D60D, 'M', 'f'), + (0x1D60E, 'M', 'g'), + (0x1D60F, 'M', 'h'), + (0x1D610, 'M', 'i'), + (0x1D611, 'M', 'j'), + (0x1D612, 'M', 'k'), + (0x1D613, 'M', 'l'), + (0x1D614, 'M', 'm'), + (0x1D615, 'M', 'n'), + (0x1D616, 'M', 'o'), + (0x1D617, 'M', 'p'), + (0x1D618, 'M', 'q'), + (0x1D619, 'M', 'r'), + (0x1D61A, 'M', 's'), + ] + +def _seg_66() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D61B, 'M', 't'), + (0x1D61C, 'M', 'u'), + (0x1D61D, 'M', 'v'), + (0x1D61E, 'M', 'w'), + (0x1D61F, 'M', 'x'), + (0x1D620, 'M', 'y'), + (0x1D621, 'M', 'z'), + (0x1D622, 'M', 'a'), + (0x1D623, 'M', 'b'), + (0x1D624, 'M', 'c'), + (0x1D625, 'M', 'd'), + (0x1D626, 'M', 'e'), + (0x1D627, 'M', 'f'), + (0x1D628, 'M', 'g'), + (0x1D629, 'M', 'h'), + (0x1D62A, 'M', 'i'), + (0x1D62B, 'M', 'j'), + (0x1D62C, 'M', 'k'), + (0x1D62D, 'M', 'l'), + (0x1D62E, 'M', 'm'), + (0x1D62F, 'M', 'n'), + (0x1D630, 'M', 'o'), + (0x1D631, 'M', 'p'), + (0x1D632, 'M', 'q'), + (0x1D633, 'M', 'r'), + (0x1D634, 'M', 's'), + (0x1D635, 'M', 't'), + (0x1D636, 'M', 'u'), + (0x1D637, 'M', 'v'), + (0x1D638, 'M', 'w'), + (0x1D639, 'M', 'x'), + (0x1D63A, 'M', 'y'), + (0x1D63B, 'M', 'z'), + (0x1D63C, 'M', 'a'), + (0x1D63D, 'M', 'b'), + (0x1D63E, 'M', 'c'), + (0x1D63F, 'M', 'd'), + (0x1D640, 'M', 'e'), + (0x1D641, 'M', 'f'), + (0x1D642, 'M', 'g'), + (0x1D643, 'M', 'h'), + (0x1D644, 'M', 'i'), + (0x1D645, 'M', 'j'), + (0x1D646, 'M', 'k'), + (0x1D647, 'M', 'l'), + (0x1D648, 'M', 'm'), + (0x1D649, 'M', 'n'), + (0x1D64A, 'M', 'o'), + (0x1D64B, 'M', 'p'), + (0x1D64C, 'M', 'q'), + (0x1D64D, 'M', 'r'), + (0x1D64E, 'M', 's'), + (0x1D64F, 'M', 't'), + (0x1D650, 'M', 'u'), + (0x1D651, 'M', 'v'), + (0x1D652, 'M', 'w'), + (0x1D653, 'M', 'x'), + (0x1D654, 'M', 'y'), + (0x1D655, 'M', 'z'), + (0x1D656, 'M', 'a'), + (0x1D657, 'M', 'b'), + (0x1D658, 'M', 'c'), + (0x1D659, 'M', 'd'), + (0x1D65A, 'M', 'e'), + (0x1D65B, 'M', 'f'), + (0x1D65C, 'M', 'g'), + (0x1D65D, 'M', 'h'), + (0x1D65E, 'M', 'i'), + (0x1D65F, 'M', 'j'), + (0x1D660, 'M', 'k'), + (0x1D661, 'M', 'l'), + (0x1D662, 'M', 'm'), + (0x1D663, 'M', 'n'), + (0x1D664, 'M', 'o'), + (0x1D665, 'M', 'p'), + (0x1D666, 'M', 'q'), + (0x1D667, 'M', 'r'), + (0x1D668, 'M', 's'), + (0x1D669, 'M', 't'), + (0x1D66A, 'M', 'u'), + (0x1D66B, 'M', 'v'), + (0x1D66C, 'M', 'w'), + (0x1D66D, 'M', 'x'), + (0x1D66E, 'M', 'y'), + (0x1D66F, 'M', 'z'), + (0x1D670, 'M', 'a'), + (0x1D671, 'M', 'b'), + (0x1D672, 'M', 'c'), + (0x1D673, 'M', 'd'), + (0x1D674, 'M', 'e'), + (0x1D675, 'M', 'f'), + (0x1D676, 'M', 'g'), + (0x1D677, 'M', 'h'), + (0x1D678, 'M', 'i'), + (0x1D679, 'M', 'j'), + (0x1D67A, 'M', 'k'), + (0x1D67B, 'M', 'l'), + (0x1D67C, 'M', 'm'), + (0x1D67D, 'M', 'n'), + (0x1D67E, 'M', 'o'), + ] + +def _seg_67() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D67F, 'M', 'p'), + (0x1D680, 'M', 'q'), + (0x1D681, 'M', 'r'), + (0x1D682, 'M', 's'), + (0x1D683, 'M', 't'), + (0x1D684, 'M', 'u'), + (0x1D685, 'M', 'v'), + (0x1D686, 'M', 'w'), + (0x1D687, 'M', 'x'), + (0x1D688, 'M', 'y'), + (0x1D689, 'M', 'z'), + (0x1D68A, 'M', 'a'), + (0x1D68B, 'M', 'b'), + (0x1D68C, 'M', 'c'), + (0x1D68D, 'M', 'd'), + (0x1D68E, 'M', 'e'), + (0x1D68F, 'M', 'f'), + (0x1D690, 'M', 'g'), + (0x1D691, 'M', 'h'), + (0x1D692, 'M', 'i'), + (0x1D693, 'M', 'j'), + (0x1D694, 'M', 'k'), + (0x1D695, 'M', 'l'), + (0x1D696, 'M', 'm'), + (0x1D697, 'M', 'n'), + (0x1D698, 'M', 'o'), + (0x1D699, 'M', 'p'), + (0x1D69A, 'M', 'q'), + (0x1D69B, 'M', 'r'), + (0x1D69C, 'M', 's'), + (0x1D69D, 'M', 't'), + (0x1D69E, 'M', 'u'), + (0x1D69F, 'M', 'v'), + (0x1D6A0, 'M', 'w'), + (0x1D6A1, 'M', 'x'), + (0x1D6A2, 'M', 'y'), + (0x1D6A3, 'M', 'z'), + (0x1D6A4, 'M', 'ı'), + (0x1D6A5, 'M', 'ȷ'), + (0x1D6A6, 'X'), + (0x1D6A8, 'M', 'α'), + (0x1D6A9, 'M', 'β'), + (0x1D6AA, 'M', 'γ'), + (0x1D6AB, 'M', 'δ'), + (0x1D6AC, 'M', 'ε'), + (0x1D6AD, 'M', 'ζ'), + (0x1D6AE, 'M', 'η'), + (0x1D6AF, 'M', 'θ'), + (0x1D6B0, 'M', 'ι'), + (0x1D6B1, 'M', 'κ'), + (0x1D6B2, 'M', 'λ'), + (0x1D6B3, 'M', 'μ'), + (0x1D6B4, 'M', 'ν'), + (0x1D6B5, 'M', 'ξ'), + (0x1D6B6, 'M', 'ο'), + (0x1D6B7, 'M', 'π'), + (0x1D6B8, 'M', 'ρ'), + (0x1D6B9, 'M', 'θ'), + (0x1D6BA, 'M', 'σ'), + (0x1D6BB, 'M', 'τ'), + (0x1D6BC, 'M', 'υ'), + (0x1D6BD, 'M', 'φ'), + (0x1D6BE, 'M', 'χ'), + (0x1D6BF, 'M', 'ψ'), + (0x1D6C0, 'M', 'ω'), + (0x1D6C1, 'M', '∇'), + (0x1D6C2, 'M', 'α'), + (0x1D6C3, 'M', 'β'), + (0x1D6C4, 'M', 'γ'), + (0x1D6C5, 'M', 'δ'), + (0x1D6C6, 'M', 'ε'), + (0x1D6C7, 'M', 'ζ'), + (0x1D6C8, 'M', 'η'), + (0x1D6C9, 'M', 'θ'), + (0x1D6CA, 'M', 'ι'), + (0x1D6CB, 'M', 'κ'), + (0x1D6CC, 'M', 'λ'), + (0x1D6CD, 'M', 'μ'), + (0x1D6CE, 'M', 'ν'), + (0x1D6CF, 'M', 'ξ'), + (0x1D6D0, 'M', 'ο'), + (0x1D6D1, 'M', 'π'), + (0x1D6D2, 'M', 'ρ'), + (0x1D6D3, 'M', 'σ'), + (0x1D6D5, 'M', 'τ'), + (0x1D6D6, 'M', 'υ'), + (0x1D6D7, 'M', 'φ'), + (0x1D6D8, 'M', 'χ'), + (0x1D6D9, 'M', 'ψ'), + (0x1D6DA, 'M', 'ω'), + (0x1D6DB, 'M', '∂'), + (0x1D6DC, 'M', 'ε'), + (0x1D6DD, 'M', 'θ'), + (0x1D6DE, 'M', 'κ'), + (0x1D6DF, 'M', 'φ'), + (0x1D6E0, 'M', 'ρ'), + (0x1D6E1, 'M', 'π'), + (0x1D6E2, 'M', 'α'), + (0x1D6E3, 'M', 'β'), + (0x1D6E4, 'M', 'γ'), + ] + +def _seg_68() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D6E5, 'M', 'δ'), + (0x1D6E6, 'M', 'ε'), + (0x1D6E7, 'M', 'ζ'), + (0x1D6E8, 'M', 'η'), + (0x1D6E9, 'M', 'θ'), + (0x1D6EA, 'M', 'ι'), + (0x1D6EB, 'M', 'κ'), + (0x1D6EC, 'M', 'λ'), + (0x1D6ED, 'M', 'μ'), + (0x1D6EE, 'M', 'ν'), + (0x1D6EF, 'M', 'ξ'), + (0x1D6F0, 'M', 'ο'), + (0x1D6F1, 'M', 'π'), + (0x1D6F2, 'M', 'ρ'), + (0x1D6F3, 'M', 'θ'), + (0x1D6F4, 'M', 'σ'), + (0x1D6F5, 'M', 'τ'), + (0x1D6F6, 'M', 'υ'), + (0x1D6F7, 'M', 'φ'), + (0x1D6F8, 'M', 'χ'), + (0x1D6F9, 'M', 'ψ'), + (0x1D6FA, 'M', 'ω'), + (0x1D6FB, 'M', '∇'), + (0x1D6FC, 'M', 'α'), + (0x1D6FD, 'M', 'β'), + (0x1D6FE, 'M', 'γ'), + (0x1D6FF, 'M', 'δ'), + (0x1D700, 'M', 'ε'), + (0x1D701, 'M', 'ζ'), + (0x1D702, 'M', 'η'), + (0x1D703, 'M', 'θ'), + (0x1D704, 'M', 'ι'), + (0x1D705, 'M', 'κ'), + (0x1D706, 'M', 'λ'), + (0x1D707, 'M', 'μ'), + (0x1D708, 'M', 'ν'), + (0x1D709, 'M', 'ξ'), + (0x1D70A, 'M', 'ο'), + (0x1D70B, 'M', 'π'), + (0x1D70C, 'M', 'ρ'), + (0x1D70D, 'M', 'σ'), + (0x1D70F, 'M', 'τ'), + (0x1D710, 'M', 'υ'), + (0x1D711, 'M', 'φ'), + (0x1D712, 'M', 'χ'), + (0x1D713, 'M', 'ψ'), + (0x1D714, 'M', 'ω'), + (0x1D715, 'M', '∂'), + (0x1D716, 'M', 'ε'), + (0x1D717, 'M', 'θ'), + (0x1D718, 'M', 'κ'), + (0x1D719, 'M', 'φ'), + (0x1D71A, 'M', 'ρ'), + (0x1D71B, 'M', 'π'), + (0x1D71C, 'M', 'α'), + (0x1D71D, 'M', 'β'), + (0x1D71E, 'M', 'γ'), + (0x1D71F, 'M', 'δ'), + (0x1D720, 'M', 'ε'), + (0x1D721, 'M', 'ζ'), + (0x1D722, 'M', 'η'), + (0x1D723, 'M', 'θ'), + (0x1D724, 'M', 'ι'), + (0x1D725, 'M', 'κ'), + (0x1D726, 'M', 'λ'), + (0x1D727, 'M', 'μ'), + (0x1D728, 'M', 'ν'), + (0x1D729, 'M', 'ξ'), + (0x1D72A, 'M', 'ο'), + (0x1D72B, 'M', 'π'), + (0x1D72C, 'M', 'ρ'), + (0x1D72D, 'M', 'θ'), + (0x1D72E, 'M', 'σ'), + (0x1D72F, 'M', 'τ'), + (0x1D730, 'M', 'υ'), + (0x1D731, 'M', 'φ'), + (0x1D732, 'M', 'χ'), + (0x1D733, 'M', 'ψ'), + (0x1D734, 'M', 'ω'), + (0x1D735, 'M', '∇'), + (0x1D736, 'M', 'α'), + (0x1D737, 'M', 'β'), + (0x1D738, 'M', 'γ'), + (0x1D739, 'M', 'δ'), + (0x1D73A, 'M', 'ε'), + (0x1D73B, 'M', 'ζ'), + (0x1D73C, 'M', 'η'), + (0x1D73D, 'M', 'θ'), + (0x1D73E, 'M', 'ι'), + (0x1D73F, 'M', 'κ'), + (0x1D740, 'M', 'λ'), + (0x1D741, 'M', 'μ'), + (0x1D742, 'M', 'ν'), + (0x1D743, 'M', 'ξ'), + (0x1D744, 'M', 'ο'), + (0x1D745, 'M', 'π'), + (0x1D746, 'M', 'ρ'), + (0x1D747, 'M', 'σ'), + (0x1D749, 'M', 'τ'), + (0x1D74A, 'M', 'υ'), + ] + +def _seg_69() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D74B, 'M', 'φ'), + (0x1D74C, 'M', 'χ'), + (0x1D74D, 'M', 'ψ'), + (0x1D74E, 'M', 'ω'), + (0x1D74F, 'M', '∂'), + (0x1D750, 'M', 'ε'), + (0x1D751, 'M', 'θ'), + (0x1D752, 'M', 'κ'), + (0x1D753, 'M', 'φ'), + (0x1D754, 'M', 'ρ'), + (0x1D755, 'M', 'π'), + (0x1D756, 'M', 'α'), + (0x1D757, 'M', 'β'), + (0x1D758, 'M', 'γ'), + (0x1D759, 'M', 'δ'), + (0x1D75A, 'M', 'ε'), + (0x1D75B, 'M', 'ζ'), + (0x1D75C, 'M', 'η'), + (0x1D75D, 'M', 'θ'), + (0x1D75E, 'M', 'ι'), + (0x1D75F, 'M', 'κ'), + (0x1D760, 'M', 'λ'), + (0x1D761, 'M', 'μ'), + (0x1D762, 'M', 'ν'), + (0x1D763, 'M', 'ξ'), + (0x1D764, 'M', 'ο'), + (0x1D765, 'M', 'π'), + (0x1D766, 'M', 'ρ'), + (0x1D767, 'M', 'θ'), + (0x1D768, 'M', 'σ'), + (0x1D769, 'M', 'τ'), + (0x1D76A, 'M', 'υ'), + (0x1D76B, 'M', 'φ'), + (0x1D76C, 'M', 'χ'), + (0x1D76D, 'M', 'ψ'), + (0x1D76E, 'M', 'ω'), + (0x1D76F, 'M', '∇'), + (0x1D770, 'M', 'α'), + (0x1D771, 'M', 'β'), + (0x1D772, 'M', 'γ'), + (0x1D773, 'M', 'δ'), + (0x1D774, 'M', 'ε'), + (0x1D775, 'M', 'ζ'), + (0x1D776, 'M', 'η'), + (0x1D777, 'M', 'θ'), + (0x1D778, 'M', 'ι'), + (0x1D779, 'M', 'κ'), + (0x1D77A, 'M', 'λ'), + (0x1D77B, 'M', 'μ'), + (0x1D77C, 'M', 'ν'), + (0x1D77D, 'M', 'ξ'), + (0x1D77E, 'M', 'ο'), + (0x1D77F, 'M', 'π'), + (0x1D780, 'M', 'ρ'), + (0x1D781, 'M', 'σ'), + (0x1D783, 'M', 'τ'), + (0x1D784, 'M', 'υ'), + (0x1D785, 'M', 'φ'), + (0x1D786, 'M', 'χ'), + (0x1D787, 'M', 'ψ'), + (0x1D788, 'M', 'ω'), + (0x1D789, 'M', '∂'), + (0x1D78A, 'M', 'ε'), + (0x1D78B, 'M', 'θ'), + (0x1D78C, 'M', 'κ'), + (0x1D78D, 'M', 'φ'), + (0x1D78E, 'M', 'ρ'), + (0x1D78F, 'M', 'π'), + (0x1D790, 'M', 'α'), + (0x1D791, 'M', 'β'), + (0x1D792, 'M', 'γ'), + (0x1D793, 'M', 'δ'), + (0x1D794, 'M', 'ε'), + (0x1D795, 'M', 'ζ'), + (0x1D796, 'M', 'η'), + (0x1D797, 'M', 'θ'), + (0x1D798, 'M', 'ι'), + (0x1D799, 'M', 'κ'), + (0x1D79A, 'M', 'λ'), + (0x1D79B, 'M', 'μ'), + (0x1D79C, 'M', 'ν'), + (0x1D79D, 'M', 'ξ'), + (0x1D79E, 'M', 'ο'), + (0x1D79F, 'M', 'π'), + (0x1D7A0, 'M', 'ρ'), + (0x1D7A1, 'M', 'θ'), + (0x1D7A2, 'M', 'σ'), + (0x1D7A3, 'M', 'τ'), + (0x1D7A4, 'M', 'υ'), + (0x1D7A5, 'M', 'φ'), + (0x1D7A6, 'M', 'χ'), + (0x1D7A7, 'M', 'ψ'), + (0x1D7A8, 'M', 'ω'), + (0x1D7A9, 'M', '∇'), + (0x1D7AA, 'M', 'α'), + (0x1D7AB, 'M', 'β'), + (0x1D7AC, 'M', 'γ'), + (0x1D7AD, 'M', 'δ'), + (0x1D7AE, 'M', 'ε'), + (0x1D7AF, 'M', 'ζ'), + ] + +def _seg_70() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D7B0, 'M', 'η'), + (0x1D7B1, 'M', 'θ'), + (0x1D7B2, 'M', 'ι'), + (0x1D7B3, 'M', 'κ'), + (0x1D7B4, 'M', 'λ'), + (0x1D7B5, 'M', 'μ'), + (0x1D7B6, 'M', 'ν'), + (0x1D7B7, 'M', 'ξ'), + (0x1D7B8, 'M', 'ο'), + (0x1D7B9, 'M', 'π'), + (0x1D7BA, 'M', 'ρ'), + (0x1D7BB, 'M', 'σ'), + (0x1D7BD, 'M', 'τ'), + (0x1D7BE, 'M', 'υ'), + (0x1D7BF, 'M', 'φ'), + (0x1D7C0, 'M', 'χ'), + (0x1D7C1, 'M', 'ψ'), + (0x1D7C2, 'M', 'ω'), + (0x1D7C3, 'M', '∂'), + (0x1D7C4, 'M', 'ε'), + (0x1D7C5, 'M', 'θ'), + (0x1D7C6, 'M', 'κ'), + (0x1D7C7, 'M', 'φ'), + (0x1D7C8, 'M', 'ρ'), + (0x1D7C9, 'M', 'π'), + (0x1D7CA, 'M', 'ϝ'), + (0x1D7CC, 'X'), + (0x1D7CE, 'M', '0'), + (0x1D7CF, 'M', '1'), + (0x1D7D0, 'M', '2'), + (0x1D7D1, 'M', '3'), + (0x1D7D2, 'M', '4'), + (0x1D7D3, 'M', '5'), + (0x1D7D4, 'M', '6'), + (0x1D7D5, 'M', '7'), + (0x1D7D6, 'M', '8'), + (0x1D7D7, 'M', '9'), + (0x1D7D8, 'M', '0'), + (0x1D7D9, 'M', '1'), + (0x1D7DA, 'M', '2'), + (0x1D7DB, 'M', '3'), + (0x1D7DC, 'M', '4'), + (0x1D7DD, 'M', '5'), + (0x1D7DE, 'M', '6'), + (0x1D7DF, 'M', '7'), + (0x1D7E0, 'M', '8'), + (0x1D7E1, 'M', '9'), + (0x1D7E2, 'M', '0'), + (0x1D7E3, 'M', '1'), + (0x1D7E4, 'M', '2'), + (0x1D7E5, 'M', '3'), + (0x1D7E6, 'M', '4'), + (0x1D7E7, 'M', '5'), + (0x1D7E8, 'M', '6'), + (0x1D7E9, 'M', '7'), + (0x1D7EA, 'M', '8'), + (0x1D7EB, 'M', '9'), + (0x1D7EC, 'M', '0'), + (0x1D7ED, 'M', '1'), + (0x1D7EE, 'M', '2'), + (0x1D7EF, 'M', '3'), + (0x1D7F0, 'M', '4'), + (0x1D7F1, 'M', '5'), + (0x1D7F2, 'M', '6'), + (0x1D7F3, 'M', '7'), + (0x1D7F4, 'M', '8'), + (0x1D7F5, 'M', '9'), + (0x1D7F6, 'M', '0'), + (0x1D7F7, 'M', '1'), + (0x1D7F8, 'M', '2'), + (0x1D7F9, 'M', '3'), + (0x1D7FA, 'M', '4'), + (0x1D7FB, 'M', '5'), + (0x1D7FC, 'M', '6'), + (0x1D7FD, 'M', '7'), + (0x1D7FE, 'M', '8'), + (0x1D7FF, 'M', '9'), + (0x1D800, 'V'), + (0x1DA8C, 'X'), + (0x1DA9B, 'V'), + (0x1DAA0, 'X'), + (0x1DAA1, 'V'), + (0x1DAB0, 'X'), + (0x1DF00, 'V'), + (0x1DF1F, 'X'), + (0x1DF25, 'V'), + (0x1DF2B, 'X'), + (0x1E000, 'V'), + (0x1E007, 'X'), + (0x1E008, 'V'), + (0x1E019, 'X'), + (0x1E01B, 'V'), + (0x1E022, 'X'), + (0x1E023, 'V'), + (0x1E025, 'X'), + (0x1E026, 'V'), + (0x1E02B, 'X'), + (0x1E030, 'M', 'а'), + (0x1E031, 'M', 'б'), + (0x1E032, 'M', 'в'), + ] + +def _seg_71() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1E033, 'M', 'г'), + (0x1E034, 'M', 'д'), + (0x1E035, 'M', 'е'), + (0x1E036, 'M', 'ж'), + (0x1E037, 'M', 'з'), + (0x1E038, 'M', 'и'), + (0x1E039, 'M', 'к'), + (0x1E03A, 'M', 'л'), + (0x1E03B, 'M', 'м'), + (0x1E03C, 'M', 'о'), + (0x1E03D, 'M', 'п'), + (0x1E03E, 'M', 'р'), + (0x1E03F, 'M', 'с'), + (0x1E040, 'M', 'т'), + (0x1E041, 'M', 'у'), + (0x1E042, 'M', 'ф'), + (0x1E043, 'M', 'х'), + (0x1E044, 'M', 'ц'), + (0x1E045, 'M', 'ч'), + (0x1E046, 'M', 'ш'), + (0x1E047, 'M', 'ы'), + (0x1E048, 'M', 'э'), + (0x1E049, 'M', 'ю'), + (0x1E04A, 'M', 'ꚉ'), + (0x1E04B, 'M', 'ә'), + (0x1E04C, 'M', 'і'), + (0x1E04D, 'M', 'ј'), + (0x1E04E, 'M', 'ө'), + (0x1E04F, 'M', 'ү'), + (0x1E050, 'M', 'ӏ'), + (0x1E051, 'M', 'а'), + (0x1E052, 'M', 'б'), + (0x1E053, 'M', 'в'), + (0x1E054, 'M', 'г'), + (0x1E055, 'M', 'д'), + (0x1E056, 'M', 'е'), + (0x1E057, 'M', 'ж'), + (0x1E058, 'M', 'з'), + (0x1E059, 'M', 'и'), + (0x1E05A, 'M', 'к'), + (0x1E05B, 'M', 'л'), + (0x1E05C, 'M', 'о'), + (0x1E05D, 'M', 'п'), + (0x1E05E, 'M', 'с'), + (0x1E05F, 'M', 'у'), + (0x1E060, 'M', 'ф'), + (0x1E061, 'M', 'х'), + (0x1E062, 'M', 'ц'), + (0x1E063, 'M', 'ч'), + (0x1E064, 'M', 'ш'), + (0x1E065, 'M', 'ъ'), + (0x1E066, 'M', 'ы'), + (0x1E067, 'M', 'ґ'), + (0x1E068, 'M', 'і'), + (0x1E069, 'M', 'ѕ'), + (0x1E06A, 'M', 'џ'), + (0x1E06B, 'M', 'ҫ'), + (0x1E06C, 'M', 'ꙑ'), + (0x1E06D, 'M', 'ұ'), + (0x1E06E, 'X'), + (0x1E08F, 'V'), + (0x1E090, 'X'), + (0x1E100, 'V'), + (0x1E12D, 'X'), + (0x1E130, 'V'), + (0x1E13E, 'X'), + (0x1E140, 'V'), + (0x1E14A, 'X'), + (0x1E14E, 'V'), + (0x1E150, 'X'), + (0x1E290, 'V'), + (0x1E2AF, 'X'), + (0x1E2C0, 'V'), + (0x1E2FA, 'X'), + (0x1E2FF, 'V'), + (0x1E300, 'X'), + (0x1E4D0, 'V'), + (0x1E4FA, 'X'), + (0x1E7E0, 'V'), + (0x1E7E7, 'X'), + (0x1E7E8, 'V'), + (0x1E7EC, 'X'), + (0x1E7ED, 'V'), + (0x1E7EF, 'X'), + (0x1E7F0, 'V'), + (0x1E7FF, 'X'), + (0x1E800, 'V'), + (0x1E8C5, 'X'), + (0x1E8C7, 'V'), + (0x1E8D7, 'X'), + (0x1E900, 'M', '𞤢'), + (0x1E901, 'M', '𞤣'), + (0x1E902, 'M', '𞤤'), + (0x1E903, 'M', '𞤥'), + (0x1E904, 'M', '𞤦'), + (0x1E905, 'M', '𞤧'), + (0x1E906, 'M', '𞤨'), + (0x1E907, 'M', '𞤩'), + (0x1E908, 'M', '𞤪'), + (0x1E909, 'M', '𞤫'), + ] + +def _seg_72() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1E90A, 'M', '𞤬'), + (0x1E90B, 'M', '𞤭'), + (0x1E90C, 'M', '𞤮'), + (0x1E90D, 'M', '𞤯'), + (0x1E90E, 'M', '𞤰'), + (0x1E90F, 'M', '𞤱'), + (0x1E910, 'M', '𞤲'), + (0x1E911, 'M', '𞤳'), + (0x1E912, 'M', '𞤴'), + (0x1E913, 'M', '𞤵'), + (0x1E914, 'M', '𞤶'), + (0x1E915, 'M', '𞤷'), + (0x1E916, 'M', '𞤸'), + (0x1E917, 'M', '𞤹'), + (0x1E918, 'M', '𞤺'), + (0x1E919, 'M', '𞤻'), + (0x1E91A, 'M', '𞤼'), + (0x1E91B, 'M', '𞤽'), + (0x1E91C, 'M', '𞤾'), + (0x1E91D, 'M', '𞤿'), + (0x1E91E, 'M', '𞥀'), + (0x1E91F, 'M', '𞥁'), + (0x1E920, 'M', '𞥂'), + (0x1E921, 'M', '𞥃'), + (0x1E922, 'V'), + (0x1E94C, 'X'), + (0x1E950, 'V'), + (0x1E95A, 'X'), + (0x1E95E, 'V'), + (0x1E960, 'X'), + (0x1EC71, 'V'), + (0x1ECB5, 'X'), + (0x1ED01, 'V'), + (0x1ED3E, 'X'), + (0x1EE00, 'M', 'ا'), + (0x1EE01, 'M', 'ب'), + (0x1EE02, 'M', 'ج'), + (0x1EE03, 'M', 'د'), + (0x1EE04, 'X'), + (0x1EE05, 'M', 'و'), + (0x1EE06, 'M', 'ز'), + (0x1EE07, 'M', 'ح'), + (0x1EE08, 'M', 'ط'), + (0x1EE09, 'M', 'ي'), + (0x1EE0A, 'M', 'ك'), + (0x1EE0B, 'M', 'ل'), + (0x1EE0C, 'M', 'م'), + (0x1EE0D, 'M', 'ن'), + (0x1EE0E, 'M', 'س'), + (0x1EE0F, 'M', 'ع'), + (0x1EE10, 'M', 'ف'), + (0x1EE11, 'M', 'ص'), + (0x1EE12, 'M', 'ق'), + (0x1EE13, 'M', 'ر'), + (0x1EE14, 'M', 'ش'), + (0x1EE15, 'M', 'ت'), + (0x1EE16, 'M', 'ث'), + (0x1EE17, 'M', 'خ'), + (0x1EE18, 'M', 'ذ'), + (0x1EE19, 'M', 'ض'), + (0x1EE1A, 'M', 'ظ'), + (0x1EE1B, 'M', 'غ'), + (0x1EE1C, 'M', 'ٮ'), + (0x1EE1D, 'M', 'ں'), + (0x1EE1E, 'M', 'ڡ'), + (0x1EE1F, 'M', 'ٯ'), + (0x1EE20, 'X'), + (0x1EE21, 'M', 'ب'), + (0x1EE22, 'M', 'ج'), + (0x1EE23, 'X'), + (0x1EE24, 'M', 'ه'), + (0x1EE25, 'X'), + (0x1EE27, 'M', 'ح'), + (0x1EE28, 'X'), + (0x1EE29, 'M', 'ي'), + (0x1EE2A, 'M', 'ك'), + (0x1EE2B, 'M', 'ل'), + (0x1EE2C, 'M', 'م'), + (0x1EE2D, 'M', 'ن'), + (0x1EE2E, 'M', 'س'), + (0x1EE2F, 'M', 'ع'), + (0x1EE30, 'M', 'ف'), + (0x1EE31, 'M', 'ص'), + (0x1EE32, 'M', 'ق'), + (0x1EE33, 'X'), + (0x1EE34, 'M', 'ش'), + (0x1EE35, 'M', 'ت'), + (0x1EE36, 'M', 'ث'), + (0x1EE37, 'M', 'خ'), + (0x1EE38, 'X'), + (0x1EE39, 'M', 'ض'), + (0x1EE3A, 'X'), + (0x1EE3B, 'M', 'غ'), + (0x1EE3C, 'X'), + (0x1EE42, 'M', 'ج'), + (0x1EE43, 'X'), + (0x1EE47, 'M', 'ح'), + (0x1EE48, 'X'), + (0x1EE49, 'M', 'ي'), + (0x1EE4A, 'X'), + ] + +def _seg_73() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1EE4B, 'M', 'ل'), + (0x1EE4C, 'X'), + (0x1EE4D, 'M', 'ن'), + (0x1EE4E, 'M', 'س'), + (0x1EE4F, 'M', 'ع'), + (0x1EE50, 'X'), + (0x1EE51, 'M', 'ص'), + (0x1EE52, 'M', 'ق'), + (0x1EE53, 'X'), + (0x1EE54, 'M', 'ش'), + (0x1EE55, 'X'), + (0x1EE57, 'M', 'خ'), + (0x1EE58, 'X'), + (0x1EE59, 'M', 'ض'), + (0x1EE5A, 'X'), + (0x1EE5B, 'M', 'غ'), + (0x1EE5C, 'X'), + (0x1EE5D, 'M', 'ں'), + (0x1EE5E, 'X'), + (0x1EE5F, 'M', 'ٯ'), + (0x1EE60, 'X'), + (0x1EE61, 'M', 'ب'), + (0x1EE62, 'M', 'ج'), + (0x1EE63, 'X'), + (0x1EE64, 'M', 'ه'), + (0x1EE65, 'X'), + (0x1EE67, 'M', 'ح'), + (0x1EE68, 'M', 'ط'), + (0x1EE69, 'M', 'ي'), + (0x1EE6A, 'M', 'ك'), + (0x1EE6B, 'X'), + (0x1EE6C, 'M', 'م'), + (0x1EE6D, 'M', 'ن'), + (0x1EE6E, 'M', 'س'), + (0x1EE6F, 'M', 'ع'), + (0x1EE70, 'M', 'ف'), + (0x1EE71, 'M', 'ص'), + (0x1EE72, 'M', 'ق'), + (0x1EE73, 'X'), + (0x1EE74, 'M', 'ش'), + (0x1EE75, 'M', 'ت'), + (0x1EE76, 'M', 'ث'), + (0x1EE77, 'M', 'خ'), + (0x1EE78, 'X'), + (0x1EE79, 'M', 'ض'), + (0x1EE7A, 'M', 'ظ'), + (0x1EE7B, 'M', 'غ'), + (0x1EE7C, 'M', 'ٮ'), + (0x1EE7D, 'X'), + (0x1EE7E, 'M', 'ڡ'), + (0x1EE7F, 'X'), + (0x1EE80, 'M', 'ا'), + (0x1EE81, 'M', 'ب'), + (0x1EE82, 'M', 'ج'), + (0x1EE83, 'M', 'د'), + (0x1EE84, 'M', 'ه'), + (0x1EE85, 'M', 'و'), + (0x1EE86, 'M', 'ز'), + (0x1EE87, 'M', 'ح'), + (0x1EE88, 'M', 'ط'), + (0x1EE89, 'M', 'ي'), + (0x1EE8A, 'X'), + (0x1EE8B, 'M', 'ل'), + (0x1EE8C, 'M', 'م'), + (0x1EE8D, 'M', 'ن'), + (0x1EE8E, 'M', 'س'), + (0x1EE8F, 'M', 'ع'), + (0x1EE90, 'M', 'ف'), + (0x1EE91, 'M', 'ص'), + (0x1EE92, 'M', 'ق'), + (0x1EE93, 'M', 'ر'), + (0x1EE94, 'M', 'ش'), + (0x1EE95, 'M', 'ت'), + (0x1EE96, 'M', 'ث'), + (0x1EE97, 'M', 'خ'), + (0x1EE98, 'M', 'ذ'), + (0x1EE99, 'M', 'ض'), + (0x1EE9A, 'M', 'ظ'), + (0x1EE9B, 'M', 'غ'), + (0x1EE9C, 'X'), + (0x1EEA1, 'M', 'ب'), + (0x1EEA2, 'M', 'ج'), + (0x1EEA3, 'M', 'د'), + (0x1EEA4, 'X'), + (0x1EEA5, 'M', 'و'), + (0x1EEA6, 'M', 'ز'), + (0x1EEA7, 'M', 'ح'), + (0x1EEA8, 'M', 'ط'), + (0x1EEA9, 'M', 'ي'), + (0x1EEAA, 'X'), + (0x1EEAB, 'M', 'ل'), + (0x1EEAC, 'M', 'م'), + (0x1EEAD, 'M', 'ن'), + (0x1EEAE, 'M', 'س'), + (0x1EEAF, 'M', 'ع'), + (0x1EEB0, 'M', 'ف'), + (0x1EEB1, 'M', 'ص'), + (0x1EEB2, 'M', 'ق'), + (0x1EEB3, 'M', 'ر'), + (0x1EEB4, 'M', 'ش'), + ] + +def _seg_74() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1EEB5, 'M', 'ت'), + (0x1EEB6, 'M', 'ث'), + (0x1EEB7, 'M', 'خ'), + (0x1EEB8, 'M', 'ذ'), + (0x1EEB9, 'M', 'ض'), + (0x1EEBA, 'M', 'ظ'), + (0x1EEBB, 'M', 'غ'), + (0x1EEBC, 'X'), + (0x1EEF0, 'V'), + (0x1EEF2, 'X'), + (0x1F000, 'V'), + (0x1F02C, 'X'), + (0x1F030, 'V'), + (0x1F094, 'X'), + (0x1F0A0, 'V'), + (0x1F0AF, 'X'), + (0x1F0B1, 'V'), + (0x1F0C0, 'X'), + (0x1F0C1, 'V'), + (0x1F0D0, 'X'), + (0x1F0D1, 'V'), + (0x1F0F6, 'X'), + (0x1F101, '3', '0,'), + (0x1F102, '3', '1,'), + (0x1F103, '3', '2,'), + (0x1F104, '3', '3,'), + (0x1F105, '3', '4,'), + (0x1F106, '3', '5,'), + (0x1F107, '3', '6,'), + (0x1F108, '3', '7,'), + (0x1F109, '3', '8,'), + (0x1F10A, '3', '9,'), + (0x1F10B, 'V'), + (0x1F110, '3', '(a)'), + (0x1F111, '3', '(b)'), + (0x1F112, '3', '(c)'), + (0x1F113, '3', '(d)'), + (0x1F114, '3', '(e)'), + (0x1F115, '3', '(f)'), + (0x1F116, '3', '(g)'), + (0x1F117, '3', '(h)'), + (0x1F118, '3', '(i)'), + (0x1F119, '3', '(j)'), + (0x1F11A, '3', '(k)'), + (0x1F11B, '3', '(l)'), + (0x1F11C, '3', '(m)'), + (0x1F11D, '3', '(n)'), + (0x1F11E, '3', '(o)'), + (0x1F11F, '3', '(p)'), + (0x1F120, '3', '(q)'), + (0x1F121, '3', '(r)'), + (0x1F122, '3', '(s)'), + (0x1F123, '3', '(t)'), + (0x1F124, '3', '(u)'), + (0x1F125, '3', '(v)'), + (0x1F126, '3', '(w)'), + (0x1F127, '3', '(x)'), + (0x1F128, '3', '(y)'), + (0x1F129, '3', '(z)'), + (0x1F12A, 'M', '〔s〕'), + (0x1F12B, 'M', 'c'), + (0x1F12C, 'M', 'r'), + (0x1F12D, 'M', 'cd'), + (0x1F12E, 'M', 'wz'), + (0x1F12F, 'V'), + (0x1F130, 'M', 'a'), + (0x1F131, 'M', 'b'), + (0x1F132, 'M', 'c'), + (0x1F133, 'M', 'd'), + (0x1F134, 'M', 'e'), + (0x1F135, 'M', 'f'), + (0x1F136, 'M', 'g'), + (0x1F137, 'M', 'h'), + (0x1F138, 'M', 'i'), + (0x1F139, 'M', 'j'), + (0x1F13A, 'M', 'k'), + (0x1F13B, 'M', 'l'), + (0x1F13C, 'M', 'm'), + (0x1F13D, 'M', 'n'), + (0x1F13E, 'M', 'o'), + (0x1F13F, 'M', 'p'), + (0x1F140, 'M', 'q'), + (0x1F141, 'M', 'r'), + (0x1F142, 'M', 's'), + (0x1F143, 'M', 't'), + (0x1F144, 'M', 'u'), + (0x1F145, 'M', 'v'), + (0x1F146, 'M', 'w'), + (0x1F147, 'M', 'x'), + (0x1F148, 'M', 'y'), + (0x1F149, 'M', 'z'), + (0x1F14A, 'M', 'hv'), + (0x1F14B, 'M', 'mv'), + (0x1F14C, 'M', 'sd'), + (0x1F14D, 'M', 'ss'), + (0x1F14E, 'M', 'ppv'), + (0x1F14F, 'M', 'wc'), + (0x1F150, 'V'), + (0x1F16A, 'M', 'mc'), + (0x1F16B, 'M', 'md'), + ] + +def _seg_75() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1F16C, 'M', 'mr'), + (0x1F16D, 'V'), + (0x1F190, 'M', 'dj'), + (0x1F191, 'V'), + (0x1F1AE, 'X'), + (0x1F1E6, 'V'), + (0x1F200, 'M', 'ほか'), + (0x1F201, 'M', 'ココ'), + (0x1F202, 'M', 'サ'), + (0x1F203, 'X'), + (0x1F210, 'M', '手'), + (0x1F211, 'M', '字'), + (0x1F212, 'M', '双'), + (0x1F213, 'M', 'デ'), + (0x1F214, 'M', '二'), + (0x1F215, 'M', '多'), + (0x1F216, 'M', '解'), + (0x1F217, 'M', '天'), + (0x1F218, 'M', '交'), + (0x1F219, 'M', '映'), + (0x1F21A, 'M', '無'), + (0x1F21B, 'M', '料'), + (0x1F21C, 'M', '前'), + (0x1F21D, 'M', '後'), + (0x1F21E, 'M', '再'), + (0x1F21F, 'M', '新'), + (0x1F220, 'M', '初'), + (0x1F221, 'M', '終'), + (0x1F222, 'M', '生'), + (0x1F223, 'M', '販'), + (0x1F224, 'M', '声'), + (0x1F225, 'M', '吹'), + (0x1F226, 'M', '演'), + (0x1F227, 'M', '投'), + (0x1F228, 'M', '捕'), + (0x1F229, 'M', '一'), + (0x1F22A, 'M', '三'), + (0x1F22B, 'M', '遊'), + (0x1F22C, 'M', '左'), + (0x1F22D, 'M', '中'), + (0x1F22E, 'M', '右'), + (0x1F22F, 'M', '指'), + (0x1F230, 'M', '走'), + (0x1F231, 'M', '打'), + (0x1F232, 'M', '禁'), + (0x1F233, 'M', '空'), + (0x1F234, 'M', '合'), + (0x1F235, 'M', '満'), + (0x1F236, 'M', '有'), + (0x1F237, 'M', '月'), + (0x1F238, 'M', '申'), + (0x1F239, 'M', '割'), + (0x1F23A, 'M', '営'), + (0x1F23B, 'M', '配'), + (0x1F23C, 'X'), + (0x1F240, 'M', '〔本〕'), + (0x1F241, 'M', '〔三〕'), + (0x1F242, 'M', '〔二〕'), + (0x1F243, 'M', '〔安〕'), + (0x1F244, 'M', '〔点〕'), + (0x1F245, 'M', '〔打〕'), + (0x1F246, 'M', '〔盗〕'), + (0x1F247, 'M', '〔勝〕'), + (0x1F248, 'M', '〔敗〕'), + (0x1F249, 'X'), + (0x1F250, 'M', '得'), + (0x1F251, 'M', '可'), + (0x1F252, 'X'), + (0x1F260, 'V'), + (0x1F266, 'X'), + (0x1F300, 'V'), + (0x1F6D8, 'X'), + (0x1F6DC, 'V'), + (0x1F6ED, 'X'), + (0x1F6F0, 'V'), + (0x1F6FD, 'X'), + (0x1F700, 'V'), + (0x1F777, 'X'), + (0x1F77B, 'V'), + (0x1F7DA, 'X'), + (0x1F7E0, 'V'), + (0x1F7EC, 'X'), + (0x1F7F0, 'V'), + (0x1F7F1, 'X'), + (0x1F800, 'V'), + (0x1F80C, 'X'), + (0x1F810, 'V'), + (0x1F848, 'X'), + (0x1F850, 'V'), + (0x1F85A, 'X'), + (0x1F860, 'V'), + (0x1F888, 'X'), + (0x1F890, 'V'), + (0x1F8AE, 'X'), + (0x1F8B0, 'V'), + (0x1F8B2, 'X'), + (0x1F900, 'V'), + (0x1FA54, 'X'), + (0x1FA60, 'V'), + (0x1FA6E, 'X'), + ] + +def _seg_76() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1FA70, 'V'), + (0x1FA7D, 'X'), + (0x1FA80, 'V'), + (0x1FA89, 'X'), + (0x1FA90, 'V'), + (0x1FABE, 'X'), + (0x1FABF, 'V'), + (0x1FAC6, 'X'), + (0x1FACE, 'V'), + (0x1FADC, 'X'), + (0x1FAE0, 'V'), + (0x1FAE9, 'X'), + (0x1FAF0, 'V'), + (0x1FAF9, 'X'), + (0x1FB00, 'V'), + (0x1FB93, 'X'), + (0x1FB94, 'V'), + (0x1FBCB, 'X'), + (0x1FBF0, 'M', '0'), + (0x1FBF1, 'M', '1'), + (0x1FBF2, 'M', '2'), + (0x1FBF3, 'M', '3'), + (0x1FBF4, 'M', '4'), + (0x1FBF5, 'M', '5'), + (0x1FBF6, 'M', '6'), + (0x1FBF7, 'M', '7'), + (0x1FBF8, 'M', '8'), + (0x1FBF9, 'M', '9'), + (0x1FBFA, 'X'), + (0x20000, 'V'), + (0x2A6E0, 'X'), + (0x2A700, 'V'), + (0x2B73A, 'X'), + (0x2B740, 'V'), + (0x2B81E, 'X'), + (0x2B820, 'V'), + (0x2CEA2, 'X'), + (0x2CEB0, 'V'), + (0x2EBE1, 'X'), + (0x2F800, 'M', '丽'), + (0x2F801, 'M', '丸'), + (0x2F802, 'M', '乁'), + (0x2F803, 'M', '𠄢'), + (0x2F804, 'M', '你'), + (0x2F805, 'M', '侮'), + (0x2F806, 'M', '侻'), + (0x2F807, 'M', '倂'), + (0x2F808, 'M', '偺'), + (0x2F809, 'M', '備'), + (0x2F80A, 'M', '僧'), + (0x2F80B, 'M', '像'), + (0x2F80C, 'M', '㒞'), + (0x2F80D, 'M', '𠘺'), + (0x2F80E, 'M', '免'), + (0x2F80F, 'M', '兔'), + (0x2F810, 'M', '兤'), + (0x2F811, 'M', '具'), + (0x2F812, 'M', '𠔜'), + (0x2F813, 'M', '㒹'), + (0x2F814, 'M', '內'), + (0x2F815, 'M', '再'), + (0x2F816, 'M', '𠕋'), + (0x2F817, 'M', '冗'), + (0x2F818, 'M', '冤'), + (0x2F819, 'M', '仌'), + (0x2F81A, 'M', '冬'), + (0x2F81B, 'M', '况'), + (0x2F81C, 'M', '𩇟'), + (0x2F81D, 'M', '凵'), + (0x2F81E, 'M', '刃'), + (0x2F81F, 'M', '㓟'), + (0x2F820, 'M', '刻'), + (0x2F821, 'M', '剆'), + (0x2F822, 'M', '割'), + (0x2F823, 'M', '剷'), + (0x2F824, 'M', '㔕'), + (0x2F825, 'M', '勇'), + (0x2F826, 'M', '勉'), + (0x2F827, 'M', '勤'), + (0x2F828, 'M', '勺'), + (0x2F829, 'M', '包'), + (0x2F82A, 'M', '匆'), + (0x2F82B, 'M', '北'), + (0x2F82C, 'M', '卉'), + (0x2F82D, 'M', '卑'), + (0x2F82E, 'M', '博'), + (0x2F82F, 'M', '即'), + (0x2F830, 'M', '卽'), + (0x2F831, 'M', '卿'), + (0x2F834, 'M', '𠨬'), + (0x2F835, 'M', '灰'), + (0x2F836, 'M', '及'), + (0x2F837, 'M', '叟'), + (0x2F838, 'M', '𠭣'), + (0x2F839, 'M', '叫'), + (0x2F83A, 'M', '叱'), + (0x2F83B, 'M', '吆'), + (0x2F83C, 'M', '咞'), + (0x2F83D, 'M', '吸'), + (0x2F83E, 'M', '呈'), + ] + +def _seg_77() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F83F, 'M', '周'), + (0x2F840, 'M', '咢'), + (0x2F841, 'M', '哶'), + (0x2F842, 'M', '唐'), + (0x2F843, 'M', '啓'), + (0x2F844, 'M', '啣'), + (0x2F845, 'M', '善'), + (0x2F847, 'M', '喙'), + (0x2F848, 'M', '喫'), + (0x2F849, 'M', '喳'), + (0x2F84A, 'M', '嗂'), + (0x2F84B, 'M', '圖'), + (0x2F84C, 'M', '嘆'), + (0x2F84D, 'M', '圗'), + (0x2F84E, 'M', '噑'), + (0x2F84F, 'M', '噴'), + (0x2F850, 'M', '切'), + (0x2F851, 'M', '壮'), + (0x2F852, 'M', '城'), + (0x2F853, 'M', '埴'), + (0x2F854, 'M', '堍'), + (0x2F855, 'M', '型'), + (0x2F856, 'M', '堲'), + (0x2F857, 'M', '報'), + (0x2F858, 'M', '墬'), + (0x2F859, 'M', '𡓤'), + (0x2F85A, 'M', '売'), + (0x2F85B, 'M', '壷'), + (0x2F85C, 'M', '夆'), + (0x2F85D, 'M', '多'), + (0x2F85E, 'M', '夢'), + (0x2F85F, 'M', '奢'), + (0x2F860, 'M', '𡚨'), + (0x2F861, 'M', '𡛪'), + (0x2F862, 'M', '姬'), + (0x2F863, 'M', '娛'), + (0x2F864, 'M', '娧'), + (0x2F865, 'M', '姘'), + (0x2F866, 'M', '婦'), + (0x2F867, 'M', '㛮'), + (0x2F868, 'X'), + (0x2F869, 'M', '嬈'), + (0x2F86A, 'M', '嬾'), + (0x2F86C, 'M', '𡧈'), + (0x2F86D, 'M', '寃'), + (0x2F86E, 'M', '寘'), + (0x2F86F, 'M', '寧'), + (0x2F870, 'M', '寳'), + (0x2F871, 'M', '𡬘'), + (0x2F872, 'M', '寿'), + (0x2F873, 'M', '将'), + (0x2F874, 'X'), + (0x2F875, 'M', '尢'), + (0x2F876, 'M', '㞁'), + (0x2F877, 'M', '屠'), + (0x2F878, 'M', '屮'), + (0x2F879, 'M', '峀'), + (0x2F87A, 'M', '岍'), + (0x2F87B, 'M', '𡷤'), + (0x2F87C, 'M', '嵃'), + (0x2F87D, 'M', '𡷦'), + (0x2F87E, 'M', '嵮'), + (0x2F87F, 'M', '嵫'), + (0x2F880, 'M', '嵼'), + (0x2F881, 'M', '巡'), + (0x2F882, 'M', '巢'), + (0x2F883, 'M', '㠯'), + (0x2F884, 'M', '巽'), + (0x2F885, 'M', '帨'), + (0x2F886, 'M', '帽'), + (0x2F887, 'M', '幩'), + (0x2F888, 'M', '㡢'), + (0x2F889, 'M', '𢆃'), + (0x2F88A, 'M', '㡼'), + (0x2F88B, 'M', '庰'), + (0x2F88C, 'M', '庳'), + (0x2F88D, 'M', '庶'), + (0x2F88E, 'M', '廊'), + (0x2F88F, 'M', '𪎒'), + (0x2F890, 'M', '廾'), + (0x2F891, 'M', '𢌱'), + (0x2F893, 'M', '舁'), + (0x2F894, 'M', '弢'), + (0x2F896, 'M', '㣇'), + (0x2F897, 'M', '𣊸'), + (0x2F898, 'M', '𦇚'), + (0x2F899, 'M', '形'), + (0x2F89A, 'M', '彫'), + (0x2F89B, 'M', '㣣'), + (0x2F89C, 'M', '徚'), + (0x2F89D, 'M', '忍'), + (0x2F89E, 'M', '志'), + (0x2F89F, 'M', '忹'), + (0x2F8A0, 'M', '悁'), + (0x2F8A1, 'M', '㤺'), + (0x2F8A2, 'M', '㤜'), + (0x2F8A3, 'M', '悔'), + (0x2F8A4, 'M', '𢛔'), + (0x2F8A5, 'M', '惇'), + (0x2F8A6, 'M', '慈'), + ] + +def _seg_78() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F8A7, 'M', '慌'), + (0x2F8A8, 'M', '慎'), + (0x2F8A9, 'M', '慌'), + (0x2F8AA, 'M', '慺'), + (0x2F8AB, 'M', '憎'), + (0x2F8AC, 'M', '憲'), + (0x2F8AD, 'M', '憤'), + (0x2F8AE, 'M', '憯'), + (0x2F8AF, 'M', '懞'), + (0x2F8B0, 'M', '懲'), + (0x2F8B1, 'M', '懶'), + (0x2F8B2, 'M', '成'), + (0x2F8B3, 'M', '戛'), + (0x2F8B4, 'M', '扝'), + (0x2F8B5, 'M', '抱'), + (0x2F8B6, 'M', '拔'), + (0x2F8B7, 'M', '捐'), + (0x2F8B8, 'M', '𢬌'), + (0x2F8B9, 'M', '挽'), + (0x2F8BA, 'M', '拼'), + (0x2F8BB, 'M', '捨'), + (0x2F8BC, 'M', '掃'), + (0x2F8BD, 'M', '揤'), + (0x2F8BE, 'M', '𢯱'), + (0x2F8BF, 'M', '搢'), + (0x2F8C0, 'M', '揅'), + (0x2F8C1, 'M', '掩'), + (0x2F8C2, 'M', '㨮'), + (0x2F8C3, 'M', '摩'), + (0x2F8C4, 'M', '摾'), + (0x2F8C5, 'M', '撝'), + (0x2F8C6, 'M', '摷'), + (0x2F8C7, 'M', '㩬'), + (0x2F8C8, 'M', '敏'), + (0x2F8C9, 'M', '敬'), + (0x2F8CA, 'M', '𣀊'), + (0x2F8CB, 'M', '旣'), + (0x2F8CC, 'M', '書'), + (0x2F8CD, 'M', '晉'), + (0x2F8CE, 'M', '㬙'), + (0x2F8CF, 'M', '暑'), + (0x2F8D0, 'M', '㬈'), + (0x2F8D1, 'M', '㫤'), + (0x2F8D2, 'M', '冒'), + (0x2F8D3, 'M', '冕'), + (0x2F8D4, 'M', '最'), + (0x2F8D5, 'M', '暜'), + (0x2F8D6, 'M', '肭'), + (0x2F8D7, 'M', '䏙'), + (0x2F8D8, 'M', '朗'), + (0x2F8D9, 'M', '望'), + (0x2F8DA, 'M', '朡'), + (0x2F8DB, 'M', '杞'), + (0x2F8DC, 'M', '杓'), + (0x2F8DD, 'M', '𣏃'), + (0x2F8DE, 'M', '㭉'), + (0x2F8DF, 'M', '柺'), + (0x2F8E0, 'M', '枅'), + (0x2F8E1, 'M', '桒'), + (0x2F8E2, 'M', '梅'), + (0x2F8E3, 'M', '𣑭'), + (0x2F8E4, 'M', '梎'), + (0x2F8E5, 'M', '栟'), + (0x2F8E6, 'M', '椔'), + (0x2F8E7, 'M', '㮝'), + (0x2F8E8, 'M', '楂'), + (0x2F8E9, 'M', '榣'), + (0x2F8EA, 'M', '槪'), + (0x2F8EB, 'M', '檨'), + (0x2F8EC, 'M', '𣚣'), + (0x2F8ED, 'M', '櫛'), + (0x2F8EE, 'M', '㰘'), + (0x2F8EF, 'M', '次'), + (0x2F8F0, 'M', '𣢧'), + (0x2F8F1, 'M', '歔'), + (0x2F8F2, 'M', '㱎'), + (0x2F8F3, 'M', '歲'), + (0x2F8F4, 'M', '殟'), + (0x2F8F5, 'M', '殺'), + (0x2F8F6, 'M', '殻'), + (0x2F8F7, 'M', '𣪍'), + (0x2F8F8, 'M', '𡴋'), + (0x2F8F9, 'M', '𣫺'), + (0x2F8FA, 'M', '汎'), + (0x2F8FB, 'M', '𣲼'), + (0x2F8FC, 'M', '沿'), + (0x2F8FD, 'M', '泍'), + (0x2F8FE, 'M', '汧'), + (0x2F8FF, 'M', '洖'), + (0x2F900, 'M', '派'), + (0x2F901, 'M', '海'), + (0x2F902, 'M', '流'), + (0x2F903, 'M', '浩'), + (0x2F904, 'M', '浸'), + (0x2F905, 'M', '涅'), + (0x2F906, 'M', '𣴞'), + (0x2F907, 'M', '洴'), + (0x2F908, 'M', '港'), + (0x2F909, 'M', '湮'), + (0x2F90A, 'M', '㴳'), + ] + +def _seg_79() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F90B, 'M', '滋'), + (0x2F90C, 'M', '滇'), + (0x2F90D, 'M', '𣻑'), + (0x2F90E, 'M', '淹'), + (0x2F90F, 'M', '潮'), + (0x2F910, 'M', '𣽞'), + (0x2F911, 'M', '𣾎'), + (0x2F912, 'M', '濆'), + (0x2F913, 'M', '瀹'), + (0x2F914, 'M', '瀞'), + (0x2F915, 'M', '瀛'), + (0x2F916, 'M', '㶖'), + (0x2F917, 'M', '灊'), + (0x2F918, 'M', '災'), + (0x2F919, 'M', '灷'), + (0x2F91A, 'M', '炭'), + (0x2F91B, 'M', '𠔥'), + (0x2F91C, 'M', '煅'), + (0x2F91D, 'M', '𤉣'), + (0x2F91E, 'M', '熜'), + (0x2F91F, 'X'), + (0x2F920, 'M', '爨'), + (0x2F921, 'M', '爵'), + (0x2F922, 'M', '牐'), + (0x2F923, 'M', '𤘈'), + (0x2F924, 'M', '犀'), + (0x2F925, 'M', '犕'), + (0x2F926, 'M', '𤜵'), + (0x2F927, 'M', '𤠔'), + (0x2F928, 'M', '獺'), + (0x2F929, 'M', '王'), + (0x2F92A, 'M', '㺬'), + (0x2F92B, 'M', '玥'), + (0x2F92C, 'M', '㺸'), + (0x2F92E, 'M', '瑇'), + (0x2F92F, 'M', '瑜'), + (0x2F930, 'M', '瑱'), + (0x2F931, 'M', '璅'), + (0x2F932, 'M', '瓊'), + (0x2F933, 'M', '㼛'), + (0x2F934, 'M', '甤'), + (0x2F935, 'M', '𤰶'), + (0x2F936, 'M', '甾'), + (0x2F937, 'M', '𤲒'), + (0x2F938, 'M', '異'), + (0x2F939, 'M', '𢆟'), + (0x2F93A, 'M', '瘐'), + (0x2F93B, 'M', '𤾡'), + (0x2F93C, 'M', '𤾸'), + (0x2F93D, 'M', '𥁄'), + (0x2F93E, 'M', '㿼'), + (0x2F93F, 'M', '䀈'), + (0x2F940, 'M', '直'), + (0x2F941, 'M', '𥃳'), + (0x2F942, 'M', '𥃲'), + (0x2F943, 'M', '𥄙'), + (0x2F944, 'M', '𥄳'), + (0x2F945, 'M', '眞'), + (0x2F946, 'M', '真'), + (0x2F948, 'M', '睊'), + (0x2F949, 'M', '䀹'), + (0x2F94A, 'M', '瞋'), + (0x2F94B, 'M', '䁆'), + (0x2F94C, 'M', '䂖'), + (0x2F94D, 'M', '𥐝'), + (0x2F94E, 'M', '硎'), + (0x2F94F, 'M', '碌'), + (0x2F950, 'M', '磌'), + (0x2F951, 'M', '䃣'), + (0x2F952, 'M', '𥘦'), + (0x2F953, 'M', '祖'), + (0x2F954, 'M', '𥚚'), + (0x2F955, 'M', '𥛅'), + (0x2F956, 'M', '福'), + (0x2F957, 'M', '秫'), + (0x2F958, 'M', '䄯'), + (0x2F959, 'M', '穀'), + (0x2F95A, 'M', '穊'), + (0x2F95B, 'M', '穏'), + (0x2F95C, 'M', '𥥼'), + (0x2F95D, 'M', '𥪧'), + (0x2F95F, 'X'), + (0x2F960, 'M', '䈂'), + (0x2F961, 'M', '𥮫'), + (0x2F962, 'M', '篆'), + (0x2F963, 'M', '築'), + (0x2F964, 'M', '䈧'), + (0x2F965, 'M', '𥲀'), + (0x2F966, 'M', '糒'), + (0x2F967, 'M', '䊠'), + (0x2F968, 'M', '糨'), + (0x2F969, 'M', '糣'), + (0x2F96A, 'M', '紀'), + (0x2F96B, 'M', '𥾆'), + (0x2F96C, 'M', '絣'), + (0x2F96D, 'M', '䌁'), + (0x2F96E, 'M', '緇'), + (0x2F96F, 'M', '縂'), + (0x2F970, 'M', '繅'), + (0x2F971, 'M', '䌴'), + ] + +def _seg_80() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F972, 'M', '𦈨'), + (0x2F973, 'M', '𦉇'), + (0x2F974, 'M', '䍙'), + (0x2F975, 'M', '𦋙'), + (0x2F976, 'M', '罺'), + (0x2F977, 'M', '𦌾'), + (0x2F978, 'M', '羕'), + (0x2F979, 'M', '翺'), + (0x2F97A, 'M', '者'), + (0x2F97B, 'M', '𦓚'), + (0x2F97C, 'M', '𦔣'), + (0x2F97D, 'M', '聠'), + (0x2F97E, 'M', '𦖨'), + (0x2F97F, 'M', '聰'), + (0x2F980, 'M', '𣍟'), + (0x2F981, 'M', '䏕'), + (0x2F982, 'M', '育'), + (0x2F983, 'M', '脃'), + (0x2F984, 'M', '䐋'), + (0x2F985, 'M', '脾'), + (0x2F986, 'M', '媵'), + (0x2F987, 'M', '𦞧'), + (0x2F988, 'M', '𦞵'), + (0x2F989, 'M', '𣎓'), + (0x2F98A, 'M', '𣎜'), + (0x2F98B, 'M', '舁'), + (0x2F98C, 'M', '舄'), + (0x2F98D, 'M', '辞'), + (0x2F98E, 'M', '䑫'), + (0x2F98F, 'M', '芑'), + (0x2F990, 'M', '芋'), + (0x2F991, 'M', '芝'), + (0x2F992, 'M', '劳'), + (0x2F993, 'M', '花'), + (0x2F994, 'M', '芳'), + (0x2F995, 'M', '芽'), + (0x2F996, 'M', '苦'), + (0x2F997, 'M', '𦬼'), + (0x2F998, 'M', '若'), + (0x2F999, 'M', '茝'), + (0x2F99A, 'M', '荣'), + (0x2F99B, 'M', '莭'), + (0x2F99C, 'M', '茣'), + (0x2F99D, 'M', '莽'), + (0x2F99E, 'M', '菧'), + (0x2F99F, 'M', '著'), + (0x2F9A0, 'M', '荓'), + (0x2F9A1, 'M', '菊'), + (0x2F9A2, 'M', '菌'), + (0x2F9A3, 'M', '菜'), + (0x2F9A4, 'M', '𦰶'), + (0x2F9A5, 'M', '𦵫'), + (0x2F9A6, 'M', '𦳕'), + (0x2F9A7, 'M', '䔫'), + (0x2F9A8, 'M', '蓱'), + (0x2F9A9, 'M', '蓳'), + (0x2F9AA, 'M', '蔖'), + (0x2F9AB, 'M', '𧏊'), + (0x2F9AC, 'M', '蕤'), + (0x2F9AD, 'M', '𦼬'), + (0x2F9AE, 'M', '䕝'), + (0x2F9AF, 'M', '䕡'), + (0x2F9B0, 'M', '𦾱'), + (0x2F9B1, 'M', '𧃒'), + (0x2F9B2, 'M', '䕫'), + (0x2F9B3, 'M', '虐'), + (0x2F9B4, 'M', '虜'), + (0x2F9B5, 'M', '虧'), + (0x2F9B6, 'M', '虩'), + (0x2F9B7, 'M', '蚩'), + (0x2F9B8, 'M', '蚈'), + (0x2F9B9, 'M', '蜎'), + (0x2F9BA, 'M', '蛢'), + (0x2F9BB, 'M', '蝹'), + (0x2F9BC, 'M', '蜨'), + (0x2F9BD, 'M', '蝫'), + (0x2F9BE, 'M', '螆'), + (0x2F9BF, 'X'), + (0x2F9C0, 'M', '蟡'), + (0x2F9C1, 'M', '蠁'), + (0x2F9C2, 'M', '䗹'), + (0x2F9C3, 'M', '衠'), + (0x2F9C4, 'M', '衣'), + (0x2F9C5, 'M', '𧙧'), + (0x2F9C6, 'M', '裗'), + (0x2F9C7, 'M', '裞'), + (0x2F9C8, 'M', '䘵'), + (0x2F9C9, 'M', '裺'), + (0x2F9CA, 'M', '㒻'), + (0x2F9CB, 'M', '𧢮'), + (0x2F9CC, 'M', '𧥦'), + (0x2F9CD, 'M', '䚾'), + (0x2F9CE, 'M', '䛇'), + (0x2F9CF, 'M', '誠'), + (0x2F9D0, 'M', '諭'), + (0x2F9D1, 'M', '變'), + (0x2F9D2, 'M', '豕'), + (0x2F9D3, 'M', '𧲨'), + (0x2F9D4, 'M', '貫'), + (0x2F9D5, 'M', '賁'), + ] + +def _seg_81() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F9D6, 'M', '贛'), + (0x2F9D7, 'M', '起'), + (0x2F9D8, 'M', '𧼯'), + (0x2F9D9, 'M', '𠠄'), + (0x2F9DA, 'M', '跋'), + (0x2F9DB, 'M', '趼'), + (0x2F9DC, 'M', '跰'), + (0x2F9DD, 'M', '𠣞'), + (0x2F9DE, 'M', '軔'), + (0x2F9DF, 'M', '輸'), + (0x2F9E0, 'M', '𨗒'), + (0x2F9E1, 'M', '𨗭'), + (0x2F9E2, 'M', '邔'), + (0x2F9E3, 'M', '郱'), + (0x2F9E4, 'M', '鄑'), + (0x2F9E5, 'M', '𨜮'), + (0x2F9E6, 'M', '鄛'), + (0x2F9E7, 'M', '鈸'), + (0x2F9E8, 'M', '鋗'), + (0x2F9E9, 'M', '鋘'), + (0x2F9EA, 'M', '鉼'), + (0x2F9EB, 'M', '鏹'), + (0x2F9EC, 'M', '鐕'), + (0x2F9ED, 'M', '𨯺'), + (0x2F9EE, 'M', '開'), + (0x2F9EF, 'M', '䦕'), + (0x2F9F0, 'M', '閷'), + (0x2F9F1, 'M', '𨵷'), + (0x2F9F2, 'M', '䧦'), + (0x2F9F3, 'M', '雃'), + (0x2F9F4, 'M', '嶲'), + (0x2F9F5, 'M', '霣'), + (0x2F9F6, 'M', '𩅅'), + (0x2F9F7, 'M', '𩈚'), + (0x2F9F8, 'M', '䩮'), + (0x2F9F9, 'M', '䩶'), + (0x2F9FA, 'M', '韠'), + (0x2F9FB, 'M', '𩐊'), + (0x2F9FC, 'M', '䪲'), + (0x2F9FD, 'M', '𩒖'), + (0x2F9FE, 'M', '頋'), + (0x2FA00, 'M', '頩'), + (0x2FA01, 'M', '𩖶'), + (0x2FA02, 'M', '飢'), + (0x2FA03, 'M', '䬳'), + (0x2FA04, 'M', '餩'), + (0x2FA05, 'M', '馧'), + (0x2FA06, 'M', '駂'), + (0x2FA07, 'M', '駾'), + (0x2FA08, 'M', '䯎'), + (0x2FA09, 'M', '𩬰'), + (0x2FA0A, 'M', '鬒'), + (0x2FA0B, 'M', '鱀'), + (0x2FA0C, 'M', '鳽'), + (0x2FA0D, 'M', '䳎'), + (0x2FA0E, 'M', '䳭'), + (0x2FA0F, 'M', '鵧'), + (0x2FA10, 'M', '𪃎'), + (0x2FA11, 'M', '䳸'), + (0x2FA12, 'M', '𪄅'), + (0x2FA13, 'M', '𪈎'), + (0x2FA14, 'M', '𪊑'), + (0x2FA15, 'M', '麻'), + (0x2FA16, 'M', '䵖'), + (0x2FA17, 'M', '黹'), + (0x2FA18, 'M', '黾'), + (0x2FA19, 'M', '鼅'), + (0x2FA1A, 'M', '鼏'), + (0x2FA1B, 'M', '鼖'), + (0x2FA1C, 'M', '鼻'), + (0x2FA1D, 'M', '𪘀'), + (0x2FA1E, 'X'), + (0x30000, 'V'), + (0x3134B, 'X'), + (0x31350, 'V'), + (0x323B0, 'X'), + (0xE0100, 'I'), + (0xE01F0, 'X'), + ] + +uts46data = tuple( + _seg_0() + + _seg_1() + + _seg_2() + + _seg_3() + + _seg_4() + + _seg_5() + + _seg_6() + + _seg_7() + + _seg_8() + + _seg_9() + + _seg_10() + + _seg_11() + + _seg_12() + + _seg_13() + + _seg_14() + + _seg_15() + + _seg_16() + + _seg_17() + + _seg_18() + + _seg_19() + + _seg_20() + + _seg_21() + + _seg_22() + + _seg_23() + + _seg_24() + + _seg_25() + + _seg_26() + + _seg_27() + + _seg_28() + + _seg_29() + + _seg_30() + + _seg_31() + + _seg_32() + + _seg_33() + + _seg_34() + + _seg_35() + + _seg_36() + + _seg_37() + + _seg_38() + + _seg_39() + + _seg_40() + + _seg_41() + + _seg_42() + + _seg_43() + + _seg_44() + + _seg_45() + + _seg_46() + + _seg_47() + + _seg_48() + + _seg_49() + + _seg_50() + + _seg_51() + + _seg_52() + + _seg_53() + + _seg_54() + + _seg_55() + + _seg_56() + + _seg_57() + + _seg_58() + + _seg_59() + + _seg_60() + + _seg_61() + + _seg_62() + + _seg_63() + + _seg_64() + + _seg_65() + + _seg_66() + + _seg_67() + + _seg_68() + + _seg_69() + + _seg_70() + + _seg_71() + + _seg_72() + + _seg_73() + + _seg_74() + + _seg_75() + + _seg_76() + + _seg_77() + + _seg_78() + + _seg_79() + + _seg_80() + + _seg_81() +) # type: Tuple[Union[Tuple[int, str], Tuple[int, str, str]], ...] diff --git a/konfig.py b/konfig.py new file mode 100644 index 0000000..cd3857f --- /dev/null +++ b/konfig.py @@ -0,0 +1,62 @@ +"""Zugangsdaten aus config.ini. + +Passwoerter standen frueher im Quelltext - in solarManager.py, zeit.py, +gatherWaterData.py und wecker.py jeweils noch einmal. Beim Anlegen des +Repositorys waeren sie damit dauerhaft in der Historie gelandet, und genau das +musste beim Web-Repository schon einmal muehsam rueckgaengig gemacht werden. + +Jetzt steht alles in config.ini, die nicht mit eingecheckt wird. Was dort +hineingehoert, zeigt config.ini.example - beim Aufsetzen kopieren und +ausfuellen. Dasselbe Verfahren benutzt der AutoAction-Runner unter +autoActions/ schon laenger. + + import konfig + zug = konfig.datenbank() # dict fuer mysql.connector.connect + with connect(**zug) as c: + ... + + pw = konfig.wert("wattpilot", "password") +""" +import configparser +import os + +_PFAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.ini") +_konf = None + + +def _lesen(): + """Die Datei einmal einlesen und behalten.""" + global _konf + if _konf is None: + _konf = configparser.ConfigParser(interpolation=None) # Prozentzeichen im Passwort + if not _konf.read(_PFAD, encoding="utf-8"): + raise SystemExit( + "%s fehlt - config.ini.example kopieren und ausfuellen." % _PFAD) + return _konf + + +def wert(abschnitt, schluessel, vorgabe=None): + """Ein einzelner Eintrag. Ohne Vorgabe ist ein fehlender Eintrag ein Fehler. + + Lieber hier laut abbrechen als spaeter mit einem leeren Passwort an der + Datenbank scheitern - der Grund waere dann nicht mehr zu sehen. + """ + konf = _lesen() + if konf.has_option(abschnitt, schluessel): + return konf.get(abschnitt, schluessel) + if vorgabe is not None: + return vorgabe + raise SystemExit("config.ini: [%s] %s fehlt." % (abschnitt, schluessel)) + + +def datenbank(abschnitt="database"): + """Verbindungsdaten als dict, direkt fuer mysql.connector.connect geeignet. + + Der Port bleibt Text, weil connect() beides nimmt und der Aufrufer ihn + frueher auch als Text stehen hatte. + """ + return {"host": wert(abschnitt, "host", "localhost"), + "port": wert(abschnitt, "port", "3310"), + "user": wert(abschnitt, "user"), + "password": wert(abschnitt, "password"), + "database": wert(abschnitt, "database")} diff --git a/logging.ini b/logging.ini new file mode 100644 index 0000000..46248d6 --- /dev/null +++ b/logging.ini @@ -0,0 +1,74 @@ +[loggers] +keys=root,main,websockets,Wattpilot,gatherOpenDTUData,gatherDTUBIData,gatherModbusData,charger_goE,gatherWaterData,gatherHeaterData + +[handlers] +keys=consoleHandler + +[formatters] +keys=defaultFormatter + +[logger_root] +handlers=consoleHandler +level=INFO + +[logger_websockets] +handlers=consoleHandler +level=WARNING +qualname=websockets +propagate=0 + +[logger_Wattpilot] +handlers=consoleHandler +level=WARNING +qualname=Wattpilot +propagate=0 + +[logger_gatherOpenDTUData] +handlers=consoleHandler +level=DEBUG +qualname=gatherOpenDTUData +propagate=0 + +[logger_gatherDTUBIData] +handlers=consoleHandler +level=DEBUG +qualname=gatherDTUBIData +propagate=0 + +[logger_gatherModbusData] +handlers=consoleHandler +level=DEBUG +qualname=gatherModbusData +propagate=0 + +[logger_charger_goE] +handlers=consoleHandler +level=DEBUG +qualname=charger_goE +propagate=0 + +[logger_gatherWaterData] +handlers=consoleHandler +level=DEBUG +qualname=gatherWaterData +propagate=0 + +[logger_gatherHeaterData] +handlers=consoleHandler +level=DEBUG +qualname=gatherHeaterData +propagate=0 + +[handler_consoleHandler] +class=logging.StreamHandler +formatter=defaultFormatter +args=(sys.stdout,) + +[formatter_defaultFormatter] +format=%(levelname)s %(asctime)s %(pathname)s %(filename)s - %(message)s + +[logger_main] +handlers=consoleHandler +level=INFO +qualname=__main__ +propagate=0 \ No newline at end of file diff --git a/mqttClient.py b/mqttClient.py new file mode 100644 index 0000000..6c3128d --- /dev/null +++ b/mqttClient.py @@ -0,0 +1,89 @@ +import sys +import dataclasses +import logging +import json +sys.path.append("./") +import paho.mqtt.client as mqtt +logging.config.fileConfig('./logging.ini') +_LOGGER = logging.getLogger() + +class EnhancedJSONEncoder(json.JSONEncoder): + def default(self, o): + if dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + return super().default(o) + +unacked_publish = set() + + +mqttClient=mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) +#mqttClient.on_publish = on_publish +mqttClient.user_data_set(unacked_publish) + +logging.basicConfig( + format="%(asctime)s %(message)s", + level=logging.WARN, +) + +def publish(dict,topic="solarManager"): + if dataclasses.is_dataclass(dict): + for field in dataclasses.fields(dict): + key = field.name + value = getattr(dict, field.name) + if hasattr(value, '__len__') and (not isinstance(value, str)): + if dataclasses.is_dataclass(value): #make subtopics of dataclasses + publish(value,topic+"/"+key) + else: + #try: #make subtopics of dataclass arrays + if len(value): + if dataclasses.is_dataclass(value[0]): + i=0 + for d in value: + publish(d,topic+"/"+key+str(i)) + i=i+1 + #except: + else: + mqttClient.publish(topic+"/"+key,json.dumps(value, cls=EnhancedJSONEncoder)) + else: #output empty array anyway to keep data consistent + mqttClient.publish(topic+"/"+key,json.dumps(value, cls=EnhancedJSONEncoder)) + else: + try: + flt = float(value) + mqttClient.publish(topic+"/"+key,round(flt,2)) + except ValueError: + mqttClient.publish(topic+"/"+key,value) + else: + for key, value in dict.items(): + if hasattr(value, '__len__') and (not isinstance(value, str)): + if dataclasses.is_dataclass(value): #make subtopics of dataclasses + publish(value,topic+"/"+key) + else: + #try: #make subtopics of dataclass arrays + if len(value): + if dataclasses.is_dataclass(value[0]): + i=0 + for d in value: + publish(d,topic+"/"+key+str(i)) + i=i+1 + #except: + else: + mqttClient.publish(topic+"/"+key,json.dumps(value, cls=EnhancedJSONEncoder)) + else: #output empty array anyway to keep data consistent + mqttClient.publish(topic+"/"+key,json.dumps(value, cls=EnhancedJSONEncoder)) + else: + try: + flt = float(value) + mqttClient.publish(topic+"/"+key,round(flt,2)) + except ValueError: + mqttClient.publish(topic+"/"+key,value) + except: + _LOGGER.error(f"value is no number") + + #print(message) + +def startMqttClient(): + mqttClient.connect("localhost", 1883, 60) + mqttClient.loop_start() + #await asyncio.Future() # run forever +def closeMqttClient(): + mqttClient.disconnect() \ No newline at end of file diff --git a/multidict/__init__.py b/multidict/__init__.py new file mode 100644 index 0000000..d9ea722 --- /dev/null +++ b/multidict/__init__.py @@ -0,0 +1,48 @@ +"""Multidict implementation. + +HTTP Headers and URL query string require specific data structure: +multidict. It behaves mostly like a dict but it can have +several values for the same key. +""" + +from ._abc import MultiMapping, MutableMultiMapping +from ._compat import USE_EXTENSIONS + +__all__ = ( + "MultiMapping", + "MutableMultiMapping", + "MultiDictProxy", + "CIMultiDictProxy", + "MultiDict", + "CIMultiDict", + "upstr", + "istr", + "getversion", +) + +__version__ = "6.0.4" + + +try: + if not USE_EXTENSIONS: + raise ImportError + from ._multidict import ( + CIMultiDict, + CIMultiDictProxy, + MultiDict, + MultiDictProxy, + getversion, + istr, + ) +except ImportError: # pragma: no cover + from ._multidict_py import ( + CIMultiDict, + CIMultiDictProxy, + MultiDict, + MultiDictProxy, + getversion, + istr, + ) + + +upstr = istr diff --git a/multidict/__init__.pyi b/multidict/__init__.pyi new file mode 100644 index 0000000..bbc5a5c --- /dev/null +++ b/multidict/__init__.pyi @@ -0,0 +1,150 @@ +import abc +from typing import ( + Generic, + Iterable, + Iterator, + Mapping, + MutableMapping, + TypeVar, + overload, +) + +class istr(str): ... + +upstr = istr + +_S = str | istr + +_T = TypeVar("_T") + +_T_co = TypeVar("_T_co", covariant=True) + +_D = TypeVar("_D") + +class MultiMapping(Mapping[_S, _T_co]): + @overload + @abc.abstractmethod + def getall(self, key: _S) -> list[_T_co]: ... + @overload + @abc.abstractmethod + def getall(self, key: _S, default: _D) -> list[_T_co] | _D: ... + @overload + @abc.abstractmethod + def getone(self, key: _S) -> _T_co: ... + @overload + @abc.abstractmethod + def getone(self, key: _S, default: _D) -> _T_co | _D: ... + +_Arg = (Mapping[str, _T] | Mapping[istr, _T] | dict[str, _T] + | dict[istr, _T] | MultiMapping[_T] + | Iterable[tuple[str, _T]] | Iterable[tuple[istr, _T]]) + +class MutableMultiMapping(MultiMapping[_T], MutableMapping[_S, _T], Generic[_T]): + @abc.abstractmethod + def add(self, key: _S, value: _T) -> None: ... + @abc.abstractmethod + def extend(self, arg: _Arg[_T] = ..., **kwargs: _T) -> None: ... + @overload + @abc.abstractmethod + def popone(self, key: _S) -> _T: ... + @overload + @abc.abstractmethod + def popone(self, key: _S, default: _D) -> _T | _D: ... + @overload + @abc.abstractmethod + def popall(self, key: _S) -> list[_T]: ... + @overload + @abc.abstractmethod + def popall(self, key: _S, default: _D) -> list[_T] | _D: ... + +class MultiDict(MutableMultiMapping[_T], Generic[_T]): + def __init__(self, arg: _Arg[_T] = ..., **kwargs: _T) -> None: ... + def copy(self) -> MultiDict[_T]: ... + def __getitem__(self, k: _S) -> _T: ... + def __setitem__(self, k: _S, v: _T) -> None: ... + def __delitem__(self, v: _S) -> None: ... + def __iter__(self) -> Iterator[_S]: ... + def __len__(self) -> int: ... + @overload + def getall(self, key: _S) -> list[_T]: ... + @overload + def getall(self, key: _S, default: _D) -> list[_T] | _D: ... + @overload + def getone(self, key: _S) -> _T: ... + @overload + def getone(self, key: _S, default: _D) -> _T | _D: ... + def add(self, key: _S, value: _T) -> None: ... + def extend(self, arg: _Arg[_T] = ..., **kwargs: _T) -> None: ... + @overload + def popone(self, key: _S) -> _T: ... + @overload + def popone(self, key: _S, default: _D) -> _T | _D: ... + @overload + def popall(self, key: _S) -> list[_T]: ... + @overload + def popall(self, key: _S, default: _D) -> list[_T] | _D: ... + +class CIMultiDict(MutableMultiMapping[_T], Generic[_T]): + def __init__(self, arg: _Arg[_T] = ..., **kwargs: _T) -> None: ... + def copy(self) -> CIMultiDict[_T]: ... + def __getitem__(self, k: _S) -> _T: ... + def __setitem__(self, k: _S, v: _T) -> None: ... + def __delitem__(self, v: _S) -> None: ... + def __iter__(self) -> Iterator[_S]: ... + def __len__(self) -> int: ... + @overload + def getall(self, key: _S) -> list[_T]: ... + @overload + def getall(self, key: _S, default: _D) -> list[_T] | _D: ... + @overload + def getone(self, key: _S) -> _T: ... + @overload + def getone(self, key: _S, default: _D) -> _T | _D: ... + def add(self, key: _S, value: _T) -> None: ... + def extend(self, arg: _Arg[_T] = ..., **kwargs: _T) -> None: ... + @overload + def popone(self, key: _S) -> _T: ... + @overload + def popone(self, key: _S, default: _D) -> _T | _D: ... + @overload + def popall(self, key: _S) -> list[_T]: ... + @overload + def popall(self, key: _S, default: _D) -> list[_T] | _D: ... + +class MultiDictProxy(MultiMapping[_T], Generic[_T]): + def __init__( + self, arg: MultiMapping[_T] | MutableMultiMapping[_T] + ) -> None: ... + def copy(self) -> MultiDict[_T]: ... + def __getitem__(self, k: _S) -> _T: ... + def __iter__(self) -> Iterator[_S]: ... + def __len__(self) -> int: ... + @overload + def getall(self, key: _S) -> list[_T]: ... + @overload + def getall(self, key: _S, default: _D) -> list[_T] | _D: ... + @overload + def getone(self, key: _S) -> _T: ... + @overload + def getone(self, key: _S, default: _D) -> _T | _D: ... + +class CIMultiDictProxy(MultiMapping[_T], Generic[_T]): + def __init__( + self, arg: MultiMapping[_T] | MutableMultiMapping[_T] + ) -> None: ... + def __getitem__(self, k: _S) -> _T: ... + def __iter__(self) -> Iterator[_S]: ... + def __len__(self) -> int: ... + @overload + def getall(self, key: _S) -> list[_T]: ... + @overload + def getall(self, key: _S, default: _D) -> list[_T] | _D: ... + @overload + def getone(self, key: _S) -> _T: ... + @overload + def getone(self, key: _S, default: _D) -> _T | _D: ... + def copy(self) -> CIMultiDict[_T]: ... + +def getversion( + md: MultiDict[_T] | CIMultiDict[_T] | MultiDictProxy[_T] | CIMultiDictProxy[_T] +) -> int: ... diff --git a/multidict/_abc.py b/multidict/_abc.py new file mode 100644 index 0000000..0603cdd --- /dev/null +++ b/multidict/_abc.py @@ -0,0 +1,48 @@ +import abc +import sys +import types +from collections.abc import Mapping, MutableMapping + + +class _TypingMeta(abc.ABCMeta): + # A fake metaclass to satisfy typing deps in runtime + # basically MultiMapping[str] and other generic-like type instantiations + # are emulated. + # Note: real type hints are provided by __init__.pyi stub file + if sys.version_info >= (3, 9): + + def __getitem__(self, key): + return types.GenericAlias(self, key) + + else: + + def __getitem__(self, key): + return self + + +class MultiMapping(Mapping, metaclass=_TypingMeta): + @abc.abstractmethod + def getall(self, key, default=None): + raise KeyError + + @abc.abstractmethod + def getone(self, key, default=None): + raise KeyError + + +class MutableMultiMapping(MultiMapping, MutableMapping): + @abc.abstractmethod + def add(self, key, value): + raise NotImplementedError + + @abc.abstractmethod + def extend(self, *args, **kwargs): + raise NotImplementedError + + @abc.abstractmethod + def popone(self, key, default=None): + raise KeyError + + @abc.abstractmethod + def popall(self, key, default=None): + raise KeyError diff --git a/multidict/_compat.py b/multidict/_compat.py new file mode 100644 index 0000000..d1ff392 --- /dev/null +++ b/multidict/_compat.py @@ -0,0 +1,14 @@ +import os +import platform + +NO_EXTENSIONS = bool(os.environ.get("MULTIDICT_NO_EXTENSIONS")) + +PYPY = platform.python_implementation() == "PyPy" + +USE_EXTENSIONS = not NO_EXTENSIONS and not PYPY + +if USE_EXTENSIONS: + try: + from . import _multidict # noqa + except ImportError: + USE_EXTENSIONS = False diff --git a/multidict/_multidict.c b/multidict/_multidict.c new file mode 100644 index 0000000..1ba79df --- /dev/null +++ b/multidict/_multidict.c @@ -0,0 +1,1824 @@ +#include "Python.h" +#include "structmember.h" + +// Include order important +#include "_multilib/defs.h" +#include "_multilib/istr.h" +#include "_multilib/pair_list.h" +#include "_multilib/dict.h" +#include "_multilib/iter.h" +#include "_multilib/views.h" + +#ifndef _PyArg_UnpackKeywords +#define FASTCALL_OLD +#endif + + +static PyObject *collections_abc_mapping; +static PyObject *collections_abc_mut_mapping; +static PyObject *collections_abc_mut_multi_mapping; + +static PyTypeObject multidict_type; +static PyTypeObject cimultidict_type; +static PyTypeObject multidict_proxy_type; +static PyTypeObject cimultidict_proxy_type; + +static PyObject *repr_func; + +#define MultiDict_CheckExact(o) (Py_TYPE(o) == &multidict_type) +#define CIMultiDict_CheckExact(o) (Py_TYPE(o) == &cimultidict_type) +#define MultiDictProxy_CheckExact(o) (Py_TYPE(o) == &multidict_proxy_type) +#define CIMultiDictProxy_CheckExact(o) (Py_TYPE(o) == &cimultidict_proxy_type) + +/* Helper macro for something like isinstance(obj, Base) */ +#define _MultiDict_Check(o) \ + ((MultiDict_CheckExact(o)) || \ + (CIMultiDict_CheckExact(o)) || \ + (MultiDictProxy_CheckExact(o)) || \ + (CIMultiDictProxy_CheckExact(o))) + +/******************** Internal Methods ********************/ + +/* Forward declaration */ +static PyObject *multidict_items(MultiDictObject *self); + +static inline PyObject * +_multidict_getone(MultiDictObject *self, PyObject *key, PyObject *_default) +{ + PyObject *val = pair_list_get_one(&self->pairs, key); + + if (val == NULL && + PyErr_ExceptionMatches(PyExc_KeyError) && + _default != NULL) + { + PyErr_Clear(); + Py_INCREF(_default); + return _default; + } + + return val; +} + +static inline int +_multidict_eq(MultiDictObject *self, MultiDictObject *other) +{ + Py_ssize_t pos1 = 0, + pos2 = 0; + + Py_hash_t h1 = 0, + h2 = 0; + + PyObject *identity1 = NULL, + *identity2 = NULL, + *value1 = NULL, + *value2 = NULL; + + int cmp_identity = 0, + cmp_value = 0; + + if (self == other) { + return 1; + } + + if (pair_list_len(&self->pairs) != pair_list_len(&other->pairs)) { + return 0; + } + + while (_pair_list_next(&self->pairs, &pos1, &identity1, NULL, &value1, &h1) && + _pair_list_next(&other->pairs, &pos2, &identity2, NULL, &value2, &h2)) + { + if (h1 != h2) { + return 0; + } + cmp_identity = PyObject_RichCompareBool(identity1, identity2, Py_NE); + if (cmp_identity < 0) { + return -1; + } + cmp_value = PyObject_RichCompareBool(value1, value2, Py_NE); + if (cmp_value < 0) { + return -1; + } + if (cmp_identity || cmp_value) { + return 0; + } + } + + return 1; +} + +static inline int +_multidict_update_items(MultiDictObject *self, pair_list_t *pairs) +{ + return pair_list_update(&self->pairs, pairs); +} + +static inline int +_multidict_append_items(MultiDictObject *self, pair_list_t *pairs) +{ + PyObject *key = NULL, + *value = NULL; + + Py_ssize_t pos = 0; + + while (_pair_list_next(pairs, &pos, NULL, &key, &value, NULL)) { + if (pair_list_add(&self->pairs, key, value) < 0) { + return -1; + } + } + + return 0; +} + +static inline int +_multidict_append_items_seq(MultiDictObject *self, PyObject *arg, + const char *name) +{ + PyObject *key = NULL, + *value = NULL, + *item = NULL, + *iter = PyObject_GetIter(arg); + + if (iter == NULL) { + return -1; + } + + while ((item = PyIter_Next(iter)) != NULL) { + if (PyTuple_CheckExact(item)) { + if (PyTuple_GET_SIZE(item) != 2) { + goto invalid_type; + } + key = PyTuple_GET_ITEM(item, 0); + Py_INCREF(key); + value = PyTuple_GET_ITEM(item, 1); + Py_INCREF(value); + } + else if (PyList_CheckExact(item)) { + if (PyList_GET_SIZE(item) != 2) { + goto invalid_type; + } + key = PyList_GET_ITEM(item, 0); + Py_INCREF(key); + value = PyList_GET_ITEM(item, 1); + Py_INCREF(value); + } + else if (PySequence_Check(item)) { + if (PySequence_Size(item) != 2) { + goto invalid_type; + } + key = PySequence_GetItem(item, 0); + value = PySequence_GetItem(item, 1); + } else { + goto invalid_type; + } + + if (pair_list_add(&self->pairs, key, value) < 0) { + goto fail; + } + Py_CLEAR(key); + Py_CLEAR(value); + Py_CLEAR(item); + } + + Py_DECREF(iter); + + if (PyErr_Occurred()) { + return -1; + } + + return 0; +invalid_type: + PyErr_Format( + PyExc_TypeError, + "%s takes either dict or list of (key, value) pairs", + name, + NULL + ); + goto fail; +fail: + Py_XDECREF(key); + Py_XDECREF(value); + Py_XDECREF(item); + Py_DECREF(iter); + return -1; +} + +static inline int +_multidict_list_extend(PyObject *list, PyObject *target_list) +{ + PyObject *item = NULL, + *iter = PyObject_GetIter(target_list); + + if (iter == NULL) { + return -1; + } + + while ((item = PyIter_Next(iter)) != NULL) { + if (PyList_Append(list, item) < 0) { + Py_DECREF(item); + Py_DECREF(iter); + return -1; + } + Py_DECREF(item); + } + + Py_DECREF(iter); + + if (PyErr_Occurred()) { + return -1; + } + + return 0; +} + +static inline int +_multidict_extend_with_args(MultiDictObject *self, PyObject *arg, + PyObject *kwds, const char *name, int do_add) +{ + PyObject *arg_items = NULL, /* tracked by GC */ + *kwds_items = NULL; /* new reference */ + pair_list_t *pairs = NULL; + + int err = 0; + + if (kwds && !PyArg_ValidateKeywordArguments(kwds)) { + return -1; + } + + // TODO: mb can be refactored more clear + if (_MultiDict_Check(arg) && kwds == NULL) { + if (MultiDict_CheckExact(arg) || CIMultiDict_CheckExact(arg)) { + pairs = &((MultiDictObject*)arg)->pairs; + } else if (MultiDictProxy_CheckExact(arg) || CIMultiDictProxy_CheckExact(arg)) { + pairs = &((MultiDictProxyObject*)arg)->md->pairs; + } + + if (do_add) { + return _multidict_append_items(self, pairs); + } + + return _multidict_update_items(self, pairs); + } + + if (PyObject_HasAttrString(arg, "items")) { + if (_MultiDict_Check(arg)) { + arg_items = multidict_items((MultiDictObject*)arg); + } else { + arg_items = PyMapping_Items(arg); + } + if (arg_items == NULL) { + return -1; + } + } else { + arg_items = arg; + Py_INCREF(arg_items); + } + + if (kwds) { + PyObject *tmp = PySequence_List(arg_items); + Py_DECREF(arg_items); + arg_items = tmp; + if (arg_items == NULL) { + return -1; + } + + kwds_items = PyDict_Items(kwds); + if (kwds_items == NULL) { + Py_DECREF(arg_items); + return -1; + } + err = _multidict_list_extend(arg_items, kwds_items); + Py_DECREF(kwds_items); + if (err < 0) { + Py_DECREF(arg_items); + return -1; + } + } + + if (do_add) { + err = _multidict_append_items_seq(self, arg_items, name); + } else { + err = pair_list_update_from_seq(&self->pairs, arg_items); + } + + Py_DECREF(arg_items); + + return err; +} + +static inline int +_multidict_extend_with_kwds(MultiDictObject *self, PyObject *kwds, + const char *name, int do_add) +{ + PyObject *arg = NULL; + + int err = 0; + + if (!PyArg_ValidateKeywordArguments(kwds)) { + return -1; + } + + arg = PyDict_Items(kwds); + if (do_add) { + err = _multidict_append_items_seq(self, arg, name); + } else { + err = pair_list_update_from_seq(&self->pairs, arg); + } + + Py_DECREF(arg); + return err; +} + +static inline int +_multidict_extend(MultiDictObject *self, PyObject *args, PyObject *kwds, + const char *name, int do_add) +{ + PyObject *arg = NULL; + + if (args && PyObject_Length(args) > 1) { + PyErr_Format( + PyExc_TypeError, + "%s takes at most 1 positional argument (%zd given)", + name, PyObject_Length(args), NULL + ); + return -1; + } + + if (args && PyObject_Length(args) > 0) { + if (!PyArg_UnpackTuple(args, name, 0, 1, &arg)) { + return -1; + } + if (_multidict_extend_with_args(self, arg, kwds, name, do_add) < 0) { + return -1; + } + } else if (kwds && PyObject_Length(kwds) > 0) { + if (_multidict_extend_with_kwds(self, kwds, name, do_add) < 0) { + return -1; + } + } + + return 0; +} + +static inline PyObject * +_multidict_copy(MultiDictObject *self, PyTypeObject *multidict_tp_object) +{ + MultiDictObject *new_multidict = NULL; + + PyObject *arg_items = NULL, + *items = NULL; + + new_multidict = (MultiDictObject*)PyType_GenericNew( + multidict_tp_object, NULL, NULL); + if (new_multidict == NULL) { + return NULL; + } + + if (multidict_tp_object->tp_init( + (PyObject*)new_multidict, NULL, NULL) < 0) + { + return NULL; + } + + items = multidict_items(self); + if (items == NULL) { + goto fail; + } + + // TODO: "Implementation looks as slow as possible ..." + arg_items = PyTuple_New(1); + if (arg_items == NULL) { + goto fail; + } + + Py_INCREF(items); + PyTuple_SET_ITEM(arg_items, 0, items); + + if (_multidict_extend( + new_multidict, arg_items, NULL, "copy", 1) < 0) + { + goto fail; + } + + Py_DECREF(items); + Py_DECREF(arg_items); + + return (PyObject*)new_multidict; + +fail: + Py_XDECREF(items); + Py_XDECREF(arg_items); + + Py_DECREF(new_multidict); + + return NULL; +} + +static inline PyObject * +_multidict_proxy_copy(MultiDictProxyObject *self, PyTypeObject *type) +{ + PyObject *new_multidict = PyType_GenericNew(type, NULL, NULL); + if (new_multidict == NULL) { + goto fail; + } + if (type->tp_init(new_multidict, NULL, NULL) < 0) { + goto fail; + } + if (_multidict_extend_with_args( + (MultiDictObject*)new_multidict, (PyObject*)self, NULL, "copy", 1) < 0) + { + goto fail; + } + + return new_multidict; + +fail: + Py_XDECREF(new_multidict); + return NULL; +} + + +/******************** Base Methods ********************/ + +static inline PyObject * +multidict_getall(MultiDictObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *list = NULL, + *key = NULL, + *_default = NULL; + + static const char * const _keywords[] = {"key", "default", NULL}; +#ifdef FASTCALL_OLD + static _PyArg_Parser _parser = {"O|O:getall", _keywords, 0}; + if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser, + &key, &_default)) { + return NULL; + } +#else + static _PyArg_Parser _parser = {NULL, _keywords, "getall", 0}; + PyObject *argsbuf[2]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, 1, 2, 0, argsbuf); + if (!args) { + return NULL; + } + key = args[0]; + if (!noptargs) { + goto skip_optional_pos; + } + + _default = args[1]; +skip_optional_pos: +#endif + list = pair_list_get_all(&self->pairs, key); + + if (list == NULL && + PyErr_ExceptionMatches(PyExc_KeyError) && + _default != NULL) + { + PyErr_Clear(); + Py_INCREF(_default); + return _default; + } + + return list; +} + +static inline PyObject * +multidict_getone(MultiDictObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *key = NULL, + *_default = NULL; + + static const char * const _keywords[] = {"key", "default", NULL}; +#ifdef FASTCALL_OLD + static _PyArg_Parser _parser = {"O|O:getone", _keywords, 0}; + if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser, + &key, &_default)) { + return NULL; + } +#else + static _PyArg_Parser _parser = {NULL, _keywords, "getone", 0}; + PyObject *argsbuf[2]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, 1, 2, 0, argsbuf); + if (!args) { + return NULL; + } + key = args[0]; + if (!noptargs) { + goto skip_optional_pos; + } + + _default = args[1]; +skip_optional_pos: +#endif + return _multidict_getone(self, key, _default); +} + +static inline PyObject * +multidict_get(MultiDictObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *key = NULL, + *_default = Py_None, + *ret; + + static const char * const _keywords[] = {"key", "default", NULL}; +#ifdef FASTCALL_OLD + static _PyArg_Parser _parser = {"O|O:get", _keywords, 0}; + if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser, + &key, &_default)) { + return NULL; + } +#else + static _PyArg_Parser _parser = {NULL, _keywords, "get", 0}; + PyObject *argsbuf[2]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, 1, 2, 0, argsbuf); + if (!args) { + return NULL; + } + key = args[0]; + if (!noptargs) { + goto skip_optional_pos; + } + + _default = args[1]; +skip_optional_pos: +#endif + ret = _multidict_getone(self, key, _default); + return ret; +} + +static inline PyObject * +multidict_keys(MultiDictObject *self) +{ + return multidict_keysview_new((PyObject*)self); +} + +static inline PyObject * +multidict_items(MultiDictObject *self) +{ + return multidict_itemsview_new((PyObject*)self); +} + +static inline PyObject * +multidict_values(MultiDictObject *self) +{ + return multidict_valuesview_new((PyObject*)self); +} + +static inline PyObject * +multidict_reduce(MultiDictObject *self) +{ + PyObject *items = NULL, + *items_list = NULL, + *args = NULL, + *result = NULL; + + items = multidict_items(self); + if (items == NULL) { + goto ret; + } + + items_list = PySequence_List(items); + if (items_list == NULL) { + goto ret; + } + + args = PyTuple_Pack(1, items_list); + if (args == NULL) { + goto ret; + } + + result = PyTuple_Pack(2, Py_TYPE(self), args); + +ret: + Py_XDECREF(args); + Py_XDECREF(items_list); + Py_XDECREF(items); + + return result; +} + +static inline PyObject * +multidict_repr(PyObject *self) +{ + return PyObject_CallFunctionObjArgs( + repr_func, self, NULL); +} + +static inline Py_ssize_t +multidict_mp_len(MultiDictObject *self) +{ + return pair_list_len(&self->pairs); +} + +static inline PyObject * +multidict_mp_subscript(MultiDictObject *self, PyObject *key) +{ + return _multidict_getone(self, key, NULL); +} + +static inline int +multidict_mp_as_subscript(MultiDictObject *self, PyObject *key, PyObject *val) +{ + if (val == NULL) { + return pair_list_del(&self->pairs, key); + } else { + return pair_list_replace(&self->pairs, key, val); + } +} + +static inline int +multidict_sq_contains(MultiDictObject *self, PyObject *key) +{ + return pair_list_contains(&self->pairs, key); +} + +static inline PyObject * +multidict_tp_iter(MultiDictObject *self) +{ + return multidict_keys_iter_new(self); +} + +static inline PyObject * +multidict_tp_richcompare(PyObject *self, PyObject *other, int op) +{ + // TODO: refactoring me with love + + int cmp = 0; + + if (op != Py_EQ && op != Py_NE) { + Py_RETURN_NOTIMPLEMENTED; + } + + if (MultiDict_CheckExact(other) || CIMultiDict_CheckExact(other)) { + cmp = _multidict_eq( + (MultiDictObject*)self, + (MultiDictObject*)other + ); + if (cmp < 0) { + return NULL; + } + if (op == Py_NE) { + cmp = !cmp; + } + return PyBool_FromLong(cmp); + } + + if (MultiDictProxy_CheckExact(other) || CIMultiDictProxy_CheckExact(other)) { + cmp = _multidict_eq( + (MultiDictObject*)self, + ((MultiDictProxyObject*)other)->md + ); + if (cmp < 0) { + return NULL; + } + if (op == Py_NE) { + cmp = !cmp; + } + return PyBool_FromLong(cmp); + } + + cmp = PyObject_IsInstance(other, (PyObject*)collections_abc_mapping); + if (cmp < 0) { + return NULL; + } + + if (cmp) { + cmp = pair_list_eq_to_mapping(&((MultiDictObject*)self)->pairs, other); + if (cmp < 0) { + return NULL; + } + if (op == Py_NE) { + cmp = !cmp; + } + return PyBool_FromLong(cmp); + } + + Py_RETURN_NOTIMPLEMENTED; +} + +static inline void +multidict_tp_dealloc(MultiDictObject *self) +{ + PyObject_GC_UnTrack(self); + Py_TRASHCAN_SAFE_BEGIN(self); + if (self->weaklist != NULL) { + PyObject_ClearWeakRefs((PyObject *)self); + }; + pair_list_dealloc(&self->pairs); + Py_TYPE(self)->tp_free((PyObject *)self); + Py_TRASHCAN_SAFE_END(self); +} + +static inline int +multidict_tp_traverse(MultiDictObject *self, visitproc visit, void *arg) +{ + return pair_list_traverse(&self->pairs, visit, arg); +} + +static inline int +multidict_tp_clear(MultiDictObject *self) +{ + return pair_list_clear(&self->pairs); +} + +PyDoc_STRVAR(multidict_getall_doc, +"Return a list of all values matching the key."); + +PyDoc_STRVAR(multidict_getone_doc, +"Get first value matching the key."); + +PyDoc_STRVAR(multidict_get_doc, +"Get first value matching the key.\n\nThe method is alias for .getone()."); + +PyDoc_STRVAR(multidict_keys_doc, +"Return a new view of the dictionary's keys."); + +PyDoc_STRVAR(multidict_items_doc, +"Return a new view of the dictionary's items *(key, value) pairs)."); + +PyDoc_STRVAR(multidict_values_doc, +"Return a new view of the dictionary's values."); + +/******************** MultiDict ********************/ + +static inline int +multidict_tp_init(MultiDictObject *self, PyObject *args, PyObject *kwds) +{ + if (pair_list_init(&self->pairs) < 0) { + return -1; + } + if (_multidict_extend(self, args, kwds, "MultiDict", 1) < 0) { + return -1; + } + return 0; +} + +static inline PyObject * +multidict_add(MultiDictObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *key = NULL, + *val = NULL; + + static const char * const _keywords[] = {"key", "value", NULL}; +#ifdef FASTCALL_OLD + static _PyArg_Parser _parser = {"OO:add", _keywords, 0}; + if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser, + &key, &val)) { + return NULL; + } +#else + static _PyArg_Parser _parser = {NULL, _keywords, "add", 0}; + PyObject *argsbuf[2]; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, 2, 2, 0, argsbuf); + if (!args) { + return NULL; + } + key = args[0]; + val = args[1]; +#endif + if (pair_list_add(&self->pairs, key, val) < 0) { + return NULL; + } + + Py_RETURN_NONE; +} + +static inline PyObject * +multidict_copy(MultiDictObject *self) +{ + return _multidict_copy(self, &multidict_type); +} + +static inline PyObject * +multidict_extend(MultiDictObject *self, PyObject *args, PyObject *kwds) +{ + if (_multidict_extend(self, args, kwds, "extend", 1) < 0) { + return NULL; + } + + Py_RETURN_NONE; +} + +static inline PyObject * +multidict_clear(MultiDictObject *self) +{ + if (pair_list_clear(&self->pairs) < 0) { + return NULL; + } + + Py_RETURN_NONE; +} + +static inline PyObject * +multidict_setdefault(MultiDictObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *key = NULL, + *_default = NULL; + + static const char * const _keywords[] = {"key", "default", NULL}; +#ifdef FASTCALL_OLD + static _PyArg_Parser _parser = {"O|O:setdefault", _keywords, 0}; + if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser, + &key, &_default)) { + return NULL; + } +#else + static _PyArg_Parser _parser = {NULL, _keywords, "setdefault", 0}; + PyObject *argsbuf[3]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, 1, 2, 0, argsbuf); + if (!args) { + return NULL; + } + key = args[0]; + if (!noptargs) { + goto skip_optional_pos; + } + _default = args[1]; + +skip_optional_pos: +#endif + return pair_list_set_default(&self->pairs, key, _default); +} + +static inline PyObject * +multidict_popone(MultiDictObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *key = NULL, + *_default = NULL, + *ret_val = NULL; + + static const char * const _keywords[] = {"key", "default", NULL}; +#ifdef FASTCALL_OLD + static _PyArg_Parser _parser = {"O|O:popone", _keywords, 0}; + if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser, + &key, &_default)) { + return NULL; + } +#else + static _PyArg_Parser _parser = {NULL, _keywords, "popone", 0}; + PyObject *argsbuf[3]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, 1, 2, 0, argsbuf); + if (!args) { + return NULL; + } + key = args[0]; + if (!noptargs) { + goto skip_optional_pos; + } + _default = args[1]; + +skip_optional_pos: +#endif + ret_val = pair_list_pop_one(&self->pairs, key); + + if (ret_val == NULL && + PyErr_ExceptionMatches(PyExc_KeyError) && + _default != NULL) + { + PyErr_Clear(); + Py_INCREF(_default); + return _default; + } + + return ret_val; +} + +static inline PyObject * +multidict_pop(MultiDictObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *key = NULL, + *_default = NULL, + *ret_val = NULL; + + static const char * const _keywords[] = {"key", "default", NULL}; +#ifdef FASTCALL_OLD + static _PyArg_Parser _parser = {"O|O:pop", _keywords, 0}; + if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser, + &key, &_default)) { + return NULL; + } +#else + static _PyArg_Parser _parser = {NULL, _keywords, "pop", 0}; + PyObject *argsbuf[3]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, 1, 2, 0, argsbuf); + if (!args) { + return NULL; + } + key = args[0]; + if (!noptargs) { + goto skip_optional_pos; + } + _default = args[1]; + +skip_optional_pos: +#endif + ret_val = pair_list_pop_one(&self->pairs, key); + + if (ret_val == NULL && + PyErr_ExceptionMatches(PyExc_KeyError) && + _default != NULL) + { + PyErr_Clear(); + Py_INCREF(_default); + return _default; + } + + return ret_val; +} + +static inline PyObject * +multidict_popall(MultiDictObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *key = NULL, + *_default = NULL, + *ret_val = NULL; + + + static const char * const _keywords[] = {"key", "default", NULL}; +#ifdef FASTCALL_OLD + static _PyArg_Parser _parser = {"O|O:popall", _keywords, 0}; + if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser, + &key, &_default)) { + return NULL; + } +#else + static _PyArg_Parser _parser = {NULL, _keywords, "popall", 0}; + PyObject *argsbuf[3]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, + &_parser, 1, 2, 0, argsbuf); + if (!args) { + return NULL; + } + key = args[0]; + if (!noptargs) { + goto skip_optional_pos; + } + _default = args[1]; + +skip_optional_pos: +#endif + ret_val = pair_list_pop_all(&self->pairs, key); + + if (ret_val == NULL && + PyErr_ExceptionMatches(PyExc_KeyError) && + _default != NULL) + { + PyErr_Clear(); + Py_INCREF(_default); + return _default; + } + + return ret_val; +} + +static inline PyObject * +multidict_popitem(MultiDictObject *self) +{ + return pair_list_pop_item(&self->pairs); +} + +static inline PyObject * +multidict_update(MultiDictObject *self, PyObject *args, PyObject *kwds) +{ + if (_multidict_extend(self, args, kwds, "update", 0) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + +PyDoc_STRVAR(multidict_add_doc, +"Add the key and value, not overwriting any previous value."); + +PyDoc_STRVAR(multidict_copy_doc, +"Return a copy of itself."); + +PyDoc_STRVAR(multdicit_method_extend_doc, +"Extend current MultiDict with more values.\n\ +This method must be used instead of update."); + +PyDoc_STRVAR(multidict_clear_doc, +"Remove all items from MultiDict"); + +PyDoc_STRVAR(multidict_setdefault_doc, +"Return value for key, set value to default if key is not present."); + +PyDoc_STRVAR(multidict_popone_doc, +"Remove the last occurrence of key and return the corresponding value.\n\n\ +If key is not found, default is returned if given, otherwise KeyError is \ +raised.\n"); + +PyDoc_STRVAR(multidict_pop_doc, +"Remove the last occurrence of key and return the corresponding value.\n\n\ +If key is not found, default is returned if given, otherwise KeyError is \ +raised.\n"); + +PyDoc_STRVAR(multidict_popall_doc, +"Remove all occurrences of key and return the list of corresponding values.\n\n\ +If key is not found, default is returned if given, otherwise KeyError is \ +raised.\n"); + +PyDoc_STRVAR(multidict_popitem_doc, +"Remove and return an arbitrary (key, value) pair."); + +PyDoc_STRVAR(multidict_update_doc, +"Update the dictionary from *other*, overwriting existing keys."); + + +#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 9 +#define multidict_class_getitem Py_GenericAlias +#else +static inline PyObject * +multidict_class_getitem(PyObject *self, PyObject *arg) +{ + Py_INCREF(self); + return self; +} +#endif + + +PyDoc_STRVAR(sizeof__doc__, +"D.__sizeof__() -> size of D in memory, in bytes"); + +static inline PyObject * +_multidict_sizeof(MultiDictObject *self) +{ + Py_ssize_t size = sizeof(MultiDictObject); + if (self->pairs.pairs != self->pairs.buffer) { + size += (Py_ssize_t)sizeof(pair_t) * self->pairs.capacity; + } + return PyLong_FromSsize_t(size); +} + + +static PySequenceMethods multidict_sequence = { + .sq_contains = (objobjproc)multidict_sq_contains, +}; + +static PyMappingMethods multidict_mapping = { + .mp_length = (lenfunc)multidict_mp_len, + .mp_subscript = (binaryfunc)multidict_mp_subscript, + .mp_ass_subscript = (objobjargproc)multidict_mp_as_subscript, +}; + +static PyMethodDef multidict_methods[] = { + { + "getall", + (PyCFunction)multidict_getall, + METH_FASTCALL | METH_KEYWORDS, + multidict_getall_doc + }, + { + "getone", + (PyCFunction)multidict_getone, + METH_FASTCALL | METH_KEYWORDS, + multidict_getone_doc + }, + { + "get", + (PyCFunction)multidict_get, + METH_FASTCALL | METH_KEYWORDS, + multidict_get_doc + }, + { + "keys", + (PyCFunction)multidict_keys, + METH_NOARGS, + multidict_keys_doc + }, + { + "items", + (PyCFunction)multidict_items, + METH_NOARGS, + multidict_items_doc + }, + { + "values", + (PyCFunction)multidict_values, + METH_NOARGS, + multidict_values_doc + }, + { + "add", + (PyCFunction)multidict_add, + METH_FASTCALL | METH_KEYWORDS, + multidict_add_doc + }, + { + "copy", + (PyCFunction)multidict_copy, + METH_NOARGS, + multidict_copy_doc + }, + { + "extend", + (PyCFunction)multidict_extend, + METH_VARARGS | METH_KEYWORDS, + multdicit_method_extend_doc + }, + { + "clear", + (PyCFunction)multidict_clear, + METH_NOARGS, + multidict_clear_doc + }, + { + "setdefault", + (PyCFunction)multidict_setdefault, + METH_FASTCALL | METH_KEYWORDS, + multidict_setdefault_doc + }, + { + "popone", + (PyCFunction)multidict_popone, + METH_FASTCALL | METH_KEYWORDS, + multidict_popone_doc + }, + { + "pop", + (PyCFunction)multidict_pop, + METH_FASTCALL | METH_KEYWORDS, + multidict_pop_doc + }, + { + "popall", + (PyCFunction)multidict_popall, + METH_FASTCALL | METH_KEYWORDS, + multidict_popall_doc + }, + { + "popitem", + (PyCFunction)multidict_popitem, + METH_NOARGS, + multidict_popitem_doc + }, + { + "update", + (PyCFunction)multidict_update, + METH_VARARGS | METH_KEYWORDS, + multidict_update_doc + }, + { + "__reduce__", + (PyCFunction)multidict_reduce, + METH_NOARGS, + NULL, + }, + { + "__class_getitem__", + (PyCFunction)multidict_class_getitem, + METH_O | METH_CLASS, + NULL + }, + { + "__sizeof__", + (PyCFunction)_multidict_sizeof, + METH_NOARGS, + sizeof__doc__, + }, + { + NULL, + NULL + } /* sentinel */ +}; + + +PyDoc_STRVAR(MultDict_doc, +"Dictionary with the support for duplicate keys."); + + +static PyTypeObject multidict_type = { + PyVarObject_HEAD_INIT(NULL, 0) + "multidict._multidict.MultiDict", /* tp_name */ + sizeof(MultiDictObject), /* tp_basicsize */ + .tp_dealloc = (destructor)multidict_tp_dealloc, + .tp_repr = (reprfunc)multidict_repr, + .tp_as_sequence = &multidict_sequence, + .tp_as_mapping = &multidict_mapping, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_doc = MultDict_doc, + .tp_traverse = (traverseproc)multidict_tp_traverse, + .tp_clear = (inquiry)multidict_tp_clear, + .tp_richcompare = (richcmpfunc)multidict_tp_richcompare, + .tp_weaklistoffset = offsetof(MultiDictObject, weaklist), + .tp_iter = (getiterfunc)multidict_tp_iter, + .tp_methods = multidict_methods, + .tp_init = (initproc)multidict_tp_init, + .tp_alloc = PyType_GenericAlloc, + .tp_new = PyType_GenericNew, + .tp_free = PyObject_GC_Del, +}; + +/******************** CIMultiDict ********************/ + +static inline int +cimultidict_tp_init(MultiDictObject *self, PyObject *args, PyObject *kwds) +{ + if (ci_pair_list_init(&self->pairs) < 0) { + return -1; + } + if (_multidict_extend(self, args, kwds, "CIMultiDict", 1) < 0) { + return -1; + } + return 0; +} + +static inline PyObject * +cimultidict_copy(MultiDictObject *self) +{ + return _multidict_copy(self, &cimultidict_type); +} + +PyDoc_STRVAR(cimultidict_copy_doc, +"Return a copy of itself."); + +static PyMethodDef cimultidict_methods[] = { + { + "copy", + (PyCFunction)cimultidict_copy, + METH_NOARGS, + cimultidict_copy_doc + }, + { + NULL, + NULL + } /* sentinel */ +}; + +PyDoc_STRVAR(CIMultDict_doc, +"Dictionary with the support for duplicate case-insensitive keys."); + + +static PyTypeObject cimultidict_type = { + PyVarObject_HEAD_INIT(NULL, 0) + "multidict._multidict.CIMultiDict", /* tp_name */ + sizeof(MultiDictObject), /* tp_basicsize */ + .tp_dealloc = (destructor)multidict_tp_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_doc = CIMultDict_doc, + .tp_traverse = (traverseproc)multidict_tp_traverse, + .tp_clear = (inquiry)multidict_tp_clear, + .tp_weaklistoffset = offsetof(MultiDictObject, weaklist), + .tp_methods = cimultidict_methods, + .tp_base = &multidict_type, + .tp_init = (initproc)cimultidict_tp_init, + .tp_alloc = PyType_GenericAlloc, + .tp_new = PyType_GenericNew, + .tp_free = PyObject_GC_Del, +}; + +/******************** MultiDictProxy ********************/ + +static inline int +multidict_proxy_tp_init(MultiDictProxyObject *self, PyObject *args, + PyObject *kwds) +{ + PyObject *arg = NULL; + MultiDictObject *md = NULL; + + if (!PyArg_UnpackTuple(args, "multidict._multidict.MultiDictProxy", + 0, 1, &arg)) + { + return -1; + } + if (arg == NULL) { + PyErr_Format( + PyExc_TypeError, + "__init__() missing 1 required positional argument: 'arg'" + ); + return -1; + } + if (!MultiDictProxy_CheckExact(arg) && + !CIMultiDict_CheckExact(arg) && + !MultiDict_CheckExact(arg)) + { + PyErr_Format( + PyExc_TypeError, + "ctor requires MultiDict or MultiDictProxy instance, " + "not ", + Py_TYPE(arg)->tp_name + ); + return -1; + } + + md = (MultiDictObject*)arg; + if (MultiDictProxy_CheckExact(arg)) { + md = ((MultiDictProxyObject*)arg)->md; + } + Py_INCREF(md); + self->md = md; + + return 0; +} + +static inline PyObject * +multidict_proxy_getall(MultiDictProxyObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames) +{ + return multidict_getall(self->md, args, nargs, kwnames); +} + +static inline PyObject * +multidict_proxy_getone(MultiDictProxyObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames) +{ + return multidict_getone(self->md, args, nargs, kwnames); +} + +static inline PyObject * +multidict_proxy_get(MultiDictProxyObject *self, PyObject *const *args, + Py_ssize_t nargs, PyObject *kwnames) +{ + return multidict_get(self->md, args, nargs, kwnames); +} + +static inline PyObject * +multidict_proxy_keys(MultiDictProxyObject *self) +{ + return multidict_keys(self->md); +} + +static inline PyObject * +multidict_proxy_items(MultiDictProxyObject *self) +{ + return multidict_items(self->md); +} + +static inline PyObject * +multidict_proxy_values(MultiDictProxyObject *self) +{ + return multidict_values(self->md); +} + +static inline PyObject * +multidict_proxy_copy(MultiDictProxyObject *self) +{ + return _multidict_proxy_copy(self, &multidict_type); +} + +static inline PyObject * +multidict_proxy_reduce(MultiDictProxyObject *self) +{ + PyErr_Format( + PyExc_TypeError, + "can't pickle %s objects", Py_TYPE(self)->tp_name + ); + + return NULL; +} + +static inline Py_ssize_t +multidict_proxy_mp_len(MultiDictProxyObject *self) +{ + return multidict_mp_len(self->md); +} + +static inline PyObject * +multidict_proxy_mp_subscript(MultiDictProxyObject *self, PyObject *key) +{ + return multidict_mp_subscript(self->md, key); +} + +static inline int +multidict_proxy_sq_contains(MultiDictProxyObject *self, PyObject *key) +{ + return multidict_sq_contains(self->md, key); +} + +static inline PyObject * +multidict_proxy_tp_iter(MultiDictProxyObject *self) +{ + return multidict_tp_iter(self->md); +} + +static inline PyObject * +multidict_proxy_tp_richcompare(MultiDictProxyObject *self, PyObject *other, + int op) +{ + return multidict_tp_richcompare((PyObject*)self->md, other, op); +} + +static inline void +multidict_proxy_tp_dealloc(MultiDictProxyObject *self) +{ + PyObject_GC_UnTrack(self); + if (self->weaklist != NULL) { + PyObject_ClearWeakRefs((PyObject *)self); + }; + Py_XDECREF(self->md); + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static inline int +multidict_proxy_tp_traverse(MultiDictProxyObject *self, visitproc visit, + void *arg) +{ + Py_VISIT(self->md); + return 0; +} + +static inline int +multidict_proxy_tp_clear(MultiDictProxyObject *self) +{ + Py_CLEAR(self->md); + return 0; +} + +static PySequenceMethods multidict_proxy_sequence = { + .sq_contains = (objobjproc)multidict_proxy_sq_contains, +}; + +static PyMappingMethods multidict_proxy_mapping = { + .mp_length = (lenfunc)multidict_proxy_mp_len, + .mp_subscript = (binaryfunc)multidict_proxy_mp_subscript, +}; + +static PyMethodDef multidict_proxy_methods[] = { + { + "getall", + (PyCFunction)multidict_proxy_getall, + METH_FASTCALL | METH_KEYWORDS, + multidict_getall_doc + }, + { + "getone", + (PyCFunction)multidict_proxy_getone, + METH_FASTCALL | METH_KEYWORDS, + multidict_getone_doc + }, + { + "get", + (PyCFunction)multidict_proxy_get, + METH_FASTCALL | METH_KEYWORDS, + multidict_get_doc + }, + { + "keys", + (PyCFunction)multidict_proxy_keys, + METH_NOARGS, + multidict_keys_doc + }, + { + "items", + (PyCFunction)multidict_proxy_items, + METH_NOARGS, + multidict_items_doc + }, + { + "values", + (PyCFunction)multidict_proxy_values, + METH_NOARGS, + multidict_values_doc + }, + { + "copy", + (PyCFunction)multidict_proxy_copy, + METH_NOARGS, + multidict_copy_doc + }, + { + "__reduce__", + (PyCFunction)multidict_proxy_reduce, + METH_NOARGS, + NULL + }, + { + "__class_getitem__", + (PyCFunction)multidict_class_getitem, + METH_O | METH_CLASS, + NULL + }, + { + NULL, + NULL + } /* sentinel */ +}; + + +PyDoc_STRVAR(MultDictProxy_doc, +"Read-only proxy for MultiDict instance."); + + +static PyTypeObject multidict_proxy_type = { + PyVarObject_HEAD_INIT(NULL, 0) + "multidict._multidict.MultiDictProxy", /* tp_name */ + sizeof(MultiDictProxyObject), /* tp_basicsize */ + .tp_dealloc = (destructor)multidict_proxy_tp_dealloc, + .tp_repr = (reprfunc)multidict_repr, + .tp_as_sequence = &multidict_proxy_sequence, + .tp_as_mapping = &multidict_proxy_mapping, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_doc = MultDictProxy_doc, + .tp_traverse = (traverseproc)multidict_proxy_tp_traverse, + .tp_clear = (inquiry)multidict_proxy_tp_clear, + .tp_richcompare = (richcmpfunc)multidict_proxy_tp_richcompare, + .tp_weaklistoffset = offsetof(MultiDictProxyObject, weaklist), + .tp_iter = (getiterfunc)multidict_proxy_tp_iter, + .tp_methods = multidict_proxy_methods, + .tp_init = (initproc)multidict_proxy_tp_init, + .tp_alloc = PyType_GenericAlloc, + .tp_new = PyType_GenericNew, + .tp_free = PyObject_GC_Del, +}; + +/******************** CIMultiDictProxy ********************/ + +static inline int +cimultidict_proxy_tp_init(MultiDictProxyObject *self, PyObject *args, + PyObject *kwds) +{ + PyObject *arg = NULL; + MultiDictObject *md = NULL; + + if (!PyArg_UnpackTuple(args, "multidict._multidict.CIMultiDictProxy", + 1, 1, &arg)) + { + return -1; + } + if (arg == NULL) { + PyErr_Format( + PyExc_TypeError, + "__init__() missing 1 required positional argument: 'arg'" + ); + return -1; + } + if (!CIMultiDictProxy_CheckExact(arg) && !CIMultiDict_CheckExact(arg)) { + PyErr_Format( + PyExc_TypeError, + "ctor requires CIMultiDict or CIMultiDictProxy instance, " + "not ", + Py_TYPE(arg)->tp_name + ); + return -1; + } + + md = (MultiDictObject*)arg; + if (CIMultiDictProxy_CheckExact(arg)) { + md = ((MultiDictProxyObject*)arg)->md; + } + Py_INCREF(md); + self->md = md; + + return 0; +} + +static inline PyObject * +cimultidict_proxy_copy(MultiDictProxyObject *self) +{ + return _multidict_proxy_copy(self, &cimultidict_type); +} + + +PyDoc_STRVAR(CIMultDictProxy_doc, +"Read-only proxy for CIMultiDict instance."); + +PyDoc_STRVAR(cimultidict_proxy_copy_doc, +"Return copy of itself"); + +static PyMethodDef cimultidict_proxy_methods[] = { + { + "copy", + (PyCFunction)cimultidict_proxy_copy, + METH_NOARGS, + cimultidict_proxy_copy_doc + }, + { + NULL, + NULL + } /* sentinel */ +}; + +static PyTypeObject cimultidict_proxy_type = { + PyVarObject_HEAD_INIT(NULL, 0) + "multidict._multidict.CIMultiDictProxy", /* tp_name */ + sizeof(MultiDictProxyObject), /* tp_basicsize */ + .tp_dealloc = (destructor)multidict_proxy_tp_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, + .tp_doc = CIMultDictProxy_doc, + .tp_traverse = (traverseproc)multidict_proxy_tp_traverse, + .tp_clear = (inquiry)multidict_proxy_tp_clear, + .tp_richcompare = (richcmpfunc)multidict_proxy_tp_richcompare, + .tp_weaklistoffset = offsetof(MultiDictProxyObject, weaklist), + .tp_methods = cimultidict_proxy_methods, + .tp_base = &multidict_proxy_type, + .tp_init = (initproc)cimultidict_proxy_tp_init, + .tp_alloc = PyType_GenericAlloc, + .tp_new = PyType_GenericNew, + .tp_free = PyObject_GC_Del, +}; + +/******************** Other functions ********************/ + +static inline PyObject * +getversion(PyObject *self, PyObject *md) +{ + pair_list_t *pairs = NULL; + if (MultiDict_CheckExact(md) || CIMultiDict_CheckExact(md)) { + pairs = &((MultiDictObject*)md)->pairs; + } else if (MultiDictProxy_CheckExact(md) || CIMultiDictProxy_CheckExact(md)) { + pairs = &((MultiDictProxyObject*)md)->md->pairs; + } else { + PyErr_Format(PyExc_TypeError, "unexpected type"); + return NULL; + } + return PyLong_FromUnsignedLong(pair_list_version(pairs)); +} + +/******************** Module ********************/ + +static inline void +module_free(void *m) +{ + Py_CLEAR(collections_abc_mapping); + Py_CLEAR(collections_abc_mut_mapping); + Py_CLEAR(collections_abc_mut_multi_mapping); +} + +static PyMethodDef multidict_module_methods[] = { + { + "getversion", + (PyCFunction)getversion, + METH_O + }, + { + NULL, + NULL + } /* sentinel */ +}; + +static PyModuleDef multidict_module = { + PyModuleDef_HEAD_INIT, /* m_base */ + "_multidict", /* m_name */ + .m_size = -1, + .m_methods = multidict_module_methods, + .m_free = (freefunc)module_free, +}; + +PyMODINIT_FUNC +PyInit__multidict() +{ + PyObject *module = NULL, + *reg_func_call_result = NULL; + +#define WITH_MOD(NAME) \ + Py_CLEAR(module); \ + module = PyImport_ImportModule(NAME); \ + if (module == NULL) { \ + goto fail; \ + } + +#define GET_MOD_ATTR(VAR, NAME) \ + VAR = PyObject_GetAttrString(module, NAME); \ + if (VAR == NULL) { \ + goto fail; \ + } + + if (multidict_views_init() < 0) { + goto fail; + } + + if (multidict_iter_init() < 0) { + goto fail; + } + + if (istr_init() < 0) { + goto fail; + } + + if (PyType_Ready(&multidict_type) < 0 || + PyType_Ready(&cimultidict_type) < 0 || + PyType_Ready(&multidict_proxy_type) < 0 || + PyType_Ready(&cimultidict_proxy_type) < 0) + { + goto fail; + } + + WITH_MOD("collections.abc"); + GET_MOD_ATTR(collections_abc_mapping, "Mapping"); + + WITH_MOD("multidict._abc"); + GET_MOD_ATTR(collections_abc_mut_mapping, "MultiMapping"); + + WITH_MOD("multidict._abc"); + GET_MOD_ATTR(collections_abc_mut_multi_mapping, "MutableMultiMapping"); + + WITH_MOD("multidict._multidict_base"); + GET_MOD_ATTR(repr_func, "_mdrepr"); + + /* Register in _abc mappings (CI)MultiDict and (CI)MultiDictProxy */ + reg_func_call_result = PyObject_CallMethod( + collections_abc_mut_mapping, + "register", "O", + (PyObject*)&multidict_proxy_type + ); + if (reg_func_call_result == NULL) { + goto fail; + } + Py_DECREF(reg_func_call_result); + + reg_func_call_result = PyObject_CallMethod( + collections_abc_mut_mapping, + "register", "O", + (PyObject*)&cimultidict_proxy_type + ); + if (reg_func_call_result == NULL) { + goto fail; + } + Py_DECREF(reg_func_call_result); + + reg_func_call_result = PyObject_CallMethod( + collections_abc_mut_multi_mapping, + "register", "O", + (PyObject*)&multidict_type + ); + if (reg_func_call_result == NULL) { + goto fail; + } + Py_DECREF(reg_func_call_result); + + reg_func_call_result = PyObject_CallMethod( + collections_abc_mut_multi_mapping, + "register", "O", + (PyObject*)&cimultidict_type + ); + if (reg_func_call_result == NULL) { + goto fail; + } + Py_DECREF(reg_func_call_result); + + /* Instantiate this module */ + module = PyModule_Create(&multidict_module); + + Py_INCREF(&istr_type); + if (PyModule_AddObject( + module, "istr", (PyObject*)&istr_type) < 0) + { + goto fail; + } + + Py_INCREF(&multidict_type); + if (PyModule_AddObject( + module, "MultiDict", (PyObject*)&multidict_type) < 0) + { + goto fail; + } + + Py_INCREF(&cimultidict_type); + if (PyModule_AddObject( + module, "CIMultiDict", (PyObject*)&cimultidict_type) < 0) + { + goto fail; + } + + Py_INCREF(&multidict_proxy_type); + if (PyModule_AddObject( + module, "MultiDictProxy", (PyObject*)&multidict_proxy_type) < 0) + { + goto fail; + } + + Py_INCREF(&cimultidict_proxy_type); + if (PyModule_AddObject( + module, "CIMultiDictProxy", (PyObject*)&cimultidict_proxy_type) < 0) + { + goto fail; + } + + return module; + +fail: + Py_XDECREF(collections_abc_mapping); + Py_XDECREF(collections_abc_mut_mapping); + Py_XDECREF(collections_abc_mut_multi_mapping); + + return NULL; + +#undef WITH_MOD +#undef GET_MOD_ATTR +} diff --git a/multidict/_multidict_base.py b/multidict/_multidict_base.py new file mode 100644 index 0000000..3944665 --- /dev/null +++ b/multidict/_multidict_base.py @@ -0,0 +1,144 @@ +from collections.abc import ItemsView, Iterable, KeysView, Set, ValuesView + + +def _abc_itemsview_register(view_cls): + ItemsView.register(view_cls) + + +def _abc_keysview_register(view_cls): + KeysView.register(view_cls) + + +def _abc_valuesview_register(view_cls): + ValuesView.register(view_cls) + + +def _viewbaseset_richcmp(view, other, op): + if op == 0: # < + if not isinstance(other, Set): + return NotImplemented + return len(view) < len(other) and view <= other + elif op == 1: # <= + if not isinstance(other, Set): + return NotImplemented + if len(view) > len(other): + return False + for elem in view: + if elem not in other: + return False + return True + elif op == 2: # == + if not isinstance(other, Set): + return NotImplemented + return len(view) == len(other) and view <= other + elif op == 3: # != + return not view == other + elif op == 4: # > + if not isinstance(other, Set): + return NotImplemented + return len(view) > len(other) and view >= other + elif op == 5: # >= + if not isinstance(other, Set): + return NotImplemented + if len(view) < len(other): + return False + for elem in other: + if elem not in view: + return False + return True + + +def _viewbaseset_and(view, other): + if not isinstance(other, Iterable): + return NotImplemented + if isinstance(view, Set): + view = set(iter(view)) + if isinstance(other, Set): + other = set(iter(other)) + if not isinstance(other, Set): + other = set(iter(other)) + return view & other + + +def _viewbaseset_or(view, other): + if not isinstance(other, Iterable): + return NotImplemented + if isinstance(view, Set): + view = set(iter(view)) + if isinstance(other, Set): + other = set(iter(other)) + if not isinstance(other, Set): + other = set(iter(other)) + return view | other + + +def _viewbaseset_sub(view, other): + if not isinstance(other, Iterable): + return NotImplemented + if isinstance(view, Set): + view = set(iter(view)) + if isinstance(other, Set): + other = set(iter(other)) + if not isinstance(other, Set): + other = set(iter(other)) + return view - other + + +def _viewbaseset_xor(view, other): + if not isinstance(other, Iterable): + return NotImplemented + if isinstance(view, Set): + view = set(iter(view)) + if isinstance(other, Set): + other = set(iter(other)) + if not isinstance(other, Set): + other = set(iter(other)) + return view ^ other + + +def _itemsview_isdisjoint(view, other): + "Return True if two sets have a null intersection." + for v in other: + if v in view: + return False + return True + + +def _itemsview_repr(view): + lst = [] + for k, v in view: + lst.append("{!r}: {!r}".format(k, v)) + body = ", ".join(lst) + return "{}({})".format(view.__class__.__name__, body) + + +def _keysview_isdisjoint(view, other): + "Return True if two sets have a null intersection." + for k in other: + if k in view: + return False + return True + + +def _keysview_repr(view): + lst = [] + for k in view: + lst.append("{!r}".format(k)) + body = ", ".join(lst) + return "{}({})".format(view.__class__.__name__, body) + + +def _valuesview_repr(view): + lst = [] + for v in view: + lst.append("{!r}".format(v)) + body = ", ".join(lst) + return "{}({})".format(view.__class__.__name__, body) + + +def _mdrepr(md): + lst = [] + for k, v in md.items(): + lst.append("'{}': {!r}".format(k, v)) + body = ", ".join(lst) + return "<{}({})>".format(md.__class__.__name__, body) diff --git a/multidict/_multidict_py.py b/multidict/_multidict_py.py new file mode 100644 index 0000000..cdbc328 --- /dev/null +++ b/multidict/_multidict_py.py @@ -0,0 +1,526 @@ +import sys +import types +from array import array +from collections import abc + +from ._abc import MultiMapping, MutableMultiMapping + +_marker = object() + +if sys.version_info >= (3, 9): + GenericAlias = types.GenericAlias +else: + def GenericAlias(cls): + return cls + + +class istr(str): + + """Case insensitive str.""" + + __is_istr__ = True + + +upstr = istr # for relaxing backward compatibility problems + + +def getversion(md): + if not isinstance(md, _Base): + raise TypeError("Parameter should be multidict or proxy") + return md._impl._version + + +_version = array("Q", [0]) + + +class _Impl: + __slots__ = ("_items", "_version") + + def __init__(self): + self._items = [] + self.incr_version() + + def incr_version(self): + global _version + v = _version + v[0] += 1 + self._version = v[0] + + if sys.implementation.name != "pypy": + + def __sizeof__(self): + return object.__sizeof__(self) + sys.getsizeof(self._items) + + +class _Base: + def _title(self, key): + return key + + def getall(self, key, default=_marker): + """Return a list of all values matching the key.""" + identity = self._title(key) + res = [v for i, k, v in self._impl._items if i == identity] + if res: + return res + if not res and default is not _marker: + return default + raise KeyError("Key not found: %r" % key) + + def getone(self, key, default=_marker): + """Get first value matching the key. + + Raises KeyError if the key is not found and no default is provided. + """ + identity = self._title(key) + for i, k, v in self._impl._items: + if i == identity: + return v + if default is not _marker: + return default + raise KeyError("Key not found: %r" % key) + + # Mapping interface # + + def __getitem__(self, key): + return self.getone(key) + + def get(self, key, default=None): + """Get first value matching the key. + + If the key is not found, returns the default (or None if no default is provided) + """ + return self.getone(key, default) + + def __iter__(self): + return iter(self.keys()) + + def __len__(self): + return len(self._impl._items) + + def keys(self): + """Return a new view of the dictionary's keys.""" + return _KeysView(self._impl) + + def items(self): + """Return a new view of the dictionary's items *(key, value) pairs).""" + return _ItemsView(self._impl) + + def values(self): + """Return a new view of the dictionary's values.""" + return _ValuesView(self._impl) + + def __eq__(self, other): + if not isinstance(other, abc.Mapping): + return NotImplemented + if isinstance(other, _Base): + lft = self._impl._items + rht = other._impl._items + if len(lft) != len(rht): + return False + for (i1, k2, v1), (i2, k2, v2) in zip(lft, rht): + if i1 != i2 or v1 != v2: + return False + return True + if len(self._impl._items) != len(other): + return False + for k, v in self.items(): + nv = other.get(k, _marker) + if v != nv: + return False + return True + + def __contains__(self, key): + identity = self._title(key) + for i, k, v in self._impl._items: + if i == identity: + return True + return False + + def __repr__(self): + body = ", ".join("'{}': {!r}".format(k, v) for k, v in self.items()) + return "<{}({})>".format(self.__class__.__name__, body) + + __class_getitem__ = classmethod(GenericAlias) + + +class MultiDictProxy(_Base, MultiMapping): + """Read-only proxy for MultiDict instance.""" + + def __init__(self, arg): + if not isinstance(arg, (MultiDict, MultiDictProxy)): + raise TypeError( + "ctor requires MultiDict or MultiDictProxy instance" + ", not {}".format(type(arg)) + ) + + self._impl = arg._impl + + def __reduce__(self): + raise TypeError("can't pickle {} objects".format(self.__class__.__name__)) + + def copy(self): + """Return a copy of itself.""" + return MultiDict(self.items()) + + +class CIMultiDictProxy(MultiDictProxy): + """Read-only proxy for CIMultiDict instance.""" + + def __init__(self, arg): + if not isinstance(arg, (CIMultiDict, CIMultiDictProxy)): + raise TypeError( + "ctor requires CIMultiDict or CIMultiDictProxy instance" + ", not {}".format(type(arg)) + ) + + self._impl = arg._impl + + def _title(self, key): + return key.title() + + def copy(self): + """Return a copy of itself.""" + return CIMultiDict(self.items()) + + +class MultiDict(_Base, MutableMultiMapping): + """Dictionary with the support for duplicate keys.""" + + def __init__(self, *args, **kwargs): + self._impl = _Impl() + + self._extend(args, kwargs, self.__class__.__name__, self._extend_items) + + if sys.implementation.name != "pypy": + + def __sizeof__(self): + return object.__sizeof__(self) + sys.getsizeof(self._impl) + + def __reduce__(self): + return (self.__class__, (list(self.items()),)) + + def _title(self, key): + return key + + def _key(self, key): + if isinstance(key, str): + return key + else: + raise TypeError( + "MultiDict keys should be either str " "or subclasses of str" + ) + + def add(self, key, value): + identity = self._title(key) + self._impl._items.append((identity, self._key(key), value)) + self._impl.incr_version() + + def copy(self): + """Return a copy of itself.""" + cls = self.__class__ + return cls(self.items()) + + __copy__ = copy + + def extend(self, *args, **kwargs): + """Extend current MultiDict with more values. + + This method must be used instead of update. + """ + self._extend(args, kwargs, "extend", self._extend_items) + + def _extend(self, args, kwargs, name, method): + if len(args) > 1: + raise TypeError( + "{} takes at most 1 positional argument" + " ({} given)".format(name, len(args)) + ) + if args: + arg = args[0] + if isinstance(args[0], (MultiDict, MultiDictProxy)) and not kwargs: + items = arg._impl._items + else: + if hasattr(arg, "items"): + arg = arg.items() + if kwargs: + arg = list(arg) + arg.extend(list(kwargs.items())) + items = [] + for item in arg: + if not len(item) == 2: + raise TypeError( + "{} takes either dict or list of (key, value) " + "tuples".format(name) + ) + items.append((self._title(item[0]), self._key(item[0]), item[1])) + + method(items) + else: + method( + [ + (self._title(key), self._key(key), value) + for key, value in kwargs.items() + ] + ) + + def _extend_items(self, items): + for identity, key, value in items: + self.add(key, value) + + def clear(self): + """Remove all items from MultiDict.""" + self._impl._items.clear() + self._impl.incr_version() + + # Mapping interface # + + def __setitem__(self, key, value): + self._replace(key, value) + + def __delitem__(self, key): + identity = self._title(key) + items = self._impl._items + found = False + for i in range(len(items) - 1, -1, -1): + if items[i][0] == identity: + del items[i] + found = True + if not found: + raise KeyError(key) + else: + self._impl.incr_version() + + def setdefault(self, key, default=None): + """Return value for key, set value to default if key is not present.""" + identity = self._title(key) + for i, k, v in self._impl._items: + if i == identity: + return v + self.add(key, default) + return default + + def popone(self, key, default=_marker): + """Remove specified key and return the corresponding value. + + If key is not found, d is returned if given, otherwise + KeyError is raised. + + """ + identity = self._title(key) + for i in range(len(self._impl._items)): + if self._impl._items[i][0] == identity: + value = self._impl._items[i][2] + del self._impl._items[i] + self._impl.incr_version() + return value + if default is _marker: + raise KeyError(key) + else: + return default + + pop = popone # type: ignore + + def popall(self, key, default=_marker): + """Remove all occurrences of key and return the list of corresponding + values. + + If key is not found, default is returned if given, otherwise + KeyError is raised. + + """ + found = False + identity = self._title(key) + ret = [] + for i in range(len(self._impl._items) - 1, -1, -1): + item = self._impl._items[i] + if item[0] == identity: + ret.append(item[2]) + del self._impl._items[i] + self._impl.incr_version() + found = True + if not found: + if default is _marker: + raise KeyError(key) + else: + return default + else: + ret.reverse() + return ret + + def popitem(self): + """Remove and return an arbitrary (key, value) pair.""" + if self._impl._items: + i = self._impl._items.pop(0) + self._impl.incr_version() + return i[1], i[2] + else: + raise KeyError("empty multidict") + + def update(self, *args, **kwargs): + """Update the dictionary from *other*, overwriting existing keys.""" + self._extend(args, kwargs, "update", self._update_items) + + def _update_items(self, items): + if not items: + return + used_keys = {} + for identity, key, value in items: + start = used_keys.get(identity, 0) + for i in range(start, len(self._impl._items)): + item = self._impl._items[i] + if item[0] == identity: + used_keys[identity] = i + 1 + self._impl._items[i] = (identity, key, value) + break + else: + self._impl._items.append((identity, key, value)) + used_keys[identity] = len(self._impl._items) + + # drop tails + i = 0 + while i < len(self._impl._items): + item = self._impl._items[i] + identity = item[0] + pos = used_keys.get(identity) + if pos is None: + i += 1 + continue + if i >= pos: + del self._impl._items[i] + else: + i += 1 + + self._impl.incr_version() + + def _replace(self, key, value): + key = self._key(key) + identity = self._title(key) + items = self._impl._items + + for i in range(len(items)): + item = items[i] + if item[0] == identity: + items[i] = (identity, key, value) + # i points to last found item + rgt = i + self._impl.incr_version() + break + else: + self._impl._items.append((identity, key, value)) + self._impl.incr_version() + return + + # remove all tail items + i = rgt + 1 + while i < len(items): + item = items[i] + if item[0] == identity: + del items[i] + else: + i += 1 + + +class CIMultiDict(MultiDict): + """Dictionary with the support for duplicate case-insensitive keys.""" + + def _title(self, key): + return key.title() + + +class _Iter: + __slots__ = ("_size", "_iter") + + def __init__(self, size, iterator): + self._size = size + self._iter = iterator + + def __iter__(self): + return self + + def __next__(self): + return next(self._iter) + + def __length_hint__(self): + return self._size + + +class _ViewBase: + def __init__(self, impl): + self._impl = impl + + def __len__(self): + return len(self._impl._items) + + +class _ItemsView(_ViewBase, abc.ItemsView): + def __contains__(self, item): + assert isinstance(item, tuple) or isinstance(item, list) + assert len(item) == 2 + for i, k, v in self._impl._items: + if item[0] == k and item[1] == v: + return True + return False + + def __iter__(self): + return _Iter(len(self), self._iter(self._impl._version)) + + def _iter(self, version): + for i, k, v in self._impl._items: + if version != self._impl._version: + raise RuntimeError("Dictionary changed during iteration") + yield k, v + + def __repr__(self): + lst = [] + for item in self._impl._items: + lst.append("{!r}: {!r}".format(item[1], item[2])) + body = ", ".join(lst) + return "{}({})".format(self.__class__.__name__, body) + + +class _ValuesView(_ViewBase, abc.ValuesView): + def __contains__(self, value): + for item in self._impl._items: + if item[2] == value: + return True + return False + + def __iter__(self): + return _Iter(len(self), self._iter(self._impl._version)) + + def _iter(self, version): + for item in self._impl._items: + if version != self._impl._version: + raise RuntimeError("Dictionary changed during iteration") + yield item[2] + + def __repr__(self): + lst = [] + for item in self._impl._items: + lst.append("{!r}".format(item[2])) + body = ", ".join(lst) + return "{}({})".format(self.__class__.__name__, body) + + +class _KeysView(_ViewBase, abc.KeysView): + def __contains__(self, key): + for item in self._impl._items: + if item[1] == key: + return True + return False + + def __iter__(self): + return _Iter(len(self), self._iter(self._impl._version)) + + def _iter(self, version): + for item in self._impl._items: + if version != self._impl._version: + raise RuntimeError("Dictionary changed during iteration") + yield item[1] + + def __repr__(self): + lst = [] + for item in self._impl._items: + lst.append("{!r}".format(item[1])) + body = ", ".join(lst) + return "{}({})".format(self.__class__.__name__, body) diff --git a/multidict/_multilib/defs.h b/multidict/_multilib/defs.h new file mode 100644 index 0000000..c7027c8 --- /dev/null +++ b/multidict/_multilib/defs.h @@ -0,0 +1,22 @@ +#ifndef _MULTIDICT_DEFS_H +#define _MULTIDICT_DEFS_H + +#ifdef __cplusplus +extern "C" { +#endif + +_Py_IDENTIFIER(lower); + +/* We link this module statically for convenience. If compiled as a shared + library instead, some compilers don't allow addresses of Python objects + defined in other libraries to be used in static initializers here. The + DEFERRED_ADDRESS macro is used to tag the slots where such addresses + appear; the module init function must fill in the tagged slots at runtime. + The argument is for documentation -- the macro ignores it. +*/ +#define DEFERRED_ADDRESS(ADDR) 0 + +#ifdef __cplusplus +} +#endif +#endif diff --git a/multidict/_multilib/dict.h b/multidict/_multilib/dict.h new file mode 100644 index 0000000..3caf83e --- /dev/null +++ b/multidict/_multilib/dict.h @@ -0,0 +1,24 @@ +#ifndef _MULTIDICT_C_H +#define _MULTIDICT_C_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { // 16 or 24 for GC prefix + PyObject_HEAD // 16 + PyObject *weaklist; + pair_list_t pairs; +} MultiDictObject; + +typedef struct { + PyObject_HEAD + PyObject *weaklist; + MultiDictObject *md; +} MultiDictProxyObject; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/multidict/_multilib/istr.h b/multidict/_multilib/istr.h new file mode 100644 index 0000000..2688f48 --- /dev/null +++ b/multidict/_multilib/istr.h @@ -0,0 +1,85 @@ +#ifndef _MULTIDICT_ISTR_H +#define _MULTIDICT_ISTR_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + PyUnicodeObject str; + PyObject * canonical; +} istrobject; + +PyDoc_STRVAR(istr__doc__, "istr class implementation"); + +static PyTypeObject istr_type; + +static inline void +istr_dealloc(istrobject *self) +{ + Py_XDECREF(self->canonical); + PyUnicode_Type.tp_dealloc((PyObject*)self); +} + +static inline PyObject * +istr_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyObject *x = NULL; + static char *kwlist[] = {"object", "encoding", "errors", 0}; + PyObject *encoding = NULL; + PyObject *errors = NULL; + PyObject *s = NULL; + PyObject * ret = NULL; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO:str", + kwlist, &x, &encoding, &errors)) { + return NULL; + } + if (x != NULL && Py_TYPE(x) == &istr_type) { + Py_INCREF(x); + return x; + } + ret = PyUnicode_Type.tp_new(type, args, kwds); + if (!ret) { + goto fail; + } + s =_PyObject_CallMethodId(ret, &PyId_lower, NULL); + if (!s) { + goto fail; + } + ((istrobject*)ret)->canonical = s; + s = NULL; /* the reference is stollen by .canonical */ + return ret; +fail: + Py_XDECREF(ret); + return NULL; +} + +static PyTypeObject istr_type = { + PyVarObject_HEAD_INIT(DEFERRED_ADDRESS(&PyType_Type), 0) + "multidict._multidict.istr", + sizeof(istrobject), + .tp_dealloc = (destructor)istr_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT + | Py_TPFLAGS_BASETYPE + | Py_TPFLAGS_UNICODE_SUBCLASS, + .tp_doc = istr__doc__, + .tp_base = DEFERRED_ADDRESS(&PyUnicode_Type), + .tp_new = (newfunc)istr_new, +}; + + +static inline int +istr_init(void) +{ + istr_type.tp_base = &PyUnicode_Type; + if (PyType_Ready(&istr_type) < 0) { + return -1; + } + return 0; +} + +#ifdef __cplusplus +} +#endif +#endif diff --git a/multidict/_multilib/iter.h b/multidict/_multilib/iter.h new file mode 100644 index 0000000..4e2e32b --- /dev/null +++ b/multidict/_multilib/iter.h @@ -0,0 +1,238 @@ +#ifndef _MULTIDICT_ITER_H +#define _MULTIDICT_ITER_H + +#ifdef __cplusplus +extern "C" { +#endif + +static PyTypeObject multidict_items_iter_type; +static PyTypeObject multidict_values_iter_type; +static PyTypeObject multidict_keys_iter_type; + +typedef struct multidict_iter { + PyObject_HEAD + MultiDictObject *md; // MultiDict or CIMultiDict + Py_ssize_t current; + uint64_t version; +} MultidictIter; + +static inline void +_init_iter(MultidictIter *it, MultiDictObject *md) +{ + Py_INCREF(md); + + it->md = md; + it->current = 0; + it->version = pair_list_version(&md->pairs); +} + +static inline PyObject * +multidict_items_iter_new(MultiDictObject *md) +{ + MultidictIter *it = PyObject_GC_New( + MultidictIter, &multidict_items_iter_type); + if (it == NULL) { + return NULL; + } + + _init_iter(it, md); + + PyObject_GC_Track(it); + return (PyObject *)it; +} + +static inline PyObject * +multidict_keys_iter_new(MultiDictObject *md) +{ + MultidictIter *it = PyObject_GC_New( + MultidictIter, &multidict_keys_iter_type); + if (it == NULL) { + return NULL; + } + + _init_iter(it, md); + + PyObject_GC_Track(it); + return (PyObject *)it; +} + +static inline PyObject * +multidict_values_iter_new(MultiDictObject *md) +{ + MultidictIter *it = PyObject_GC_New( + MultidictIter, &multidict_values_iter_type); + if (it == NULL) { + return NULL; + } + + _init_iter(it, md); + + PyObject_GC_Track(it); + return (PyObject *)it; +} + +static inline PyObject * +multidict_items_iter_iternext(MultidictIter *self) +{ + PyObject *key = NULL; + PyObject *value = NULL; + PyObject *ret = NULL; + + if (self->version != pair_list_version(&self->md->pairs)) { + PyErr_SetString(PyExc_RuntimeError, "Dictionary changed during iteration"); + return NULL; + } + + if (!_pair_list_next(&self->md->pairs, &self->current, NULL, &key, &value, NULL)) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + + ret = PyTuple_Pack(2, key, value); + if (ret == NULL) { + return NULL; + } + + return ret; +} + +static inline PyObject * +multidict_values_iter_iternext(MultidictIter *self) +{ + PyObject *value = NULL; + + if (self->version != pair_list_version(&self->md->pairs)) { + PyErr_SetString(PyExc_RuntimeError, "Dictionary changed during iteration"); + return NULL; + } + + if (!pair_list_next(&self->md->pairs, &self->current, NULL, NULL, &value)) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + + Py_INCREF(value); + + return value; +} + +static inline PyObject * +multidict_keys_iter_iternext(MultidictIter *self) +{ + PyObject *key = NULL; + + if (self->version != pair_list_version(&self->md->pairs)) { + PyErr_SetString(PyExc_RuntimeError, "Dictionary changed during iteration"); + return NULL; + } + + if (!pair_list_next(&self->md->pairs, &self->current, NULL, &key, NULL)) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + + Py_INCREF(key); + + return key; +} + +static inline void +multidict_iter_dealloc(MultidictIter *self) +{ + PyObject_GC_UnTrack(self); + Py_XDECREF(self->md); + PyObject_GC_Del(self); +} + +static inline int +multidict_iter_traverse(MultidictIter *self, visitproc visit, void *arg) +{ + Py_VISIT(self->md); + return 0; +} + +static inline int +multidict_iter_clear(MultidictIter *self) +{ + Py_CLEAR(self->md); + return 0; +} + +static inline PyObject * +multidict_iter_len(MultidictIter *self) +{ + return PyLong_FromLong(pair_list_len(&self->md->pairs)); +} + +PyDoc_STRVAR(length_hint_doc, + "Private method returning an estimate of len(list(it))."); + +static PyMethodDef multidict_iter_methods[] = { + { + "__length_hint__", + (PyCFunction)(void(*)(void))multidict_iter_len, + METH_NOARGS, + length_hint_doc + }, + { + NULL, + NULL + } /* sentinel */ +}; + +/***********************************************************************/ + +static PyTypeObject multidict_items_iter_type = { + PyVarObject_HEAD_INIT(DEFERRED_ADDRESS(&PyType_Type), 0) + "multidict._multidict._itemsiter", /* tp_name */ + sizeof(MultidictIter), /* tp_basicsize */ + .tp_dealloc = (destructor)multidict_iter_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_traverse = (traverseproc)multidict_iter_traverse, + .tp_clear = (inquiry)multidict_iter_clear, + .tp_iter = PyObject_SelfIter, + .tp_iternext = (iternextfunc)multidict_items_iter_iternext, + .tp_methods = multidict_iter_methods, +}; + +static PyTypeObject multidict_values_iter_type = { + PyVarObject_HEAD_INIT(DEFERRED_ADDRESS(&PyType_Type), 0) + "multidict._multidict._valuesiter", /* tp_name */ + sizeof(MultidictIter), /* tp_basicsize */ + .tp_dealloc = (destructor)multidict_iter_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_traverse = (traverseproc)multidict_iter_traverse, + .tp_clear = (inquiry)multidict_iter_clear, + .tp_iter = PyObject_SelfIter, + .tp_iternext = (iternextfunc)multidict_values_iter_iternext, + .tp_methods = multidict_iter_methods, +}; + +static PyTypeObject multidict_keys_iter_type = { + PyVarObject_HEAD_INIT(DEFERRED_ADDRESS(&PyType_Type), 0) + "multidict._multidict._keysiter", /* tp_name */ + sizeof(MultidictIter), /* tp_basicsize */ + .tp_dealloc = (destructor)multidict_iter_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_traverse = (traverseproc)multidict_iter_traverse, + .tp_clear = (inquiry)multidict_iter_clear, + .tp_iter = PyObject_SelfIter, + .tp_iternext = (iternextfunc)multidict_keys_iter_iternext, + .tp_methods = multidict_iter_methods, +}; + +static inline int +multidict_iter_init() +{ + if (PyType_Ready(&multidict_items_iter_type) < 0 || + PyType_Ready(&multidict_values_iter_type) < 0 || + PyType_Ready(&multidict_keys_iter_type) < 0) { + return -1; + } + return 0; +} + +#ifdef __cplusplus +} +#endif +#endif diff --git a/multidict/_multilib/pair_list.h b/multidict/_multilib/pair_list.h new file mode 100644 index 0000000..7eafd21 --- /dev/null +++ b/multidict/_multilib/pair_list.h @@ -0,0 +1,1244 @@ +#ifndef _MULTIDICT_PAIR_LIST_H +#define _MULTIDICT_PAIR_LIST_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +typedef PyObject * (*calc_identity_func)(PyObject *key); + +typedef struct pair { + PyObject *identity; // 8 + PyObject *key; // 8 + PyObject *value; // 8 + Py_hash_t hash; // 8 +} pair_t; + +/* Note about the structure size +With 29 pairs the MultiDict object size is slightly less than 1KiB +(1000-1008 bytes depending on Python version, +plus extra 12 bytes for memory allocator internal structures). +As the result the max reserved size is 1020 bytes at most. + +To fit into 512 bytes, the structure can contain only 13 pairs +which is too small, e.g. https://www.python.org returns 16 headers +(9 of them are caching proxy information though). + +The embedded buffer intention is to fit the vast majority of possible +HTTP headers into the buffer without allocating an extra memory block. +*/ + +#if (PY_VERSION_HEX < 0x03080000) +#define EMBEDDED_CAPACITY 28 +#else +#define EMBEDDED_CAPACITY 29 +#endif + +typedef struct pair_list { // 40 + Py_ssize_t capacity; // 8 + Py_ssize_t size; // 8 + uint64_t version; // 8 + calc_identity_func calc_identity; // 8 + pair_t *pairs; // 8 + pair_t buffer[EMBEDDED_CAPACITY]; +} pair_list_t; + +#define MIN_CAPACITY 63 +#define CAPACITY_STEP 64 + +/* Global counter used to set ma_version_tag field of dictionary. + * It is incremented each time that a dictionary is created and each + * time that a dictionary is modified. */ +static uint64_t pair_list_global_version = 0; + +#define NEXT_VERSION() (++pair_list_global_version) + + +static inline int +str_cmp(PyObject *s1, PyObject *s2) +{ + PyObject *ret = PyUnicode_RichCompare(s1, s2, Py_EQ); + if (ret == Py_True) { + Py_DECREF(ret); + return 1; + } + else if (ret == NULL) { + return -1; + } + else { + Py_DECREF(ret); + return 0; + } +} + + +static inline PyObject * +key_to_str(PyObject *key) +{ + PyObject *ret; + PyTypeObject *type = Py_TYPE(key); + if (type == &istr_type) { + ret = ((istrobject*)key)->canonical; + Py_INCREF(ret); + return ret; + } + if (PyUnicode_CheckExact(key)) { + Py_INCREF(key); + return key; + } + if (PyUnicode_Check(key)) { + return PyObject_Str(key); + } + PyErr_SetString(PyExc_TypeError, + "MultiDict keys should be either str " + "or subclasses of str"); + return NULL; +} + + +static inline PyObject * +ci_key_to_str(PyObject *key) +{ + PyObject *ret; + PyTypeObject *type = Py_TYPE(key); + if (type == &istr_type) { + ret = ((istrobject*)key)->canonical; + Py_INCREF(ret); + return ret; + } + if (PyUnicode_Check(key)) { + return _PyObject_CallMethodId(key, &PyId_lower, NULL); + } + PyErr_SetString(PyExc_TypeError, + "CIMultiDict keys should be either str " + "or subclasses of str"); + return NULL; +} + +static inline pair_t * +pair_list_get(pair_list_t *list, Py_ssize_t i) +{ + pair_t *item = list->pairs + i; + return item; +} + + +static inline int +pair_list_grow(pair_list_t *list) +{ + // Grow by one element if needed + Py_ssize_t new_capacity; + pair_t *new_pairs; + + if (list->size < list->capacity) { + return 0; + } + + if (list->pairs == list->buffer) { + new_pairs = PyMem_New(pair_t, MIN_CAPACITY); + memcpy(new_pairs, list->buffer, (size_t)list->capacity * sizeof(pair_t)); + + list->pairs = new_pairs; + list->capacity = MIN_CAPACITY; + return 0; + } else { + new_capacity = list->capacity + CAPACITY_STEP; + new_pairs = PyMem_Resize(list->pairs, pair_t, (size_t)new_capacity); + + if (NULL == new_pairs) { + // Resizing error + return -1; + } + + list->pairs = new_pairs; + list->capacity = new_capacity; + return 0; + } +} + + +static inline int +pair_list_shrink(pair_list_t *list) +{ + // Shrink by one element if needed. + // Optimization is applied to prevent jitter + // (grow-shrink-grow-shrink on adding-removing the single element + // when the buffer is full). + // To prevent this, the buffer is resized if the size is less than the capacity + // by 2*CAPACITY_STEP factor. + // The switch back to embedded buffer is never performed for both reasons: + // the code simplicity and the jitter prevention. + + pair_t *new_pairs; + Py_ssize_t new_capacity; + + if (list->capacity - list->size < 2 * CAPACITY_STEP) { + return 0; + } + new_capacity = list->capacity - CAPACITY_STEP; + if (new_capacity < MIN_CAPACITY) { + return 0; + } + + new_pairs = PyMem_Resize(list->pairs, pair_t, (size_t)new_capacity); + + if (NULL == new_pairs) { + // Resizing error + return -1; + } + + list->pairs = new_pairs; + list->capacity = new_capacity; + + return 0; +} + + +static inline int +_pair_list_init(pair_list_t *list, calc_identity_func calc_identity) +{ + list->pairs = list->buffer; + list->capacity = EMBEDDED_CAPACITY; + list->size = 0; + list->version = NEXT_VERSION(); + list->calc_identity = calc_identity; + return 0; +} + +static inline int +pair_list_init(pair_list_t *list) +{ + return _pair_list_init(list, key_to_str); +} + + +static inline int +ci_pair_list_init(pair_list_t *list) +{ + return _pair_list_init(list, ci_key_to_str); +} + + +static inline void +pair_list_dealloc(pair_list_t *list) +{ + pair_t *pair; + Py_ssize_t pos; + + for (pos = 0; pos < list->size; pos++) { + pair = pair_list_get(list, pos); + + Py_XDECREF(pair->identity); + Py_XDECREF(pair->key); + Py_XDECREF(pair->value); + } + + /* + Strictly speaking, resetting size and capacity and + assigning pairs to buffer is not necessary. + Do it to consistency and idemotency. + The cleanup doesn't hurt performance. + !!! + !!! The buffer deletion is crucial though. + !!! + */ + list->size = 0; + if (list->pairs != list->buffer) { + PyMem_Del(list->pairs); + list->pairs = list->buffer; + list->capacity = EMBEDDED_CAPACITY; + } +} + + +static inline Py_ssize_t +pair_list_len(pair_list_t *list) +{ + return list->size; +} + + +static inline int +_pair_list_add_with_hash(pair_list_t *list, + PyObject *identity, + PyObject *key, + PyObject *value, + Py_hash_t hash) +{ + pair_t *pair; + + if (pair_list_grow(list) < 0) { + return -1; + } + + pair = pair_list_get(list, list->size); + + Py_INCREF(identity); + pair->identity = identity; + + Py_INCREF(key); + pair->key = key; + + Py_INCREF(value); + pair->value = value; + + pair->hash = hash; + + list->version = NEXT_VERSION(); + list->size += 1; + + return 0; +} + + +static inline int +pair_list_add(pair_list_t *list, + PyObject *key, + PyObject *value) +{ + Py_hash_t hash; + PyObject *identity = NULL; + int ret; + + identity = list->calc_identity(key); + if (identity == NULL) { + goto fail; + } + hash = PyObject_Hash(identity); + if (hash == -1) { + goto fail; + } + ret = _pair_list_add_with_hash(list, identity, key, value, hash); + Py_DECREF(identity); + return ret; +fail: + Py_XDECREF(identity); + return -1; +} + + +static inline int +pair_list_del_at(pair_list_t *list, Py_ssize_t pos) +{ + // return 1 on success, -1 on failure + Py_ssize_t tail; + pair_t *pair; + + pair = pair_list_get(list, pos); + Py_DECREF(pair->identity); + Py_DECREF(pair->key); + Py_DECREF(pair->value); + + list->size -= 1; + list->version = NEXT_VERSION(); + + if (list->size == pos) { + // remove from tail, no need to shift body + return 0; + } + + tail = list->size - pos; + // TODO: raise an error if tail < 0 + memmove((void *)pair_list_get(list, pos), + (void *)pair_list_get(list, pos + 1), + sizeof(pair_t) * (size_t)tail); + + return pair_list_shrink(list); +} + + +static inline int +_pair_list_drop_tail(pair_list_t *list, PyObject *identity, Py_hash_t hash, + Py_ssize_t pos) +{ + // return 1 if deleted, 0 if not found + pair_t *pair; + int ret; + int found = 0; + + if (pos >= list->size) { + return 0; + } + + for (; pos < list->size; pos++) { + pair = pair_list_get(list, pos); + if (pair->hash != hash) { + continue; + } + ret = str_cmp(pair->identity, identity); + if (ret > 0) { + if (pair_list_del_at(list, pos) < 0) { + return -1; + } + found = 1; + pos--; + } + else if (ret == -1) { + return -1; + } + } + + return found; +} + +static inline int +_pair_list_del_hash(pair_list_t *list, PyObject *identity, + PyObject *key, Py_hash_t hash) +{ + int ret = _pair_list_drop_tail(list, identity, hash, 0); + + if (ret < 0) { + return -1; + } + else if (ret == 0) { + PyErr_SetObject(PyExc_KeyError, key); + return -1; + } + else { + list->version = NEXT_VERSION(); + return 0; + } +} + + +static inline int +pair_list_del(pair_list_t *list, PyObject *key) +{ + PyObject *identity = NULL; + Py_hash_t hash; + int ret; + + identity = list->calc_identity(key); + if (identity == NULL) { + goto fail; + } + + hash = PyObject_Hash(identity); + if (hash == -1) { + goto fail; + } + + ret = _pair_list_del_hash(list, identity, key, hash); + Py_DECREF(identity); + return ret; +fail: + Py_XDECREF(identity); + return -1; +} + + +static inline uint64_t +pair_list_version(pair_list_t *list) +{ + return list->version; +} + + +static inline int +_pair_list_next(pair_list_t *list, Py_ssize_t *ppos, PyObject **pidentity, + PyObject **pkey, PyObject **pvalue, Py_hash_t *phash) +{ + pair_t *pair; + + if (*ppos >= list->size) { + return 0; + } + + pair = pair_list_get(list, *ppos); + + if (pidentity) { + *pidentity = pair->identity; + } + if (pkey) { + *pkey = pair->key; + } + if (pvalue) { + *pvalue = pair->value; + } + if (phash) { + *phash = pair->hash; + } + + *ppos += 1; + return 1; +} + + +static inline int +pair_list_next(pair_list_t *list, Py_ssize_t *ppos, PyObject **pidentity, + PyObject **pkey, PyObject **pvalue) +{ + Py_hash_t hash; + return _pair_list_next(list, ppos, pidentity, pkey, pvalue, &hash); +} + + +static inline int +pair_list_contains(pair_list_t *list, PyObject *key) +{ + Py_hash_t hash1, hash2; + Py_ssize_t pos = 0; + PyObject *ident = NULL; + PyObject *identity = NULL; + int tmp; + + ident = list->calc_identity(key); + if (ident == NULL) { + goto fail; + } + + hash1 = PyObject_Hash(ident); + if (hash1 == -1) { + goto fail; + } + + while (_pair_list_next(list, &pos, &identity, NULL, NULL, &hash2)) { + if (hash1 != hash2) { + continue; + } + tmp = str_cmp(ident, identity); + if (tmp > 0) { + Py_DECREF(ident); + return 1; + } + else if (tmp < 0) { + goto fail; + } + } + + Py_DECREF(ident); + return 0; +fail: + Py_XDECREF(ident); + return -1; +} + + +static inline PyObject * +pair_list_get_one(pair_list_t *list, PyObject *key) +{ + Py_hash_t hash1, hash2; + Py_ssize_t pos = 0; + PyObject *ident = NULL; + PyObject *identity = NULL; + PyObject *value = NULL; + int tmp; + + ident = list->calc_identity(key); + if (ident == NULL) { + goto fail; + } + + hash1 = PyObject_Hash(ident); + if (hash1 == -1) { + goto fail; + } + + while (_pair_list_next(list, &pos, &identity, NULL, &value, &hash2)) { + if (hash1 != hash2) { + continue; + } + tmp = str_cmp(ident, identity); + if (tmp > 0) { + Py_INCREF(value); + Py_DECREF(ident); + return value; + } + else if (tmp < 0) { + goto fail; + } + } + + Py_DECREF(ident); + PyErr_SetObject(PyExc_KeyError, key); + return NULL; +fail: + Py_XDECREF(ident); + return NULL; +} + + +static inline PyObject * +pair_list_get_all(pair_list_t *list, PyObject *key) +{ + Py_hash_t hash1, hash2; + Py_ssize_t pos = 0; + PyObject *ident = NULL; + PyObject *identity = NULL; + PyObject *value = NULL; + PyObject *res = NULL; + int tmp; + + ident = list->calc_identity(key); + if (ident == NULL) { + goto fail; + } + + hash1 = PyObject_Hash(ident); + if (hash1 == -1) { + goto fail; + } + + while (_pair_list_next(list, &pos, &identity, NULL, &value, &hash2)) { + if (hash1 != hash2) { + continue; + } + tmp = str_cmp(ident, identity); + if (tmp > 0) { + if (res == NULL) { + res = PyList_New(1); + if (res == NULL) { + goto fail; + } + if (PyList_SetItem(res, 0, value) < 0) { + goto fail; + } + Py_INCREF(value); + } + else if (PyList_Append(res, value) < 0) { + goto fail; + } + } + else if (tmp < 0) { + goto fail; + } + } + + if (res == NULL) { + PyErr_SetObject(PyExc_KeyError, key); + } + Py_DECREF(ident); + return res; + +fail: + Py_XDECREF(ident); + Py_XDECREF(res); + return NULL; +} + + +static inline PyObject * +pair_list_set_default(pair_list_t *list, PyObject *key, PyObject *value) +{ + Py_hash_t hash1, hash2; + Py_ssize_t pos = 0; + PyObject *ident = NULL; + PyObject *identity = NULL; + PyObject *value2 = NULL; + int tmp; + + ident = list->calc_identity(key); + if (ident == NULL) { + goto fail; + } + + hash1 = PyObject_Hash(ident); + if (hash1 == -1) { + goto fail; + } + + while (_pair_list_next(list, &pos, &identity, NULL, &value2, &hash2)) { + if (hash1 != hash2) { + continue; + } + tmp = str_cmp(ident, identity); + if (tmp > 0) { + Py_INCREF(value2); + Py_DECREF(ident); + return value2; + } + else if (tmp < 0) { + goto fail; + } + } + + if (_pair_list_add_with_hash(list, ident, key, value, hash1) < 0) { + goto fail; + } + + Py_INCREF(value); + Py_DECREF(ident); + return value; +fail: + Py_XDECREF(ident); + return NULL; +} + + +static inline PyObject * +pair_list_pop_one(pair_list_t *list, PyObject *key) +{ + pair_t *pair; + + Py_hash_t hash; + Py_ssize_t pos; + PyObject *value = NULL; + int tmp; + PyObject *ident = NULL; + + ident = list->calc_identity(key); + if (ident == NULL) { + goto fail; + } + + hash = PyObject_Hash(ident); + if (hash == -1) { + goto fail; + } + + for (pos=0; pos < list->size; pos++) { + pair = pair_list_get(list, pos); + if (pair->hash != hash) { + continue; + } + tmp = str_cmp(ident, pair->identity); + if (tmp > 0) { + value = pair->value; + Py_INCREF(value); + if (pair_list_del_at(list, pos) < 0) { + goto fail; + } + Py_DECREF(ident); + return value; + } + else if (tmp < 0) { + goto fail; + } + } + + PyErr_SetObject(PyExc_KeyError, key); + goto fail; + +fail: + Py_XDECREF(value); + Py_XDECREF(ident); + return NULL; +} + + +static inline PyObject * +pair_list_pop_all(pair_list_t *list, PyObject *key) +{ + Py_hash_t hash; + Py_ssize_t pos; + pair_t *pair; + int tmp; + PyObject *res = NULL; + PyObject *ident = NULL; + + ident = list->calc_identity(key); + if (ident == NULL) { + goto fail; + } + + hash = PyObject_Hash(ident); + if (hash == -1) { + goto fail; + } + + if (list->size == 0) { + PyErr_SetObject(PyExc_KeyError, ident); + goto fail; + } + + for (pos = list->size - 1; pos >= 0; pos--) { + pair = pair_list_get(list, pos); + if (hash != pair->hash) { + continue; + } + tmp = str_cmp(ident, pair->identity); + if (tmp > 0) { + if (res == NULL) { + res = PyList_New(1); + if (res == NULL) { + goto fail; + } + if (PyList_SetItem(res, 0, pair->value) < 0) { + goto fail; + } + Py_INCREF(pair->value); + } else if (PyList_Append(res, pair->value) < 0) { + goto fail; + } + if (pair_list_del_at(list, pos) < 0) { + goto fail; + } + } + else if (tmp < 0) { + goto fail; + } + } + + if (res == NULL) { + PyErr_SetObject(PyExc_KeyError, key); + } else if (PyList_Reverse(res) < 0) { + goto fail; + } + Py_DECREF(ident); + return res; + +fail: + Py_XDECREF(ident); + Py_XDECREF(res); + return NULL; +} + + +static inline PyObject * +pair_list_pop_item(pair_list_t *list) +{ + PyObject *ret; + pair_t *pair; + + if (list->size == 0) { + PyErr_SetString(PyExc_KeyError, "empty multidict"); + return NULL; + } + + pair = pair_list_get(list, 0); + ret = PyTuple_Pack(2, pair->key, pair->value); + if (ret == NULL) { + return NULL; + } + + if (pair_list_del_at(list, 0) < 0) { + Py_DECREF(ret); + return NULL; + } + + return ret; +} + + +static inline int +pair_list_replace(pair_list_t *list, PyObject * key, PyObject *value) +{ + pair_t *pair; + + Py_ssize_t pos; + int tmp; + int found = 0; + + PyObject *identity = NULL; + Py_hash_t hash; + + identity = list->calc_identity(key); + if (identity == NULL) { + goto fail; + } + + hash = PyObject_Hash(identity); + if (hash == -1) { + goto fail; + } + + + for (pos = 0; pos < list->size; pos++) { + pair = pair_list_get(list, pos); + if (hash != pair->hash) { + continue; + } + tmp = str_cmp(identity, pair->identity); + if (tmp > 0) { + found = 1; + Py_INCREF(key); + Py_DECREF(pair->key); + pair->key = key; + Py_INCREF(value); + Py_DECREF(pair->value); + pair->value = value; + break; + } + else if (tmp < 0) { + goto fail; + } + } + + if (!found) { + if (_pair_list_add_with_hash(list, identity, key, value, hash) < 0) { + goto fail; + } + Py_DECREF(identity); + return 0; + } + else { + list->version = NEXT_VERSION(); + if (_pair_list_drop_tail(list, identity, hash, pos+1) < 0) { + goto fail; + } + Py_DECREF(identity); + return 0; + } +fail: + Py_XDECREF(identity); + return -1; +} + + +static inline int +_dict_set_number(PyObject *dict, PyObject *key, Py_ssize_t num) +{ + PyObject *tmp = PyLong_FromSsize_t(num); + if (tmp == NULL) { + return -1; + } + + if (PyDict_SetItem(dict, key, tmp) < 0) { + Py_DECREF(tmp); + return -1; + } + + return 0; +} + + +static inline int +_pair_list_post_update(pair_list_t *list, PyObject* used_keys, Py_ssize_t pos) +{ + pair_t *pair; + PyObject *tmp; + Py_ssize_t num; + + for (; pos < list->size; pos++) { + pair = pair_list_get(list, pos); + tmp = PyDict_GetItem(used_keys, pair->identity); + if (tmp == NULL) { + // not found + continue; + } + + num = PyLong_AsSsize_t(tmp); + if (num == -1) { + if (!PyErr_Occurred()) { + PyErr_SetString(PyExc_RuntimeError, "invalid internal state"); + } + return -1; + } + + if (pos >= num) { + // del self[pos] + if (pair_list_del_at(list, pos) < 0) { + return -1; + } + pos--; + } + } + + list->version = NEXT_VERSION(); + return 0; +} + +// TODO: need refactoring function name +static inline int +_pair_list_update(pair_list_t *list, PyObject *key, + PyObject *value, PyObject *used_keys, + PyObject *identity, Py_hash_t hash) +{ + PyObject *item = NULL; + pair_t *pair = NULL; + Py_ssize_t pos; + int found; + int ident_cmp_res; + + item = PyDict_GetItem(used_keys, identity); + if (item == NULL) { + pos = 0; + } + else { + pos = PyLong_AsSsize_t(item); + if (pos == -1) { + if (!PyErr_Occurred()) { + PyErr_SetString(PyExc_RuntimeError, "invalid internal state"); + } + return -1; + } + } + + found = 0; + for (; pos < list->size; pos++) { + pair = pair_list_get(list, pos); + if (pair->hash != hash) { + continue; + } + + ident_cmp_res = str_cmp(pair->identity, identity); + if (ident_cmp_res > 0) { + Py_INCREF(key); + Py_DECREF(pair->key); + pair->key = key; + + Py_INCREF(value); + Py_DECREF(pair->value); + pair->value = value; + + if (_dict_set_number(used_keys, pair->identity, pos + 1) < 0) { + return -1; + } + + found = 1; + break; + } + else if (ident_cmp_res < 0) { + return -1; + } + } + + if (!found) { + if (_pair_list_add_with_hash(list, identity, key, value, hash) < 0) { + return -1; + } + if (_dict_set_number(used_keys, identity, list->size) < 0) { + return -1; + } + } + + return 0; +} + + +static inline int +pair_list_update(pair_list_t *list, pair_list_t *other) +{ + PyObject *used_keys = NULL; + pair_t *pair = NULL; + + Py_ssize_t pos; + + if (other->size == 0) { + return 0; + } + + used_keys = PyDict_New(); + if (used_keys == NULL) { + return -1; + } + + for (pos = 0; pos < other->size; pos++) { + pair = pair_list_get(other, pos); + if (_pair_list_update(list, pair->key, pair->value, used_keys, + pair->identity, pair->hash) < 0) { + goto fail; + } + } + + if (_pair_list_post_update(list, used_keys, 0) < 0) { + goto fail; + } + + Py_DECREF(used_keys); + return 0; + +fail: + Py_XDECREF(used_keys); + return -1; +} + + +static inline int +pair_list_update_from_seq(pair_list_t *list, PyObject *seq) +{ + PyObject *it = NULL; // iter(seq) + PyObject *fast = NULL; // item as a 2-tuple or 2-list + PyObject *item = NULL; // seq[i] + PyObject *used_keys = NULL; // dict() + + PyObject *key = NULL; + PyObject *value = NULL; + PyObject *identity = NULL; + + Py_hash_t hash; + + Py_ssize_t i; + Py_ssize_t n; + + it = PyObject_GetIter(seq); + if (it == NULL) { + return -1; + } + + used_keys = PyDict_New(); + if (used_keys == NULL) { + goto fail_1; + } + + for (i = 0; ; ++i) { // i - index into seq of current element + fast = NULL; + item = PyIter_Next(it); + if (item == NULL) { + if (PyErr_Occurred()) { + goto fail_1; + } + break; + } + + // Convert item to sequence, and verify length 2. + fast = PySequence_Fast(item, ""); + if (fast == NULL) { + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + PyErr_Format(PyExc_TypeError, + "multidict cannot convert sequence element #%zd" + " to a sequence", + i); + } + goto fail_1; + } + + n = PySequence_Fast_GET_SIZE(fast); + if (n != 2) { + PyErr_Format(PyExc_ValueError, + "multidict update sequence element #%zd " + "has length %zd; 2 is required", + i, n); + goto fail_1; + } + + key = PySequence_Fast_GET_ITEM(fast, 0); + value = PySequence_Fast_GET_ITEM(fast, 1); + Py_INCREF(key); + Py_INCREF(value); + + identity = list->calc_identity(key); + if (identity == NULL) { + goto fail_1; + } + + hash = PyObject_Hash(identity); + if (hash == -1) { + goto fail_1; + } + + if (_pair_list_update(list, key, value, used_keys, identity, hash) < 0) { + goto fail_1; + } + + Py_DECREF(key); + Py_DECREF(value); + Py_DECREF(fast); + Py_DECREF(item); + Py_DECREF(identity); + } + + if (_pair_list_post_update(list, used_keys, 0) < 0) { + goto fail_2; + } + + Py_DECREF(it); + Py_DECREF(used_keys); + return 0; + +fail_1: + Py_XDECREF(key); + Py_XDECREF(value); + Py_XDECREF(fast); + Py_XDECREF(item); + Py_XDECREF(identity); + +fail_2: + Py_XDECREF(it); + Py_XDECREF(used_keys); + return -1; +} + +static inline int +pair_list_eq_to_mapping(pair_list_t *list, PyObject *other) +{ + PyObject *key = NULL; + PyObject *avalue = NULL; + PyObject *bvalue; + + Py_ssize_t pos, other_len; + + int eq; + + if (!PyMapping_Check(other)) { + PyErr_Format(PyExc_TypeError, + "other argument must be a mapping, not %s", + Py_TYPE(other)->tp_name); + return -1; + } + + other_len = PyMapping_Size(other); + if (other_len < 0) { + return -1; + } + if (pair_list_len(list) != other_len) { + return 0; + } + + pos = 0; + while (pair_list_next(list, &pos, NULL, &key, &avalue)) { + bvalue = PyObject_GetItem(other, key); + if (bvalue == NULL) { + if (PyErr_ExceptionMatches(PyExc_KeyError)) { + PyErr_Clear(); + return 0; + } + return -1; + } + + eq = PyObject_RichCompareBool(avalue, bvalue, Py_EQ); + Py_DECREF(bvalue); + + if (eq <= 0) { + return eq; + } + } + + return 1; +} + + +/***********************************************************************/ + +static inline int +pair_list_traverse(pair_list_t *list, visitproc visit, void *arg) +{ + pair_t *pair = NULL; + Py_ssize_t pos; + + for (pos = 0; pos < list->size; pos++) { + pair = pair_list_get(list, pos); + // Don't need traverse the identity: it is a terminal + Py_VISIT(pair->key); + Py_VISIT(pair->value); + } + + return 0; +} + + +static inline int +pair_list_clear(pair_list_t *list) +{ + pair_t *pair = NULL; + Py_ssize_t pos; + + if (list->size == 0) { + return 0; + } + + list->version = NEXT_VERSION(); + for (pos = 0; pos < list->size; pos++) { + pair = pair_list_get(list, pos); + Py_CLEAR(pair->key); + Py_CLEAR(pair->identity); + Py_CLEAR(pair->value); + } + list->size = 0; + if (list->pairs != list->buffer) { + PyMem_Del(list->pairs); + list->pairs = list->buffer; + } + + return 0; +} + + +#ifdef __cplusplus +} +#endif +#endif diff --git a/multidict/_multilib/views.h b/multidict/_multilib/views.h new file mode 100644 index 0000000..5b1ebfe --- /dev/null +++ b/multidict/_multilib/views.h @@ -0,0 +1,464 @@ +#ifndef _MULTIDICT_VIEWS_H +#define _MULTIDICT_VIEWS_H + +#ifdef __cplusplus +extern "C" { +#endif + +static PyTypeObject multidict_itemsview_type; +static PyTypeObject multidict_valuesview_type; +static PyTypeObject multidict_keysview_type; + +static PyObject *viewbaseset_richcmp_func; +static PyObject *viewbaseset_and_func; +static PyObject *viewbaseset_or_func; +static PyObject *viewbaseset_sub_func; +static PyObject *viewbaseset_xor_func; + +static PyObject *abc_itemsview_register_func; +static PyObject *abc_keysview_register_func; +static PyObject *abc_valuesview_register_func; + +static PyObject *itemsview_isdisjoint_func; +static PyObject *itemsview_repr_func; + +static PyObject *keysview_repr_func; +static PyObject *keysview_isdisjoint_func; + +static PyObject *valuesview_repr_func; + +typedef struct { + PyObject_HEAD + PyObject *md; +} _Multidict_ViewObject; + + +/********** Base **********/ + +static inline void +_init_view(_Multidict_ViewObject *self, PyObject *md) +{ + Py_INCREF(md); + self->md = md; +} + +static inline void +multidict_view_dealloc(_Multidict_ViewObject *self) +{ + PyObject_GC_UnTrack(self); + Py_XDECREF(self->md); + PyObject_GC_Del(self); +} + +static inline int +multidict_view_traverse(_Multidict_ViewObject *self, visitproc visit, void *arg) +{ + Py_VISIT(self->md); + return 0; +} + +static inline int +multidict_view_clear(_Multidict_ViewObject *self) +{ + Py_CLEAR(self->md); + return 0; +} + +static inline Py_ssize_t +multidict_view_len(_Multidict_ViewObject *self) +{ + return pair_list_len(&((MultiDictObject*)self->md)->pairs); +} + +static inline PyObject * +multidict_view_richcompare(PyObject *self, PyObject *other, int op) +{ + PyObject *ret; + PyObject *op_obj = PyLong_FromLong(op); + if (op_obj == NULL) { + return NULL; + } + ret = PyObject_CallFunctionObjArgs( + viewbaseset_richcmp_func, self, other, op_obj, NULL); + Py_DECREF(op_obj); + return ret; +} + +static inline PyObject * +multidict_view_and(PyObject *self, PyObject *other) +{ + return PyObject_CallFunctionObjArgs( + viewbaseset_and_func, self, other, NULL); +} + +static inline PyObject * +multidict_view_or(PyObject *self, PyObject *other) +{ + return PyObject_CallFunctionObjArgs( + viewbaseset_or_func, self, other, NULL); +} + +static inline PyObject * +multidict_view_sub(PyObject *self, PyObject *other) +{ + return PyObject_CallFunctionObjArgs( + viewbaseset_sub_func, self, other, NULL); +} + +static inline PyObject * +multidict_view_xor(PyObject *self, PyObject *other) +{ + return PyObject_CallFunctionObjArgs( + viewbaseset_xor_func, self, other, NULL); +} + +static PyNumberMethods multidict_view_as_number = { + .nb_subtract = (binaryfunc)multidict_view_sub, + .nb_and = (binaryfunc)multidict_view_and, + .nb_xor = (binaryfunc)multidict_view_xor, + .nb_or = (binaryfunc)multidict_view_or, +}; + +/********** Items **********/ + +static inline PyObject * +multidict_itemsview_new(PyObject *md) +{ + _Multidict_ViewObject *mv = PyObject_GC_New( + _Multidict_ViewObject, &multidict_itemsview_type); + if (mv == NULL) { + return NULL; + } + + _init_view(mv, md); + + PyObject_GC_Track(mv); + return (PyObject *)mv; +} + +static inline PyObject * +multidict_itemsview_iter(_Multidict_ViewObject *self) +{ + return multidict_items_iter_new((MultiDictObject*)self->md); +} + +static inline PyObject * +multidict_itemsview_repr(_Multidict_ViewObject *self) +{ + return PyObject_CallFunctionObjArgs( + itemsview_repr_func, self, NULL); +} + +static inline PyObject * +multidict_itemsview_isdisjoint(_Multidict_ViewObject *self, PyObject *other) +{ + return PyObject_CallFunctionObjArgs( + itemsview_isdisjoint_func, self, other, NULL); +} + +PyDoc_STRVAR(itemsview_isdisjoint_doc, + "Return True if two sets have a null intersection."); + +static PyMethodDef multidict_itemsview_methods[] = { + { + "isdisjoint", + (PyCFunction)multidict_itemsview_isdisjoint, + METH_O, + itemsview_isdisjoint_doc + }, + { + NULL, + NULL + } /* sentinel */ +}; + +static inline int +multidict_itemsview_contains(_Multidict_ViewObject *self, PyObject *obj) +{ + PyObject *akey = NULL, + *aval = NULL, + *bkey = NULL, + *bval = NULL, + *iter = NULL, + *item = NULL; + int ret1, ret2; + + if (!PyTuple_Check(obj) || PyTuple_GET_SIZE(obj) != 2) { + return 0; + } + + bkey = PyTuple_GET_ITEM(obj, 0); + bval = PyTuple_GET_ITEM(obj, 1); + + iter = multidict_itemsview_iter(self); + if (iter == NULL) { + return 0; + } + + while ((item = PyIter_Next(iter)) != NULL) { + akey = PyTuple_GET_ITEM(item, 0); + aval = PyTuple_GET_ITEM(item, 1); + + ret1 = PyObject_RichCompareBool(akey, bkey, Py_EQ); + if (ret1 < 0) { + Py_DECREF(iter); + Py_DECREF(item); + return -1; + } + ret2 = PyObject_RichCompareBool(aval, bval, Py_EQ); + if (ret2 < 0) { + Py_DECREF(iter); + Py_DECREF(item); + return -1; + } + if (ret1 > 0 && ret2 > 0) + { + Py_DECREF(iter); + Py_DECREF(item); + return 1; + } + + Py_DECREF(item); + } + + Py_DECREF(iter); + + if (PyErr_Occurred()) { + return -1; + } + + return 0; +} + +static PySequenceMethods multidict_itemsview_as_sequence = { + .sq_length = (lenfunc)multidict_view_len, + .sq_contains = (objobjproc)multidict_itemsview_contains, +}; + +static PyTypeObject multidict_itemsview_type = { + PyVarObject_HEAD_INIT(DEFERRED_ADDRESS(&PyType_Type), 0) + "multidict._multidict._ItemsView", /* tp_name */ + sizeof(_Multidict_ViewObject), /* tp_basicsize */ + .tp_dealloc = (destructor)multidict_view_dealloc, + .tp_repr = (reprfunc)multidict_itemsview_repr, + .tp_as_number = &multidict_view_as_number, + .tp_as_sequence = &multidict_itemsview_as_sequence, + .tp_getattro = PyObject_GenericGetAttr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_traverse = (traverseproc)multidict_view_traverse, + .tp_clear = (inquiry)multidict_view_clear, + .tp_richcompare = multidict_view_richcompare, + .tp_iter = (getiterfunc)multidict_itemsview_iter, + .tp_methods = multidict_itemsview_methods, +}; + + +/********** Keys **********/ + +static inline PyObject * +multidict_keysview_new(PyObject *md) +{ + _Multidict_ViewObject *mv = PyObject_GC_New( + _Multidict_ViewObject, &multidict_keysview_type); + if (mv == NULL) { + return NULL; + } + + _init_view(mv, md); + + PyObject_GC_Track(mv); + return (PyObject *)mv; +} + +static inline PyObject * +multidict_keysview_iter(_Multidict_ViewObject *self) +{ + return multidict_keys_iter_new(((MultiDictObject*)self->md)); +} + +static inline PyObject * +multidict_keysview_repr(_Multidict_ViewObject *self) +{ + return PyObject_CallFunctionObjArgs( + keysview_repr_func, self, NULL); +} + +static inline PyObject * +multidict_keysview_isdisjoint(_Multidict_ViewObject *self, PyObject *other) +{ + return PyObject_CallFunctionObjArgs( + keysview_isdisjoint_func, self, other, NULL); +} + +PyDoc_STRVAR(keysview_isdisjoint_doc, + "Return True if two sets have a null intersection."); + +static PyMethodDef multidict_keysview_methods[] = { + { + "isdisjoint", + (PyCFunction)multidict_keysview_isdisjoint, + METH_O, + keysview_isdisjoint_doc + }, + { + NULL, + NULL + } /* sentinel */ +}; + +static inline int +multidict_keysview_contains(_Multidict_ViewObject *self, PyObject *key) +{ + return pair_list_contains(&((MultiDictObject*)self->md)->pairs, key); +} + +static PySequenceMethods multidict_keysview_as_sequence = { + .sq_length = (lenfunc)multidict_view_len, + .sq_contains = (objobjproc)multidict_keysview_contains, +}; + +static PyTypeObject multidict_keysview_type = { + PyVarObject_HEAD_INIT(DEFERRED_ADDRESS(&PyType_Type), 0) + "multidict._multidict._KeysView", /* tp_name */ + sizeof(_Multidict_ViewObject), /* tp_basicsize */ + .tp_dealloc = (destructor)multidict_view_dealloc, + .tp_repr = (reprfunc)multidict_keysview_repr, + .tp_as_number = &multidict_view_as_number, + .tp_as_sequence = &multidict_keysview_as_sequence, + .tp_getattro = PyObject_GenericGetAttr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_traverse = (traverseproc)multidict_view_traverse, + .tp_clear = (inquiry)multidict_view_clear, + .tp_richcompare = multidict_view_richcompare, + .tp_iter = (getiterfunc)multidict_keysview_iter, + .tp_methods = multidict_keysview_methods, +}; + + +/********** Values **********/ + +static inline PyObject * +multidict_valuesview_new(PyObject *md) +{ + _Multidict_ViewObject *mv = PyObject_GC_New( + _Multidict_ViewObject, &multidict_valuesview_type); + if (mv == NULL) { + return NULL; + } + + _init_view(mv, md); + + PyObject_GC_Track(mv); + return (PyObject *)mv; +} + +static inline PyObject * +multidict_valuesview_iter(_Multidict_ViewObject *self) +{ + return multidict_values_iter_new(((MultiDictObject*)self->md)); +} + +static inline PyObject * +multidict_valuesview_repr(_Multidict_ViewObject *self) +{ + return PyObject_CallFunctionObjArgs( + valuesview_repr_func, self, NULL); +} + +static PySequenceMethods multidict_valuesview_as_sequence = { + .sq_length = (lenfunc)multidict_view_len, +}; + +static PyTypeObject multidict_valuesview_type = { + PyVarObject_HEAD_INIT(DEFERRED_ADDRESS(&PyType_Type), 0) + "multidict._multidict._ValuesView", /* tp_name */ + sizeof(_Multidict_ViewObject), /* tp_basicsize */ + .tp_dealloc = (destructor)multidict_view_dealloc, + .tp_repr = (reprfunc)multidict_valuesview_repr, + .tp_as_sequence = &multidict_valuesview_as_sequence, + .tp_getattro = PyObject_GenericGetAttr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_traverse = (traverseproc)multidict_view_traverse, + .tp_clear = (inquiry)multidict_view_clear, + .tp_iter = (getiterfunc)multidict_valuesview_iter, +}; + + +static inline int +multidict_views_init() +{ + PyObject *reg_func_call_result = NULL; + PyObject *module = PyImport_ImportModule("multidict._multidict_base"); + if (module == NULL) { + goto fail; + } + +#define GET_MOD_ATTR(VAR, NAME) \ + VAR = PyObject_GetAttrString(module, NAME); \ + if (VAR == NULL) { \ + goto fail; \ + } + + GET_MOD_ATTR(viewbaseset_richcmp_func, "_viewbaseset_richcmp"); + GET_MOD_ATTR(viewbaseset_and_func, "_viewbaseset_and"); + GET_MOD_ATTR(viewbaseset_or_func, "_viewbaseset_or"); + GET_MOD_ATTR(viewbaseset_sub_func, "_viewbaseset_sub"); + GET_MOD_ATTR(viewbaseset_xor_func, "_viewbaseset_xor"); + + GET_MOD_ATTR(abc_itemsview_register_func, "_abc_itemsview_register"); + GET_MOD_ATTR(abc_keysview_register_func, "_abc_keysview_register"); + GET_MOD_ATTR(abc_valuesview_register_func, "_abc_valuesview_register"); + + GET_MOD_ATTR(itemsview_repr_func, "_itemsview_isdisjoint"); + GET_MOD_ATTR(itemsview_repr_func, "_itemsview_repr"); + + GET_MOD_ATTR(keysview_repr_func, "_keysview_repr"); + GET_MOD_ATTR(keysview_isdisjoint_func, "_keysview_isdisjoint"); + + GET_MOD_ATTR(valuesview_repr_func, "_valuesview_repr"); + + if (PyType_Ready(&multidict_itemsview_type) < 0 || + PyType_Ready(&multidict_valuesview_type) < 0 || + PyType_Ready(&multidict_keysview_type) < 0) + { + goto fail; + } + + // abc.ItemsView.register(_ItemsView) + reg_func_call_result = PyObject_CallFunctionObjArgs( + abc_itemsview_register_func, (PyObject*)&multidict_itemsview_type, NULL); + if (reg_func_call_result == NULL) { + goto fail; + } + Py_DECREF(reg_func_call_result); + + // abc.KeysView.register(_KeysView) + reg_func_call_result = PyObject_CallFunctionObjArgs( + abc_keysview_register_func, (PyObject*)&multidict_keysview_type, NULL); + if (reg_func_call_result == NULL) { + goto fail; + } + Py_DECREF(reg_func_call_result); + + // abc.ValuesView.register(_KeysView) + reg_func_call_result = PyObject_CallFunctionObjArgs( + abc_valuesview_register_func, (PyObject*)&multidict_valuesview_type, NULL); + if (reg_func_call_result == NULL) { + goto fail; + } + Py_DECREF(reg_func_call_result); + + Py_DECREF(module); + return 0; + +fail: + Py_CLEAR(module); + return -1; + +#undef GET_MOD_ATTR +} + +#ifdef __cplusplus +} +#endif +#endif diff --git a/multidict/py.typed b/multidict/py.typed new file mode 100644 index 0000000..dfe8cc0 --- /dev/null +++ b/multidict/py.typed @@ -0,0 +1 @@ +PEP-561 marker. \ No newline at end of file diff --git a/mysql/__init__.py b/mysql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mysql/connector/__init__.py b/mysql/connector/__init__.py new file mode 100644 index 0000000..86878f9 --- /dev/null +++ b/mysql/connector/__init__.py @@ -0,0 +1,123 @@ +# Copyright (c) 2009, 2022, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""MySQL Connector/Python - MySQL driver written in Python.""" + +try: + from .connection_cext import CMySQLConnection +except ImportError: + HAVE_CEXT = False +else: + HAVE_CEXT = True + + +from . import version +from .connection import MySQLConnection +from .constants import CharacterSet, ClientFlag, FieldFlag, FieldType, RefreshOption +from .dbapi import ( + BINARY, + DATETIME, + NUMBER, + ROWID, + STRING, + Binary, + Date, + DateFromTicks, + Time, + TimeFromTicks, + Timestamp, + TimestampFromTicks, + apilevel, + paramstyle, + threadsafety, +) +from .errors import ( # pylint: disable=redefined-builtin + DatabaseError, + DataError, + Error, + IntegrityError, + InterfaceError, + InternalError, + NotSupportedError, + OperationalError, + PoolError, + ProgrammingError, + Warning, + custom_error_exception, +) +from .pooling import connect + +Connect = connect + +__version_info__ = version.VERSION +__version__ = version.VERSION_TEXT + +__all__ = [ + "MySQLConnection", + "Connect", + "custom_error_exception", + # Some useful constants + "FieldType", + "FieldFlag", + "ClientFlag", + "CharacterSet", + "RefreshOption", + "HAVE_CEXT", + # Error handling + "Error", + "Warning", + "InterfaceError", + "DatabaseError", + "NotSupportedError", + "DataError", + "IntegrityError", + "PoolError", + "ProgrammingError", + "OperationalError", + "InternalError", + # DBAPI PEP 249 required exports + "connect", + "apilevel", + "threadsafety", + "paramstyle", + "Date", + "Time", + "Timestamp", + "Binary", + "DateFromTicks", + "DateFromTicks", + "TimestampFromTicks", + "TimeFromTicks", + "STRING", + "BINARY", + "NUMBER", + "DATETIME", + "ROWID", + # C Extension + "CMySQLConnection", +] diff --git a/mysql/connector/abstracts.py b/mysql/connector/abstracts.py new file mode 100644 index 0000000..90fafcf --- /dev/null +++ b/mysql/connector/abstracts.py @@ -0,0 +1,1806 @@ +# Copyright (c) 2014, 2023, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# mypy: disable-error-code="assignment,attr-defined" + +"""Module gathering all abstract base classes.""" + +from __future__ import annotations + +import importlib +import os +import re +import weakref + +from abc import ABC, abstractmethod +from datetime import date, datetime, time, timedelta +from decimal import Decimal +from inspect import signature +from time import sleep +from types import TracebackType +from typing import ( + Any, + BinaryIO, + Callable, + Dict, + Generator, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + Union, +) + +TLS_V1_3_SUPPORTED = False +try: + import ssl + + if hasattr(ssl, "HAS_TLSv1_3") and ssl.HAS_TLSv1_3: + TLS_V1_3_SUPPORTED = True +except ImportError: + # If import fails, we don't have SSL support. + pass + +from .constants import ( + CONN_ATTRS_DN, + DEFAULT_CONFIGURATION, + DEPRECATED_TLS_VERSIONS, + OPENSSL_CS_NAMES, + TLS_CIPHER_SUITES, + TLS_VERSIONS, + CharacterSet, + ClientFlag, +) +from .conversion import MySQLConverter, MySQLConverterBase +from .errors import ( + Error, + InterfaceError, + NotSupportedError, + OperationalError, + ProgrammingError, +) +from .opentelemetry.constants import ( + CONNECTION_SPAN_NAME, + OPTION_CNX_SPAN, + OPTION_CNX_TRACER, + OTEL_ENABLED, +) + +if OTEL_ENABLED: + from .opentelemetry.instrumentation import ( + end_span, + record_exception_event, + set_connection_span_attrs, + trace, + ) + +from .optionfiles import read_option_files +from .types import ( + ConnAttrsType, + DescriptionType, + HandShakeType, + StrOrBytes, + SupportedMysqlBinaryProtocolTypes, + WarningType, +) + +NAMED_TUPLE_CACHE: weakref.WeakValueDictionary[Any, Any] = weakref.WeakValueDictionary() + +DUPLICATED_IN_LIST_ERROR = ( + "The '{list}' list must not contain repeated values, the value " + "'{value}' is duplicated." +) + +TLS_VERSION_ERROR = ( + "The given tls_version: '{}' is not recognized as a valid " + "TLS protocol version (should be one of {})." +) + +TLS_VERSION_DEPRECATED_ERROR = ( + "The given tls_version: '{}' are no longer allowed (should be one of {})." +) + +TLS_VER_NO_SUPPORTED = ( + "No supported TLS protocol version found in the 'tls-versions' list '{}'. " +) + +KRB_SERVICE_PINCIPAL_ERROR = ( + 'Option "krb_service_principal" {error}, must be a string in the form ' + '"primary/instance@realm" e.g "ldap/ldapauth@MYSQL.COM" where "@realm" ' + "is optional and if it is not given will be assumed to belong to the " + "default realm, as configured in the krb5.conf file." +) + +MYSQL_PY_TYPES = ( + Decimal, + bytes, + date, + datetime, + float, + int, + str, + time, + timedelta, +) + + +class MySQLConnectionAbstract(ABC): + """Abstract class for classes connecting to a MySQL server""" + + def __init__(self) -> None: + """Initialize""" + # opentelemetry related + self._tracer: Any = None + self._span: Any = None + self.otel_context_propagation: bool = True + + self._client_flags: int = ClientFlag.get_default() + self._charset_id: int = 45 + self._sql_mode: Optional[str] = None + self._time_zone: Optional[str] = None + self._autocommit: bool = False + self._server_version: Optional[Tuple[int, ...]] = None + self._handshake: Optional[HandShakeType] = None + self._conn_attrs: ConnAttrsType = {} + + self._user: str = "" + self._password: str = "" + self._password1: str = "" + self._password2: str = "" + self._password3: str = "" + self._database: str = "" + self._host: str = "127.0.0.1" + self._port: int = 3306 + self._unix_socket: Optional[str] = None + self._client_host: str = "" + self._client_port: int = 0 + self._ssl: Dict[str, Optional[Union[str, bool, List[str]]]] = {} + self._ssl_disabled: bool = DEFAULT_CONFIGURATION["ssl_disabled"] + self._force_ipv6: bool = False + self._oci_config_file: Optional[str] = None + self._oci_config_profile: Optional[str] = None + self._fido_callback: Optional[Union[str, Callable]] = None + self._krb_service_principal: Optional[str] = None + + self._use_unicode: bool = True + self._get_warnings: bool = False + self._raise_on_warnings: bool = False + self._connection_timeout: Optional[int] = DEFAULT_CONFIGURATION[ + "connect_timeout" + ] + self._buffered: bool = False + self._unread_result: bool = False + self._have_next_result: bool = False + self._raw: bool = False + self._in_transaction: bool = False + self._allow_local_infile: bool = DEFAULT_CONFIGURATION["allow_local_infile"] + self._allow_local_infile_in_path: Optional[str] = DEFAULT_CONFIGURATION[ + "allow_local_infile_in_path" + ] + + self._prepared_statements: Any = None + self._query_attrs: Dict[str, Any] = {} + + self._ssl_active: bool = False + self._auth_plugin: Optional[str] = None + self._auth_plugin_class: Optional[str] = None + self._pool_config_version: Any = None + self.converter: Optional[MySQLConverter] = None + self._converter_class: Optional[Type[MySQLConverter]] = None + self._converter_str_fallback: bool = False + self._compress: bool = False + + self._consume_results: bool = False + self._init_command: Optional[str] = None + + def __enter__(self) -> MySQLConnectionAbstract: + return self + + def __exit__( + self, + exc_type: Type[BaseException], + exc_value: BaseException, + traceback: TracebackType, + ) -> None: + self.close() + + def get_self(self) -> MySQLConnectionAbstract: + """Return self for weakref.proxy + + This method is used when the original object is needed when using + weakref.proxy. + """ + return self + + @property + def is_secure(self) -> bool: + """Return True if is a secure connection.""" + return self._ssl_active or ( + self._unix_socket is not None and os.name == "posix" + ) + + @property + def have_next_result(self) -> bool: + """Return if have next result.""" + return self._have_next_result + + @property + def query_attrs(self) -> List[Tuple[str, Any]]: + """Return query attributes list.""" + return list(self._query_attrs.items()) + + def query_attrs_append( + self, value: Tuple[str, SupportedMysqlBinaryProtocolTypes] + ) -> None: + """Add element to the query attributes list. + + If an element in the query attributes list already matches + the attribute name provided, the new element will NOT be added. + """ + attr_name, attr_value = value + if attr_name not in self._query_attrs: + self._query_attrs[attr_name] = attr_value + + def query_attrs_remove(self, name: str) -> Any: + """Remove element by name from the query attributes list. + + If no match, `None` is returned; else the corresponding value is returned. + """ + return self._query_attrs.pop(name, None) + + def query_attrs_clear(self) -> None: + """Clear query attributes list.""" + self._query_attrs = {} + + def _validate_tls_ciphersuites(self) -> None: + """Validates the tls_ciphersuites option.""" + tls_ciphersuites = [] + tls_cs = self._ssl["tls_ciphersuites"] + + if isinstance(tls_cs, str): + if not (tls_cs.startswith("[") and tls_cs.endswith("]")): + raise AttributeError( + f"tls_ciphersuites must be a list, found: '{tls_cs}'" + ) + tls_css = tls_cs[1:-1].split(",") + if not tls_css: + raise AttributeError( + "No valid cipher suite found in 'tls_ciphersuites' list" + ) + for _tls_cs in tls_css: + _tls_cs = tls_cs.strip().upper() + if _tls_cs: + tls_ciphersuites.append(_tls_cs) + + elif isinstance(tls_cs, (list, set)): + tls_ciphersuites = [tls_cs for tls_cs in tls_cs if tls_cs] + else: + raise AttributeError( + "tls_ciphersuites should be a list with one or more " + f"ciphersuites. Found: '{tls_cs}'" + ) + + tls_versions = ( + TLS_VERSIONS[:] + if self._ssl.get("tls_versions", None) is None + else self._ssl["tls_versions"][:] # type: ignore[index] + ) + + # A newer TLS version can use a cipher introduced on + # an older version. + tls_versions.sort(reverse=True) # type: ignore[union-attr] + newer_tls_ver = tls_versions[0] + # translated_names[0] belongs to TLSv1, TLSv1.1 and TLSv1.2 + # translated_names[1] are TLSv1.3 only + translated_names: List[List[str]] = [[], []] + iani_cipher_suites_names = {} + ossl_cipher_suites_names: List[str] = [] + + # Old ciphers can work with new TLS versions. + # Find all the ciphers introduced on previous TLS versions. + for tls_ver in TLS_VERSIONS[: TLS_VERSIONS.index(newer_tls_ver) + 1]: + iani_cipher_suites_names.update(TLS_CIPHER_SUITES[tls_ver]) + ossl_cipher_suites_names.extend(OPENSSL_CS_NAMES[tls_ver]) + + for name in tls_ciphersuites: + if "-" in name and name in ossl_cipher_suites_names: + if name in OPENSSL_CS_NAMES["TLSv1.3"]: + translated_names[1].append(name) + else: + translated_names[0].append(name) + elif name in iani_cipher_suites_names: + translated_name = iani_cipher_suites_names[name] + if translated_name in translated_names: + raise AttributeError( + DUPLICATED_IN_LIST_ERROR.format( + list="tls_ciphersuites", value=translated_name + ) + ) + if name in TLS_CIPHER_SUITES["TLSv1.3"]: + translated_names[1].append(iani_cipher_suites_names[name]) + else: + translated_names[0].append(iani_cipher_suites_names[name]) + else: + raise AttributeError( + f"The value '{name}' in tls_ciphersuites is not a valid " + "cipher suite" + ) + if not translated_names[0] and not translated_names[1]: + raise AttributeError( + "No valid cipher suite found in the 'tls_ciphersuites' list" + ) + + self._ssl["tls_ciphersuites"] = [ + ":".join(translated_names[0]), + ":".join(translated_names[1]), + ] + + def _validate_tls_versions(self) -> None: + """Validates the tls_versions option.""" + tls_versions = [] + tls_version = self._ssl["tls_versions"] + + if isinstance(tls_version, str): + if not (tls_version.startswith("[") and tls_version.endswith("]")): + raise AttributeError( + f"tls_versions must be a list, found: '{tls_version}'" + ) + tls_vers = tls_version[1:-1].split(",") + for tls_ver in tls_vers: + tls_version = tls_ver.strip() + if tls_version == "": + continue + if tls_version in tls_versions: + raise AttributeError( + DUPLICATED_IN_LIST_ERROR.format( + list="tls_versions", value=tls_version + ) + ) + tls_versions.append(tls_version) + if tls_vers == ["TLSv1.3"] and not TLS_V1_3_SUPPORTED: + raise AttributeError( + TLS_VER_NO_SUPPORTED.format(tls_version, TLS_VERSIONS) + ) + elif isinstance(tls_version, list): + if not tls_version: + raise AttributeError( + "At least one TLS protocol version must be specified in " + "'tls_versions' list" + ) + for tls_ver in tls_version: + if tls_ver in tls_versions: + raise AttributeError( + DUPLICATED_IN_LIST_ERROR.format( + list="tls_versions", value=tls_ver + ) + ) + tls_versions.append(tls_ver) + elif isinstance(tls_version, set): + for tls_ver in tls_version: + tls_versions.append(tls_ver) + else: + raise AttributeError( + "tls_versions should be a list with one or more of versions " + f"in {', '.join(TLS_VERSIONS)}. found: '{tls_versions}'" + ) + + if not tls_versions: + raise AttributeError( + "At least one TLS protocol version must be specified " + "in 'tls_versions' list when this option is given" + ) + + use_tls_versions = [] + deprecated_tls_versions = [] + invalid_tls_versions = [] + for tls_ver in tls_versions: + if tls_ver in TLS_VERSIONS: + use_tls_versions.append(tls_ver) + if tls_ver in DEPRECATED_TLS_VERSIONS: + deprecated_tls_versions.append(tls_ver) + else: + invalid_tls_versions.append(tls_ver) + + if use_tls_versions: + if use_tls_versions == ["TLSv1.3"] and not TLS_V1_3_SUPPORTED: + raise NotSupportedError( + TLS_VER_NO_SUPPORTED.format(tls_version, TLS_VERSIONS) + ) + use_tls_versions.sort() + self._ssl["tls_versions"] = use_tls_versions + elif deprecated_tls_versions: + raise NotSupportedError( + TLS_VERSION_DEPRECATED_ERROR.format( + deprecated_tls_versions, TLS_VERSIONS + ) + ) + elif invalid_tls_versions: + raise AttributeError(TLS_VERSION_ERROR.format(tls_ver, TLS_VERSIONS)) + + @property + def user(self) -> str: + """User used while connecting to MySQL""" + return self._user + + @property + def server_host(self) -> str: + """MySQL server IP address or name""" + return self._host + + @property + def server_port(self) -> int: + "MySQL server TCP/IP port" + return self._port + + @property + def unix_socket(self) -> Optional[str]: + "MySQL Unix socket file location" + return self._unix_socket + + @property + @abstractmethod + def database(self) -> str: + """Get the current database""" + + @database.setter + def database(self, value: str) -> None: + """Set the current database""" + self.cmd_query(f"USE {value}") + + @property + def can_consume_results(self) -> bool: + """Returns whether to consume results""" + return self._consume_results + + @can_consume_results.setter + def can_consume_results(self, value: bool) -> None: + """Set if can consume results.""" + assert isinstance(value, bool) + self._consume_results = value + + @property + def pool_config_version(self) -> Any: + """Return the pool configuration version""" + return self._pool_config_version + + @pool_config_version.setter + def pool_config_version(self, value: Any) -> None: + """Set the pool configuration version""" + self._pool_config_version = value + + def config(self, **kwargs: Any) -> None: + """Configure the MySQL Connection + + This method allows you to configure the MySQLConnection instance. + + Raises on errors. + """ + # opentelemetry related + self._span = kwargs.pop(OPTION_CNX_SPAN, None) + self._tracer = kwargs.pop(OPTION_CNX_TRACER, None) + + config = kwargs.copy() + if "dsn" in config: + raise NotSupportedError("Data source name is not supported") + + # Read option files + config = read_option_files(**config) + + # Configure how we handle MySQL warnings + try: + self.get_warnings = config["get_warnings"] + del config["get_warnings"] + except KeyError: + pass # Leave what was set or default + try: + self.raise_on_warnings = config["raise_on_warnings"] + del config["raise_on_warnings"] + except KeyError: + pass # Leave what was set or default + + # Configure client flags + try: + default = ClientFlag.get_default() + self.set_client_flags(config["client_flags"] or default) + del config["client_flags"] + except KeyError: + pass # Missing client_flags-argument is OK + + try: + if config["compress"]: + self._compress = True + self.set_client_flags([ClientFlag.COMPRESS]) + except KeyError: + pass # Missing compress argument is OK + + self._allow_local_infile = config.get( + "allow_local_infile", DEFAULT_CONFIGURATION["allow_local_infile"] + ) + self._allow_local_infile_in_path = config.get( + "allow_local_infile_in_path", + DEFAULT_CONFIGURATION["allow_local_infile_in_path"], + ) + infile_in_path = None + if self._allow_local_infile_in_path: + infile_in_path = os.path.abspath(self._allow_local_infile_in_path) + if ( + infile_in_path + and os.path.exists(infile_in_path) + and not os.path.isdir(infile_in_path) + or os.path.islink(infile_in_path) + ): + raise AttributeError("allow_local_infile_in_path must be a directory") + if self._allow_local_infile or self._allow_local_infile_in_path: + self.set_client_flags([ClientFlag.LOCAL_FILES]) + else: + self.set_client_flags([-ClientFlag.LOCAL_FILES]) + + try: + if not config["consume_results"]: + self._consume_results = False + else: + self._consume_results = True + except KeyError: + self._consume_results = False + + # Configure auth_plugin + try: + self._auth_plugin = config["auth_plugin"] + del config["auth_plugin"] + except KeyError: + self._auth_plugin = "" + + # Configure character set and collation + if "charset" in config or "collation" in config: + try: + charset = config["charset"] + del config["charset"] + except KeyError: + charset = None + try: + collation = config["collation"] + del config["collation"] + except KeyError: + collation = None + self._charset_id = CharacterSet.get_charset_info(charset, collation)[0] + + # Set converter class + try: + self.set_converter_class(config["converter_class"]) + except KeyError: + pass # Using default converter class + except TypeError as err: + raise AttributeError( + "Converter class should be a subclass of " + "conversion.MySQLConverterBase" + ) from err + + # Compatible configuration with other drivers + compat_map = [ + # (,) + ("db", "database"), + ("username", "user"), + ("passwd", "password"), + ("connect_timeout", "connection_timeout"), + ("read_default_file", "option_files"), + ] + for compat, translate in compat_map: + try: + if translate not in config: + config[translate] = config[compat] + del config[compat] + except KeyError: + pass # Missing compat argument is OK + + # Configure login information + if "user" in config or "password" in config: + try: + user = config["user"] + del config["user"] + except KeyError: + user = self._user + try: + password = config["password"] + del config["password"] + except KeyError: + password = self._password + self.set_login(user, password) + + # Configure host information + if "host" in config and config["host"]: + self._host = config["host"] + + # Check network locations + try: + self._port = int(config["port"]) + del config["port"] + except KeyError: + pass # Missing port argument is OK + except ValueError as err: + raise InterfaceError("TCP/IP port number should be an integer") from err + + if "ssl_disabled" in config: + self._ssl_disabled = config.pop("ssl_disabled") + + # If an init_command is set, keep it, so we can execute it in _post_connection + if "init_command" in config: + self._init_command = config["init_command"] + del config["init_command"] + + # Other configuration + set_ssl_flag = False + for key, value in config.items(): + try: + DEFAULT_CONFIGURATION[key] + except KeyError: + raise AttributeError(f"Unsupported argument '{key}'") from None + # SSL Configuration + if key.startswith("ssl_"): + set_ssl_flag = True + self._ssl.update({key.replace("ssl_", ""): value}) + elif key.startswith("tls_"): + set_ssl_flag = True + self._ssl.update({key: value}) + else: + attribute = "_" + key + try: + setattr(self, attribute, value.strip()) + except AttributeError: + setattr(self, attribute, value) + + # Disable SSL for unix socket connections + if self._unix_socket and os.name == "posix": + self._ssl_disabled = True + + if self._ssl_disabled and self._auth_plugin == "mysql_clear_password": + raise InterfaceError( + "Clear password authentication is not supported over insecure channels" + ) + + if set_ssl_flag: + if "verify_cert" not in self._ssl: + self._ssl["verify_cert"] = DEFAULT_CONFIGURATION["ssl_verify_cert"] + if "verify_identity" not in self._ssl: + self._ssl["verify_identity"] = DEFAULT_CONFIGURATION[ + "ssl_verify_identity" + ] + # Make sure both ssl_key/ssl_cert are set, or neither (XOR) + if "ca" not in self._ssl or self._ssl["ca"] is None: + self._ssl["ca"] = "" + if bool("key" in self._ssl) != bool("cert" in self._ssl): + raise AttributeError( + "ssl_key and ssl_cert need to be both specified, or neither" + ) + # Make sure key/cert are set to None + if not set(("key", "cert")) <= set(self._ssl): + self._ssl["key"] = None + self._ssl["cert"] = None + elif (self._ssl["key"] is None) != (self._ssl["cert"] is None): + raise AttributeError( + "ssl_key and ssl_cert need to be both set, or neither" + ) + if "tls_versions" in self._ssl and self._ssl["tls_versions"] is not None: + self._validate_tls_versions() + + if ( + "tls_ciphersuites" in self._ssl + and self._ssl["tls_ciphersuites"] is not None + ): + self._validate_tls_ciphersuites() + + if self._conn_attrs is None: + self._conn_attrs = {} + elif not isinstance(self._conn_attrs, dict): + raise InterfaceError("conn_attrs must be of type dict") + else: + for attr_name, attr_value in self._conn_attrs.items(): + if attr_name in CONN_ATTRS_DN: + continue + # Validate name type + if not isinstance(attr_name, str): + raise InterfaceError( + "Attribute name should be a string, found: " + f"'{attr_name}' in '{self._conn_attrs}'" + ) + # Validate attribute name limit 32 characters + if len(attr_name) > 32: + raise InterfaceError( + f"Attribute name '{attr_name}' exceeds 32 characters limit size" + ) + # Validate names in connection attributes cannot start with "_" + if attr_name.startswith("_"): + raise InterfaceError( + "Key names in connection attributes cannot start with " + "'_', found: '{attr_name}'" + ) + # Validate value type + if not isinstance(attr_value, str): + raise InterfaceError( + f"Attribute '{attr_name}' value: '{attr_value}' must " + "be a string type" + ) + # Validate attribute value limit 1024 characters + if len(attr_value) > 1024: + raise InterfaceError( + f"Attribute '{attr_name}' value: '{attr_value}' " + "exceeds 1024 characters limit size" + ) + + if self._client_flags & ClientFlag.CONNECT_ARGS: + self._add_default_conn_attrs() + + if "kerberos_auth_mode" in config and config["kerberos_auth_mode"] is not None: + if not isinstance(config["kerberos_auth_mode"], str): + raise InterfaceError("'kerberos_auth_mode' must be of type str") + kerberos_auth_mode = config["kerberos_auth_mode"].lower() + if kerberos_auth_mode == "sspi": + if os.name != "nt": + raise InterfaceError( + "'kerberos_auth_mode=SSPI' is only available on Windows" + ) + self._auth_plugin_class = "MySQLSSPIKerberosAuthPlugin" + elif kerberos_auth_mode == "gssapi": + self._auth_plugin_class = "MySQLKerberosAuthPlugin" + else: + raise InterfaceError( + "Invalid 'kerberos_auth_mode' mode. Please use 'SSPI' or 'GSSAPI'" + ) + + if ( + "krb_service_principal" in config + and config["krb_service_principal"] is not None + ): + self._krb_service_principal = config["krb_service_principal"] + if not isinstance(self._krb_service_principal, str): + raise InterfaceError( + KRB_SERVICE_PINCIPAL_ERROR.format(error="is not a string") + ) + if self._krb_service_principal == "": + raise InterfaceError( + KRB_SERVICE_PINCIPAL_ERROR.format( + error="can not be an empty string" + ) + ) + if "/" not in self._krb_service_principal: + raise InterfaceError( + KRB_SERVICE_PINCIPAL_ERROR.format(error="is incorrectly formatted") + ) + + if self._fido_callback: + # Import the callable if it's a str + if isinstance(self._fido_callback, str): + try: + module, callback = self._fido_callback.rsplit(".", 1) + except ValueError: + raise ProgrammingError( + f"No callable named '{self._fido_callback}'" + ) from None + try: + module = importlib.import_module(module) + self._fido_callback = getattr(module, callback) + except (AttributeError, ModuleNotFoundError) as err: + raise ProgrammingError(f"{err}") from err + # Check if it's a callable + if not callable(self._fido_callback): + raise ProgrammingError("Expected a callable for 'fido_callback'") + # Check the callable signature if has only 1 positional argument + params = len(signature(self._fido_callback).parameters) + if params != 1: + raise ProgrammingError( + "'fido_callback' requires 1 positional argument, but the " + f"callback provided has {params}" + ) + + def _add_default_conn_attrs(self) -> Any: + """Add the default connection attributes.""" + + @staticmethod + def _check_server_version(server_version: StrOrBytes) -> Tuple[int, ...]: + """Check the MySQL version + + This method will check the MySQL version and raise an InterfaceError + when it is not supported or invalid. It will return the version + as a tuple with major, minor and patch. + + Raises InterfaceError if invalid server version. + + Returns tuple + """ + if isinstance(server_version, (bytearray, bytes)): + server_version = server_version.decode() + + regex_ver = re.compile(r"^(\d{1,2})\.(\d{1,2})\.(\d{1,3})(.*)") + match = regex_ver.match(server_version) + if not match: + raise InterfaceError("Failed parsing MySQL version") + + version = tuple(int(v) for v in match.groups()[0:3]) + if version < (4, 1): + raise InterfaceError(f"MySQL Version '{server_version}' is not supported") + + return version + + def get_server_version(self) -> Tuple[int, ...]: + """Get the MySQL version + + This method returns the MySQL server version as a tuple. If not + previously connected, it will return None. + + Returns a tuple or None. + """ + return self._server_version + + def get_server_info(self) -> Optional[str]: + """Get the original MySQL version information + + This method returns the original MySQL server as text. If not + previously connected, it will return None. + + Returns a string or None. + """ + try: + return self._handshake["server_version_original"] # type: ignore[return-value] + except (TypeError, KeyError): + return None + + @property + @abstractmethod + def in_transaction(self) -> Any: + """MySQL session has started a transaction""" + + def set_client_flags(self, flags: Union[int, Sequence[int]]) -> int: + """Set the client flags + + The flags-argument can be either an int or a list (or tuple) of + ClientFlag-values. If it is an integer, it will set client_flags + to flags as is. + If flags is a list (or tuple), each flag will be set or unset + when it's negative. + + set_client_flags([ClientFlag.FOUND_ROWS,-ClientFlag.LONG_FLAG]) + + Raises ProgrammingError when the flags argument is not a set or + an integer bigger than 0. + + Returns self.client_flags + """ + if isinstance(flags, int) and flags > 0: + self._client_flags = flags + elif isinstance(flags, (tuple, list)): + for flag in flags: + if flag < 0: + self._client_flags &= ~abs(flag) + else: + self._client_flags |= flag + else: + raise ProgrammingError("set_client_flags expect integer (>0) or set") + return self._client_flags + + def isset_client_flag(self, flag: int) -> bool: + """Check if a client flag is set""" + if (self._client_flags & flag) > 0: + return True + return False + + @property + def time_zone(self) -> str: + """Get the current time zone""" + return self.info_query("SELECT @@session.time_zone")[0] + + @time_zone.setter + def time_zone(self, value: str) -> None: + """Set the time zone""" + self.cmd_query(f"SET @@session.time_zone = '{value}'") + self._time_zone = value + + @property + def sql_mode(self) -> str: + """Get the SQL mode""" + if self._sql_mode is None: + self._sql_mode = self.info_query("SELECT @@session.sql_mode")[0] + return self._sql_mode + + @sql_mode.setter + def sql_mode(self, value: Union[str, Sequence[int]]) -> None: + """Set the SQL mode + + This method sets the SQL Mode for the current connection. The value + argument can be either a string with comma separate mode names, or + a sequence of mode names. + + It is good practice to use the constants class SQLMode: + from mysql.connector.constants import SQLMode + cnx.sql_mode = [SQLMode.NO_ZERO_DATE, SQLMode.REAL_AS_FLOAT] + """ + if isinstance(value, (list, tuple)): + value = ",".join(value) + self.cmd_query(f"SET @@session.sql_mode = '{value}'") + self._sql_mode = value + + @abstractmethod + def info_query(self, query: Any) -> Any: + """Send a query which only returns 1 row""" + + def set_login( + self, username: Optional[str] = None, password: Optional[str] = None + ) -> None: + """Set login information for MySQL + + Set the username and/or password for the user connecting to + the MySQL Server. + """ + if username is not None: + self._user = username.strip() + else: + self._user = "" + if password is not None: + self._password = password + else: + self._password = "" + + def set_unicode(self, value: bool = True) -> None: + """Toggle unicode mode + + Set whether we return string fields as unicode or not. + Default is True. + """ + self._use_unicode = value + if self.converter: + self.converter.set_unicode(value) + + @property + def autocommit(self) -> bool: + """Get whether autocommit is on or off""" + value = self.info_query("SELECT @@session.autocommit")[0] + return value == 1 + + @autocommit.setter + def autocommit(self, value: bool) -> None: + """Toggle autocommit""" + switch = "ON" if value else "OFF" + self.cmd_query(f"SET @@session.autocommit = {switch}") + self._autocommit = value + + @property + def get_warnings(self) -> bool: + """Get whether this connection retrieves warnings automatically + + This method returns whether this connection retrieves warnings + automatically. + + Returns True, or False when warnings are not retrieved. + """ + return self._get_warnings + + @get_warnings.setter + def get_warnings(self, value: bool) -> None: + """Set whether warnings should be automatically retrieved + + The toggle-argument must be a boolean. When True, cursors for this + connection will retrieve information about warnings (if any). + + Raises ValueError on error. + """ + if not isinstance(value, bool): + raise ValueError("Expected a boolean type") + self._get_warnings = value + + @property + def raise_on_warnings(self) -> bool: + """Get whether this connection raises an error on warnings + + This method returns whether this connection will raise errors when + MySQL reports warnings. + + Returns True or False. + """ + return self._raise_on_warnings + + @raise_on_warnings.setter + def raise_on_warnings(self, value: bool) -> None: + """Set whether warnings raise an error + + The toggle-argument must be a boolean. When True, cursors for this + connection will raise an error when MySQL reports warnings. + + Raising on warnings implies retrieving warnings automatically. In + other words: warnings will be set to True. If set to False, warnings + will be also set to False. + + Raises ValueError on error. + """ + if not isinstance(value, bool): + raise ValueError("Expected a boolean type") + self._raise_on_warnings = value + # Don't disable warning retrieval if raising explicitly disabled + if value: + self._get_warnings = value + + @property + def unread_result(self) -> bool: + """Get whether there is an unread result + + This method is used by cursors to check whether another cursor still + needs to retrieve its result set. + + Returns True, or False when there is no unread result. + """ + return self._unread_result + + @unread_result.setter + def unread_result(self, value: bool) -> None: + """Set whether there is an unread result + + This method is used by cursors to let other cursors know there is + still a result set that needs to be retrieved. + + Raises ValueError on errors. + """ + if not isinstance(value, bool): + raise ValueError("Expected a boolean type") + self._unread_result = value + + @property + def charset(self) -> str: + """Returns the character set for current connection + + This property returns the character set name of the current connection. + The server is queried when the connection is active. If not connected, + the configured character set name is returned. + + Returns a string. + """ + return CharacterSet.get_info(self._charset_id)[0] + + @property + def python_charset(self) -> str: + """Returns the Python character set for current connection + + This property returns the character set name of the current connection. + Note that, unlike property charset, this checks if the previously set + character set is supported by Python and if not, it returns the + equivalent character set that Python supports. + + Returns a string. + """ + encoding = CharacterSet.get_info(self._charset_id)[0] + if encoding in ("utf8mb4", "utf8mb3", "binary"): + return "utf8" + return encoding + + def set_charset_collation( + self, charset: Optional[Union[int, str]] = None, collation: Optional[str] = None + ) -> None: + """Sets the character set and collation for the current connection + + This method sets the character set and collation to be used for + the current connection. The charset argument can be either the + name of a character set as a string, or the numerical equivalent + as defined in constants.CharacterSet. + + When the collation is not given, the default will be looked up and + used. + + For example, the following will set the collation for the latin1 + character set to latin1_general_ci: + + set_charset('latin1','latin1_general_ci') + + """ + err_msg = "{} should be either integer, string or None" + if not isinstance(charset, (int, str)) and charset is not None: + raise ValueError(err_msg.format("charset")) + if not isinstance(collation, str) and collation is not None: + raise ValueError("collation should be either string or None") + + if charset: + if isinstance(charset, int): + ( + self._charset_id, + charset_name, + collation_name, + ) = CharacterSet.get_charset_info(charset) + elif isinstance(charset, str): + ( + self._charset_id, + charset_name, + collation_name, + ) = CharacterSet.get_charset_info(charset, collation) + else: + raise ValueError(err_msg.format("charset")) + elif collation: + ( + self._charset_id, + charset_name, + collation_name, + ) = CharacterSet.get_charset_info(collation=collation) + else: + charset = DEFAULT_CONFIGURATION["charset"] + ( + self._charset_id, + charset_name, + collation_name, + ) = CharacterSet.get_charset_info(charset, collation=None) + + self._execute_query(f"SET NAMES '{charset_name}' COLLATE '{collation_name}'") + + try: + # Required for C Extension + self.set_character_set_name(charset_name) + except AttributeError: + # Not required for pure Python connection + pass + + if self.converter: + self.converter.set_charset(charset_name) + + @property + def collation(self) -> str: + """Returns the collation for current connection + + This property returns the collation name of the current connection. + The server is queried when the connection is active. If not connected, + the configured collation name is returned. + + Returns a string. + """ + return CharacterSet.get_charset_info(self._charset_id)[2] + + @abstractmethod + def _do_handshake(self) -> Any: + """Gather information of the MySQL server before authentication""" + + @abstractmethod + def _open_connection(self) -> Any: + """Open the connection to the MySQL server""" + + def _post_connection(self) -> None: + """Executes commands after connection has been established + + This method executes commands after the connection has been + established. Some setting like autocommit, character set, and SQL mode + are set using this method. + """ + self.set_charset_collation(self._charset_id) + self.autocommit = self._autocommit + if self._time_zone: + self.time_zone = self._time_zone + if self._sql_mode: + self.sql_mode = self._sql_mode + if self._init_command: + self._execute_query(self._init_command) + + @abstractmethod + def disconnect(self) -> Any: + """Disconnect from the MySQL server""" + + close: Callable[[], Any] = disconnect + + def connect(self, **kwargs: Any) -> None: + """Connect to the MySQL server + + This method sets up the connection to the MySQL server. If no + arguments are given, it will use the already configured or default + values. + """ + if kwargs: + self.config(**kwargs) + + self.disconnect() + self._open_connection() + # Server does not allow to run any other statement different from ALTER + # when user's password has been expired. + if not self._client_flags & ClientFlag.CAN_HANDLE_EXPIRED_PASSWORDS: + self._post_connection() + + def reconnect(self, attempts: int = 1, delay: int = 0) -> None: + """Attempt to reconnect to the MySQL server + + The argument attempts should be the number of times a reconnect + is tried. The delay argument is the number of seconds to wait between + each retry. + + You may want to set the number of attempts higher and use delay when + you expect the MySQL server to be down for maintenance or when you + expect the network to be temporary unavailable. + + Raises InterfaceError on errors. + """ + counter = 0 + span = None + + if self._tracer: + span = self._tracer.start_span( + name=CONNECTION_SPAN_NAME, kind=trace.SpanKind.CLIENT + ) + + try: + while counter != attempts: + counter = counter + 1 + try: + self.disconnect() + self.connect() + if self.is_connected(): + break + except (Error, IOError) as err: + if counter == attempts: + msg = ( + f"Can not reconnect to MySQL after {attempts} " + f"attempt(s): {err}" + ) + raise InterfaceError(msg) from err + if delay > 0: + sleep(delay) + except InterfaceError as interface_err: + if OTEL_ENABLED: + set_connection_span_attrs(self, span) + record_exception_event(span, interface_err) + end_span(span) + raise + + self._span = span + if OTEL_ENABLED: + set_connection_span_attrs(self, self._span) + + @abstractmethod + def is_connected(self) -> Any: + """Reports whether the connection to MySQL Server is available""" + + @abstractmethod + def ping(self, reconnect: bool = False, attempts: int = 1, delay: int = 0) -> Any: + """Check availability of the MySQL server""" + + @abstractmethod + def commit(self) -> Any: + """Commit current transaction""" + + @abstractmethod + def cursor( + self, + buffered: Optional[bool] = None, + raw: Optional[bool] = None, + prepared: Optional[bool] = None, + cursor_class: Optional[type] = None, + dictionary: Optional[bool] = None, + named_tuple: Optional[bool] = None, + ) -> "MySQLCursorAbstract": + """Instantiates and returns a cursor""" + + @abstractmethod + def _execute_query(self, query: Any) -> Any: + """Execute a query""" + + @abstractmethod + def rollback(self) -> Any: + """Rollback current transaction""" + + def start_transaction( + self, + consistent_snapshot: bool = False, + isolation_level: Optional[str] = None, + readonly: Optional[bool] = None, + ) -> None: + """Start a transaction + + This method explicitly starts a transaction sending the + START TRANSACTION statement to the MySQL server. You can optionally + set whether there should be a consistent snapshot, which + isolation level you need or which access mode i.e. READ ONLY or + READ WRITE. + + For example, to start a transaction with isolation level SERIALIZABLE, + you would do the following: + >>> cnx = mysql.connector.connect(..) + >>> cnx.start_transaction(isolation_level='SERIALIZABLE') + + Raises ProgrammingError when a transaction is already in progress + and when ValueError when isolation_level specifies an Unknown + level. + """ + if self.in_transaction: + raise ProgrammingError("Transaction already in progress") + + if isolation_level: + level = isolation_level.strip().replace("-", " ").upper() + levels = [ + "READ UNCOMMITTED", + "READ COMMITTED", + "REPEATABLE READ", + "SERIALIZABLE", + ] + + if level not in levels: + raise ValueError(f'Unknown isolation level "{isolation_level}"') + + self._execute_query(f"SET TRANSACTION ISOLATION LEVEL {level}") + + if readonly is not None: + if self._server_version < (5, 6, 5): + raise ValueError( + f"MySQL server version {self._server_version} does not " + "support this feature" + ) + + if readonly: + access_mode = "READ ONLY" + else: + access_mode = "READ WRITE" + self._execute_query(f"SET TRANSACTION {access_mode}") + + query = "START TRANSACTION" + if consistent_snapshot: + query += " WITH CONSISTENT SNAPSHOT" + self.cmd_query(query) + + def reset_session( + self, + user_variables: Optional[Dict[str, Any]] = None, + session_variables: Optional[Dict[str, Any]] = None, + ) -> None: + """Clears the current active session + + This method resets the session state, if the MySQL server is 5.7.3 + or later active session will be reset without re-authenticating. + For other server versions session will be reset by re-authenticating. + + It is possible to provide a sequence of variables and their values to + be set after clearing the session. This is possible for both user + defined variables and session variables. + This method takes two arguments user_variables and session_variables + which are dictionaries. + + Raises OperationalError if not connected, InternalError if there are + unread results and InterfaceError on errors. + """ + if not self.is_connected(): + raise OperationalError("MySQL Connection not available") + + try: + self.cmd_reset_connection() + except (NotSupportedError, NotImplementedError): + if self._compress: + raise NotSupportedError( + "Reset session is not supported with compression for " + "MySQL server version 5.7.2 or earlier" + ) from None + self.cmd_change_user( + self._user, + self._password, + self._database, + self._charset_id, + ) + + if user_variables or session_variables: + cur = self.cursor() + if user_variables: + for key, value in user_variables.items(): + cur.execute(f"SET @`{key}` = {value}") + if session_variables: + for key, value in session_variables.items(): + cur.execute(f"SET SESSION `{key}` = {value}") + cur.close() + + def set_converter_class(self, convclass: Optional[Type[MySQLConverter]]) -> None: + """ + Set the converter class to be used. This should be a class overloading + methods and members of conversion.MySQLConverter. + """ + if convclass and issubclass(convclass, MySQLConverterBase): + charset_name = CharacterSet.get_info(self._charset_id)[0] + self._converter_class = convclass + self.converter = convclass(charset_name, self._use_unicode) + self.converter.str_fallback = self._converter_str_fallback + else: + raise TypeError( + "Converter class should be a subclass of conversion.MySQLConverterBase." + ) + + @abstractmethod + def get_rows( + self, + count: Optional[int] = None, + binary: bool = False, + columns: Optional[List[DescriptionType]] = None, + raw: Optional[bool] = None, + prep_stmt: Any = None, + ) -> Tuple[List[Any], Optional[Mapping[str, Any]]]: + """Get all rows returned by the MySQL server""" + + def cmd_init_db(self, database: str) -> Optional[Mapping[str, Any]]: + """Change the current database""" + raise NotImplementedError + + def cmd_query( + self, + query: Any, + raw: bool = False, + buffered: bool = False, + raw_as_string: bool = False, + ) -> Optional[Mapping[str, Any]]: + """Send a query to the MySQL server""" + raise NotImplementedError + + def cmd_query_iter( + self, statements: Any + ) -> Generator[Mapping[str, Any], None, None]: + """Send one or more statements to the MySQL server""" + raise NotImplementedError + + def cmd_refresh(self, options: int) -> Optional[Mapping[str, Any]]: + """Send the Refresh command to the MySQL server""" + raise NotImplementedError + + def cmd_quit(self) -> Any: + """Close the current connection with the server""" + raise NotImplementedError + + def cmd_shutdown( + self, shutdown_type: Optional[int] = None + ) -> Optional[Mapping[str, Any]]: + """Shut down the MySQL Server""" + raise NotImplementedError + + def cmd_statistics(self) -> Optional[Mapping[str, Any]]: + """Send the statistics command to the MySQL Server""" + raise NotImplementedError + + @staticmethod + def cmd_process_info() -> Any: + """Get the process list of the MySQL Server + + This method is a placeholder to notify that the PROCESS_INFO command + is not supported by raising the NotSupportedError. The command + "SHOW PROCESSLIST" should be send using the cmd_query()-method or + using the INFORMATION_SCHEMA database. + + Raises NotSupportedError exception + """ + raise NotSupportedError( + "Not implemented. Use SHOW PROCESSLIST or INFORMATION_SCHEMA" + ) + + def cmd_process_kill(self, mysql_pid: int) -> Optional[Mapping[str, Any]]: + """Kill a MySQL process""" + raise NotImplementedError + + def cmd_debug(self) -> Optional[Mapping[str, Any]]: + """Send the DEBUG command""" + raise NotImplementedError + + def cmd_ping(self) -> Optional[Mapping[str, Any]]: + """Send the PING command""" + raise NotImplementedError + + def cmd_change_user( + self, + username: str = "", + password: str = "", + database: str = "", + charset: int = 45, + password1: str = "", + password2: str = "", + password3: str = "", + oci_config_file: str = "", + ) -> Optional[Mapping[str, Any]]: + """Change the current logged in user""" + raise NotImplementedError + + def cmd_stmt_prepare(self, statement: Any) -> Optional[Mapping[str, Any]]: + """Prepare a MySQL statement""" + raise NotImplementedError + + def cmd_stmt_execute( + self, + statement_id: Any, + data: Sequence[Any] = (), + parameters: Sequence[Any] = (), + flags: int = 0, + ) -> Any: + """Execute a prepared MySQL statement""" + raise NotImplementedError + + def cmd_stmt_close(self, statement_id: Any) -> Any: + """Deallocate a prepared MySQL statement""" + raise NotImplementedError + + def cmd_stmt_send_long_data( + self, statement_id: Any, param_id: int, data: BinaryIO + ) -> Any: + """Send data for a column""" + raise NotImplementedError + + def cmd_stmt_reset(self, statement_id: Any) -> Any: + """Reset data for prepared statement sent as long data""" + raise NotImplementedError + + def cmd_reset_connection(self) -> Any: + """Resets the session state without re-authenticating""" + raise NotImplementedError + + +class MySQLCursorAbstract(ABC): + """Abstract cursor class + + Abstract class defining cursor class with method and members + required by the Python Database API Specification v2.0. + """ + + def __init__(self) -> None: + """Initialization""" + self._description: Optional[List[DescriptionType]] = None + self._rowcount: int = -1 + self._last_insert_id: Optional[int] = None + self._warnings: Optional[List[WarningType]] = None + self._warning_count: int = 0 + self._executed: Optional[StrOrBytes] = None + self._executed_list: List[StrOrBytes] = [] + self._stored_results: List[Any] = [] + self.arraysize: int = 1 + + def __enter__(self) -> MySQLCursorAbstract: + return self + + def __exit__( + self, + exc_type: Type[BaseException], + exc_value: BaseException, + traceback: TracebackType, + ) -> None: + self.close() + + @abstractmethod + def callproc(self, procname: str, args: Sequence[Any] = ()) -> Any: + """Calls a stored procedure with the given arguments + + The arguments will be set during this session, meaning + they will be called like ___arg where + is an enumeration (+1) of the arguments. + + Coding Example: + 1) Defining the Stored Routine in MySQL: + CREATE PROCEDURE multiply(IN pFac1 INT, IN pFac2 INT, OUT pProd INT) + BEGIN + SET pProd := pFac1 * pFac2; + END + + 2) Executing in Python: + args = (5,5,0) # 0 is to hold pprod + cursor.callproc('multiply', args) + print(cursor.fetchone()) + + Does not return a value, but a result set will be + available when the CALL-statement execute successfully. + Raises exceptions when something is wrong. + """ + + @abstractmethod + def close(self) -> Any: + """Close the cursor.""" + + @abstractmethod + def execute( + self, + operation: str, + params: Union[Sequence[Any], Dict[str, Any]] = (), + multi: bool = False, + ) -> Any: + """Executes the given operation + + Executes the given operation substituting any markers with + the given parameters. + + For example, getting all rows where id is 5: + cursor.execute("SELECT * FROM t1 WHERE id = %s", (5,)) + + The multi argument should be set to True when executing multiple + statements in one operation. If not set and multiple results are + found, an InterfaceError will be raised. + + If warnings where generated, and connection.get_warnings is True, then + self._warnings will be a list containing these warnings. + + Returns an iterator when multi is True, otherwise None. + """ + + @abstractmethod + def executemany( + self, operation: str, seq_params: Sequence[Union[Sequence[Any], Dict[str, Any]]] + ) -> Any: + """Execute the given operation multiple times + + The executemany() method will execute the operation iterating + over the list of parameters in seq_params. + + Example: Inserting 3 new employees and their phone number + + data = [ + ('Jane','555-001'), + ('Joe', '555-001'), + ('John', '555-003') + ] + stmt = "INSERT INTO employees (name, phone) VALUES ('%s','%s')" + cursor.executemany(stmt, data) + + INSERT statements are optimized by batching the data, that is + using the MySQL multiple rows syntax. + + Results are discarded. If they are needed, consider looping over + data using the execute() method. + """ + + @abstractmethod + def fetchone(self) -> Optional[Sequence[Any]]: + """Returns next row of a query result set + + Returns a tuple or None. + """ + + @abstractmethod + def fetchmany(self, size: int = 1) -> List[Sequence[Any]]: + """Returns the next set of rows of a query result, returning a + list of tuples. When no more rows are available, it returns an + empty list. + + The number of rows returned can be specified using the size argument, + which defaults to one + """ + + @abstractmethod + def fetchall(self) -> Sequence[Any]: + """Returns all rows of a query result set + + Returns a list of tuples. + """ + + def nextset(self) -> Any: + """Not Implemented.""" + + def setinputsizes(self, sizes: Any) -> Any: + """Not Implemented.""" + + def setoutputsize(self, size: Any, column: Any = None) -> Any: + """Not Implemented.""" + + def reset(self, free: bool = True) -> Any: + """Reset the cursor to default""" + + @property + @abstractmethod + def description( + self, + ) -> Optional[List[DescriptionType]]: + """Returns description of columns in a result + + This property returns a list of tuples describing the columns in + in a result set. A tuple is described as follows:: + + (column_name, + type, + None, + None, + None, + None, + null_ok, + column_flags) # Addition to PEP-249 specs + + Returns a list of tuples. + """ + return self._description + + @property + @abstractmethod + def rowcount(self) -> int: + """Returns the number of rows produced or affected + + This property returns the number of rows produced by queries + such as a SELECT, or affected rows when executing DML statements + like INSERT or UPDATE. + + Note that for non-buffered cursors it is impossible to know the + number of rows produced before having fetched them all. For those, + the number of rows will be -1 right after execution, and + incremented when fetching rows. + + Returns an integer. + """ + return self._rowcount + + @property + def lastrowid(self) -> Optional[int]: + """Returns the value generated for an AUTO_INCREMENT column + + Returns the value generated for an AUTO_INCREMENT column by + the previous INSERT or UPDATE statement or None when there is + no such value available. + + Returns a long value or None. + """ + return self._last_insert_id + + @property + def warnings(self) -> Optional[List[WarningType]]: + """Return warnings.""" + return self._warnings + + @property + def warning_count(self) -> int: + """Returns the number of warnings + + This property returns the number of warnings generated by the + previously executed operation. + + Returns an integer value. + """ + return self._warning_count + + def fetchwarnings(self) -> Optional[List[WarningType]]: + """Returns Warnings.""" + return self._warnings + + def get_attributes(self) -> Optional[List[Tuple[str, Any]]]: + """Get the added query attributes so far.""" + if hasattr(self, "_cnx"): + return self._cnx.query_attrs + if hasattr(self, "_connection"): + return self._connection.query_attrs + return None + + def add_attribute(self, name: str, value: Any) -> None: + """Add a query attribute and his value.""" + if not isinstance(name, str): + raise ProgrammingError("Parameter `name` must be a string type") + if value is not None and not isinstance(value, MYSQL_PY_TYPES): + raise ProgrammingError( + f"Object {value} cannot be converted to a MySQL type" + ) + if hasattr(self, "_cnx"): + self._cnx.query_attrs_append((name, value)) + elif hasattr(self, "_connection"): + self._connection.query_attrs_append((name, value)) + + def remove_attribute(self, name: str) -> Any: + """Remove a query attribute by name. + + If no match, `None` is returned; else the corresponding value is returned. + """ + if not isinstance(name, str): + raise ProgrammingError("Parameter `name` must be a string type") + if hasattr(self, "_cnx"): + return self._cnx.query_attrs_remove(name) + if hasattr(self, "_connection"): + return self._connection.query_attrs_remove(name) + return None + + def clear_attributes(self) -> None: + """Remove all the query attributes.""" + if hasattr(self, "_cnx"): + self._cnx.query_attrs_clear() + elif hasattr(self, "_connection"): + self._connection.query_attrs_clear() diff --git a/mysql/connector/authentication.py b/mysql/connector/authentication.py new file mode 100644 index 0000000..d885684 --- /dev/null +++ b/mysql/connector/authentication.py @@ -0,0 +1,77 @@ +# Copyright (c) 2014, 2022, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Implementing support for MySQL Authentication Plugins""" + +import importlib + +from functools import lru_cache +from typing import Optional, Type + +from .errors import NotSupportedError, ProgrammingError +from .logger import logger +from .plugins import BaseAuthPlugin + +DEFAULT_PLUGINS_PKG = "mysql.connector.plugins" + + +@lru_cache(maxsize=10, typed=False) +def get_auth_plugin( + plugin_name: str, + auth_plugin_class: Optional[str] = None, +) -> Type[BaseAuthPlugin]: # AUTH_PLUGIN_CLASS_TYPES: + """Return authentication class based on plugin name + + This function returns the class for the authentication plugin plugin_name. + The returned class is a subclass of BaseAuthPlugin. + + Args: + plugin_name (str): Authentication plugin name. + auth_plugin_class (str): Authentication plugin class name. + + Raises: + NotSupportedError: When plugin_name is not supported. + + Returns: + Subclass of `BaseAuthPlugin`. + """ + package = DEFAULT_PLUGINS_PKG + if plugin_name: + try: + logger.info("package: %s", package) + logger.info("plugin_name: %s", plugin_name) + plugin_module = importlib.import_module(f".{plugin_name}", package) + if not auth_plugin_class or not hasattr(plugin_module, auth_plugin_class): + auth_plugin_class = plugin_module.AUTHENTICATION_PLUGIN_CLASS + logger.info("AUTHENTICATION_PLUGIN_CLASS: %s", auth_plugin_class) + return getattr(plugin_module, auth_plugin_class) + except ModuleNotFoundError as err: + logger.warning("Requested Module was not found: %s", err) + except ValueError as err: + raise ProgrammingError(f"Invalid module name: {err}") from err + raise NotSupportedError(f"Authentication plugin '{plugin_name}' is not supported") diff --git a/mysql/connector/charsets.py b/mysql/connector/charsets.py new file mode 100644 index 0000000..5ce9b1a --- /dev/null +++ b/mysql/connector/charsets.py @@ -0,0 +1,620 @@ +# -*- coding: utf-8 -*- # pylint: disable=missing-module-docstring + +# Copyright (c) 2013, 2022, Oracle and/or its affiliates. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from typing import List, Optional, Tuple + +"""This module contains the MySQL Server Character Sets.""" # pylint: disable=pointless-string-statement + +# This file was auto-generated. +_GENERATED_ON: str = "2022-05-09" +_MYSQL_VERSION: Tuple[int, int, int] = (8, 0, 30) + +MYSQL_CHARACTER_SETS: List[Optional[Tuple[str, str, bool]]] = [ + # (character set name, collation, default) + None, + ("big5", "big5_chinese_ci", True), # 1 + ("latin2", "latin2_czech_cs", False), # 2 + ("dec8", "dec8_swedish_ci", True), # 3 + ("cp850", "cp850_general_ci", True), # 4 + ("latin1", "latin1_german1_ci", False), # 5 + ("hp8", "hp8_english_ci", True), # 6 + ("koi8r", "koi8r_general_ci", True), # 7 + ("latin1", "latin1_swedish_ci", True), # 8 + ("latin2", "latin2_general_ci", True), # 9 + ("swe7", "swe7_swedish_ci", True), # 10 + ("ascii", "ascii_general_ci", True), # 11 + ("ujis", "ujis_japanese_ci", True), # 12 + ("sjis", "sjis_japanese_ci", True), # 13 + ("cp1251", "cp1251_bulgarian_ci", False), # 14 + ("latin1", "latin1_danish_ci", False), # 15 + ("hebrew", "hebrew_general_ci", True), # 16 + None, + ("tis620", "tis620_thai_ci", True), # 18 + ("euckr", "euckr_korean_ci", True), # 19 + ("latin7", "latin7_estonian_cs", False), # 20 + ("latin2", "latin2_hungarian_ci", False), # 21 + ("koi8u", "koi8u_general_ci", True), # 22 + ("cp1251", "cp1251_ukrainian_ci", False), # 23 + ("gb2312", "gb2312_chinese_ci", True), # 24 + ("greek", "greek_general_ci", True), # 25 + ("cp1250", "cp1250_general_ci", True), # 26 + ("latin2", "latin2_croatian_ci", False), # 27 + ("gbk", "gbk_chinese_ci", True), # 28 + ("cp1257", "cp1257_lithuanian_ci", False), # 29 + ("latin5", "latin5_turkish_ci", True), # 30 + ("latin1", "latin1_german2_ci", False), # 31 + ("armscii8", "armscii8_general_ci", True), # 32 + ("utf8mb3", "utf8mb3_general_ci", True), # 33 + ("cp1250", "cp1250_czech_cs", False), # 34 + ("ucs2", "ucs2_general_ci", True), # 35 + ("cp866", "cp866_general_ci", True), # 36 + ("keybcs2", "keybcs2_general_ci", True), # 37 + ("macce", "macce_general_ci", True), # 38 + ("macroman", "macroman_general_ci", True), # 39 + ("cp852", "cp852_general_ci", True), # 40 + ("latin7", "latin7_general_ci", True), # 41 + ("latin7", "latin7_general_cs", False), # 42 + ("macce", "macce_bin", False), # 43 + ("cp1250", "cp1250_croatian_ci", False), # 44 + ("utf8mb4", "utf8mb4_general_ci", False), # 45 + ("utf8mb4", "utf8mb4_bin", False), # 46 + ("latin1", "latin1_bin", False), # 47 + ("latin1", "latin1_general_ci", False), # 48 + ("latin1", "latin1_general_cs", False), # 49 + ("cp1251", "cp1251_bin", False), # 50 + ("cp1251", "cp1251_general_ci", True), # 51 + ("cp1251", "cp1251_general_cs", False), # 52 + ("macroman", "macroman_bin", False), # 53 + ("utf16", "utf16_general_ci", True), # 54 + ("utf16", "utf16_bin", False), # 55 + ("utf16le", "utf16le_general_ci", True), # 56 + ("cp1256", "cp1256_general_ci", True), # 57 + ("cp1257", "cp1257_bin", False), # 58 + ("cp1257", "cp1257_general_ci", True), # 59 + ("utf32", "utf32_general_ci", True), # 60 + ("utf32", "utf32_bin", False), # 61 + ("utf16le", "utf16le_bin", False), # 62 + ("binary", "binary", True), # 63 + ("armscii8", "armscii8_bin", False), # 64 + ("ascii", "ascii_bin", False), # 65 + ("cp1250", "cp1250_bin", False), # 66 + ("cp1256", "cp1256_bin", False), # 67 + ("cp866", "cp866_bin", False), # 68 + ("dec8", "dec8_bin", False), # 69 + ("greek", "greek_bin", False), # 70 + ("hebrew", "hebrew_bin", False), # 71 + ("hp8", "hp8_bin", False), # 72 + ("keybcs2", "keybcs2_bin", False), # 73 + ("koi8r", "koi8r_bin", False), # 74 + ("koi8u", "koi8u_bin", False), # 75 + ("utf8mb3", "utf8mb3_tolower_ci", False), # 76 + ("latin2", "latin2_bin", False), # 77 + ("latin5", "latin5_bin", False), # 78 + ("latin7", "latin7_bin", False), # 79 + ("cp850", "cp850_bin", False), # 80 + ("cp852", "cp852_bin", False), # 81 + ("swe7", "swe7_bin", False), # 82 + ("utf8mb3", "utf8mb3_bin", False), # 83 + ("big5", "big5_bin", False), # 84 + ("euckr", "euckr_bin", False), # 85 + ("gb2312", "gb2312_bin", False), # 86 + ("gbk", "gbk_bin", False), # 87 + ("sjis", "sjis_bin", False), # 88 + ("tis620", "tis620_bin", False), # 89 + ("ucs2", "ucs2_bin", False), # 90 + ("ujis", "ujis_bin", False), # 91 + ("geostd8", "geostd8_general_ci", True), # 92 + ("geostd8", "geostd8_bin", False), # 93 + ("latin1", "latin1_spanish_ci", False), # 94 + ("cp932", "cp932_japanese_ci", True), # 95 + ("cp932", "cp932_bin", False), # 96 + ("eucjpms", "eucjpms_japanese_ci", True), # 97 + ("eucjpms", "eucjpms_bin", False), # 98 + ("cp1250", "cp1250_polish_ci", False), # 99 + None, + ("utf16", "utf16_unicode_ci", False), # 101 + ("utf16", "utf16_icelandic_ci", False), # 102 + ("utf16", "utf16_latvian_ci", False), # 103 + ("utf16", "utf16_romanian_ci", False), # 104 + ("utf16", "utf16_slovenian_ci", False), # 105 + ("utf16", "utf16_polish_ci", False), # 106 + ("utf16", "utf16_estonian_ci", False), # 107 + ("utf16", "utf16_spanish_ci", False), # 108 + ("utf16", "utf16_swedish_ci", False), # 109 + ("utf16", "utf16_turkish_ci", False), # 110 + ("utf16", "utf16_czech_ci", False), # 111 + ("utf16", "utf16_danish_ci", False), # 112 + ("utf16", "utf16_lithuanian_ci", False), # 113 + ("utf16", "utf16_slovak_ci", False), # 114 + ("utf16", "utf16_spanish2_ci", False), # 115 + ("utf16", "utf16_roman_ci", False), # 116 + ("utf16", "utf16_persian_ci", False), # 117 + ("utf16", "utf16_esperanto_ci", False), # 118 + ("utf16", "utf16_hungarian_ci", False), # 119 + ("utf16", "utf16_sinhala_ci", False), # 120 + ("utf16", "utf16_german2_ci", False), # 121 + ("utf16", "utf16_croatian_ci", False), # 122 + ("utf16", "utf16_unicode_520_ci", False), # 123 + ("utf16", "utf16_vietnamese_ci", False), # 124 + None, + None, + None, + ("ucs2", "ucs2_unicode_ci", False), # 128 + ("ucs2", "ucs2_icelandic_ci", False), # 129 + ("ucs2", "ucs2_latvian_ci", False), # 130 + ("ucs2", "ucs2_romanian_ci", False), # 131 + ("ucs2", "ucs2_slovenian_ci", False), # 132 + ("ucs2", "ucs2_polish_ci", False), # 133 + ("ucs2", "ucs2_estonian_ci", False), # 134 + ("ucs2", "ucs2_spanish_ci", False), # 135 + ("ucs2", "ucs2_swedish_ci", False), # 136 + ("ucs2", "ucs2_turkish_ci", False), # 137 + ("ucs2", "ucs2_czech_ci", False), # 138 + ("ucs2", "ucs2_danish_ci", False), # 139 + ("ucs2", "ucs2_lithuanian_ci", False), # 140 + ("ucs2", "ucs2_slovak_ci", False), # 141 + ("ucs2", "ucs2_spanish2_ci", False), # 142 + ("ucs2", "ucs2_roman_ci", False), # 143 + ("ucs2", "ucs2_persian_ci", False), # 144 + ("ucs2", "ucs2_esperanto_ci", False), # 145 + ("ucs2", "ucs2_hungarian_ci", False), # 146 + ("ucs2", "ucs2_sinhala_ci", False), # 147 + ("ucs2", "ucs2_german2_ci", False), # 148 + ("ucs2", "ucs2_croatian_ci", False), # 149 + ("ucs2", "ucs2_unicode_520_ci", False), # 150 + ("ucs2", "ucs2_vietnamese_ci", False), # 151 + None, + None, + None, + None, + None, + None, + None, + ("ucs2", "ucs2_general_mysql500_ci", False), # 159 + ("utf32", "utf32_unicode_ci", False), # 160 + ("utf32", "utf32_icelandic_ci", False), # 161 + ("utf32", "utf32_latvian_ci", False), # 162 + ("utf32", "utf32_romanian_ci", False), # 163 + ("utf32", "utf32_slovenian_ci", False), # 164 + ("utf32", "utf32_polish_ci", False), # 165 + ("utf32", "utf32_estonian_ci", False), # 166 + ("utf32", "utf32_spanish_ci", False), # 167 + ("utf32", "utf32_swedish_ci", False), # 168 + ("utf32", "utf32_turkish_ci", False), # 169 + ("utf32", "utf32_czech_ci", False), # 170 + ("utf32", "utf32_danish_ci", False), # 171 + ("utf32", "utf32_lithuanian_ci", False), # 172 + ("utf32", "utf32_slovak_ci", False), # 173 + ("utf32", "utf32_spanish2_ci", False), # 174 + ("utf32", "utf32_roman_ci", False), # 175 + ("utf32", "utf32_persian_ci", False), # 176 + ("utf32", "utf32_esperanto_ci", False), # 177 + ("utf32", "utf32_hungarian_ci", False), # 178 + ("utf32", "utf32_sinhala_ci", False), # 179 + ("utf32", "utf32_german2_ci", False), # 180 + ("utf32", "utf32_croatian_ci", False), # 181 + ("utf32", "utf32_unicode_520_ci", False), # 182 + ("utf32", "utf32_vietnamese_ci", False), # 183 + None, + None, + None, + None, + None, + None, + None, + None, + ("utf8mb3", "utf8mb3_unicode_ci", False), # 192 + ("utf8mb3", "utf8mb3_icelandic_ci", False), # 193 + ("utf8mb3", "utf8mb3_latvian_ci", False), # 194 + ("utf8mb3", "utf8mb3_romanian_ci", False), # 195 + ("utf8mb3", "utf8mb3_slovenian_ci", False), # 196 + ("utf8mb3", "utf8mb3_polish_ci", False), # 197 + ("utf8mb3", "utf8mb3_estonian_ci", False), # 198 + ("utf8mb3", "utf8mb3_spanish_ci", False), # 199 + ("utf8mb3", "utf8mb3_swedish_ci", False), # 200 + ("utf8mb3", "utf8mb3_turkish_ci", False), # 201 + ("utf8mb3", "utf8mb3_czech_ci", False), # 202 + ("utf8mb3", "utf8mb3_danish_ci", False), # 203 + ("utf8mb3", "utf8mb3_lithuanian_ci", False), # 204 + ("utf8mb3", "utf8mb3_slovak_ci", False), # 205 + ("utf8mb3", "utf8mb3_spanish2_ci", False), # 206 + ("utf8mb3", "utf8mb3_roman_ci", False), # 207 + ("utf8mb3", "utf8mb3_persian_ci", False), # 208 + ("utf8mb3", "utf8mb3_esperanto_ci", False), # 209 + ("utf8mb3", "utf8mb3_hungarian_ci", False), # 210 + ("utf8mb3", "utf8mb3_sinhala_ci", False), # 211 + ("utf8mb3", "utf8mb3_german2_ci", False), # 212 + ("utf8mb3", "utf8mb3_croatian_ci", False), # 213 + ("utf8mb3", "utf8mb3_unicode_520_ci", False), # 214 + ("utf8mb3", "utf8mb3_vietnamese_ci", False), # 215 + None, + None, + None, + None, + None, + None, + None, + ("utf8mb3", "utf8mb3_general_mysql500_ci", False), # 223 + ("utf8mb4", "utf8mb4_unicode_ci", False), # 224 + ("utf8mb4", "utf8mb4_icelandic_ci", False), # 225 + ("utf8mb4", "utf8mb4_latvian_ci", False), # 226 + ("utf8mb4", "utf8mb4_romanian_ci", False), # 227 + ("utf8mb4", "utf8mb4_slovenian_ci", False), # 228 + ("utf8mb4", "utf8mb4_polish_ci", False), # 229 + ("utf8mb4", "utf8mb4_estonian_ci", False), # 230 + ("utf8mb4", "utf8mb4_spanish_ci", False), # 231 + ("utf8mb4", "utf8mb4_swedish_ci", False), # 232 + ("utf8mb4", "utf8mb4_turkish_ci", False), # 233 + ("utf8mb4", "utf8mb4_czech_ci", False), # 234 + ("utf8mb4", "utf8mb4_danish_ci", False), # 235 + ("utf8mb4", "utf8mb4_lithuanian_ci", False), # 236 + ("utf8mb4", "utf8mb4_slovak_ci", False), # 237 + ("utf8mb4", "utf8mb4_spanish2_ci", False), # 238 + ("utf8mb4", "utf8mb4_roman_ci", False), # 239 + ("utf8mb4", "utf8mb4_persian_ci", False), # 240 + ("utf8mb4", "utf8mb4_esperanto_ci", False), # 241 + ("utf8mb4", "utf8mb4_hungarian_ci", False), # 242 + ("utf8mb4", "utf8mb4_sinhala_ci", False), # 243 + ("utf8mb4", "utf8mb4_german2_ci", False), # 244 + ("utf8mb4", "utf8mb4_croatian_ci", False), # 245 + ("utf8mb4", "utf8mb4_unicode_520_ci", False), # 246 + ("utf8mb4", "utf8mb4_vietnamese_ci", False), # 247 + ("gb18030", "gb18030_chinese_ci", True), # 248 + ("gb18030", "gb18030_bin", False), # 249 + ("gb18030", "gb18030_unicode_520_ci", False), # 250 + None, + None, + None, + None, + ("utf8mb4", "utf8mb4_0900_ai_ci", True), # 255 + ("utf8mb4", "utf8mb4_de_pb_0900_ai_ci", False), # 256 + ("utf8mb4", "utf8mb4_is_0900_ai_ci", False), # 257 + ("utf8mb4", "utf8mb4_lv_0900_ai_ci", False), # 258 + ("utf8mb4", "utf8mb4_ro_0900_ai_ci", False), # 259 + ("utf8mb4", "utf8mb4_sl_0900_ai_ci", False), # 260 + ("utf8mb4", "utf8mb4_pl_0900_ai_ci", False), # 261 + ("utf8mb4", "utf8mb4_et_0900_ai_ci", False), # 262 + ("utf8mb4", "utf8mb4_es_0900_ai_ci", False), # 263 + ("utf8mb4", "utf8mb4_sv_0900_ai_ci", False), # 264 + ("utf8mb4", "utf8mb4_tr_0900_ai_ci", False), # 265 + ("utf8mb4", "utf8mb4_cs_0900_ai_ci", False), # 266 + ("utf8mb4", "utf8mb4_da_0900_ai_ci", False), # 267 + ("utf8mb4", "utf8mb4_lt_0900_ai_ci", False), # 268 + ("utf8mb4", "utf8mb4_sk_0900_ai_ci", False), # 269 + ("utf8mb4", "utf8mb4_es_trad_0900_ai_ci", False), # 270 + ("utf8mb4", "utf8mb4_la_0900_ai_ci", False), # 271 + None, + ("utf8mb4", "utf8mb4_eo_0900_ai_ci", False), # 273 + ("utf8mb4", "utf8mb4_hu_0900_ai_ci", False), # 274 + ("utf8mb4", "utf8mb4_hr_0900_ai_ci", False), # 275 + None, + ("utf8mb4", "utf8mb4_vi_0900_ai_ci", False), # 277 + ("utf8mb4", "utf8mb4_0900_as_cs", False), # 278 + ("utf8mb4", "utf8mb4_de_pb_0900_as_cs", False), # 279 + ("utf8mb4", "utf8mb4_is_0900_as_cs", False), # 280 + ("utf8mb4", "utf8mb4_lv_0900_as_cs", False), # 281 + ("utf8mb4", "utf8mb4_ro_0900_as_cs", False), # 282 + ("utf8mb4", "utf8mb4_sl_0900_as_cs", False), # 283 + ("utf8mb4", "utf8mb4_pl_0900_as_cs", False), # 284 + ("utf8mb4", "utf8mb4_et_0900_as_cs", False), # 285 + ("utf8mb4", "utf8mb4_es_0900_as_cs", False), # 286 + ("utf8mb4", "utf8mb4_sv_0900_as_cs", False), # 287 + ("utf8mb4", "utf8mb4_tr_0900_as_cs", False), # 288 + ("utf8mb4", "utf8mb4_cs_0900_as_cs", False), # 289 + ("utf8mb4", "utf8mb4_da_0900_as_cs", False), # 290 + ("utf8mb4", "utf8mb4_lt_0900_as_cs", False), # 291 + ("utf8mb4", "utf8mb4_sk_0900_as_cs", False), # 292 + ("utf8mb4", "utf8mb4_es_trad_0900_as_cs", False), # 293 + ("utf8mb4", "utf8mb4_la_0900_as_cs", False), # 294 + None, + ("utf8mb4", "utf8mb4_eo_0900_as_cs", False), # 296 + ("utf8mb4", "utf8mb4_hu_0900_as_cs", False), # 297 + ("utf8mb4", "utf8mb4_hr_0900_as_cs", False), # 298 + None, + ("utf8mb4", "utf8mb4_vi_0900_as_cs", False), # 300 + None, + None, + ("utf8mb4", "utf8mb4_ja_0900_as_cs", False), # 303 + ("utf8mb4", "utf8mb4_ja_0900_as_cs_ks", False), # 304 + ("utf8mb4", "utf8mb4_0900_as_ci", False), # 305 + ("utf8mb4", "utf8mb4_ru_0900_ai_ci", False), # 306 + ("utf8mb4", "utf8mb4_ru_0900_as_cs", False), # 307 + ("utf8mb4", "utf8mb4_zh_0900_as_cs", False), # 308 + ("utf8mb4", "utf8mb4_0900_bin", False), # 309 + ("utf8mb4", "utf8mb4_nb_0900_ai_ci", False), # 310 + ("utf8mb4", "utf8mb4_nb_0900_as_cs", False), # 311 + ("utf8mb4", "utf8mb4_nn_0900_ai_ci", False), # 312 + ("utf8mb4", "utf8mb4_nn_0900_as_cs", False), # 313 + ("utf8mb4", "utf8mb4_sr_latn_0900_ai_ci", False), # 314 + ("utf8mb4", "utf8mb4_sr_latn_0900_as_cs", False), # 315 + ("utf8mb4", "utf8mb4_bs_0900_ai_ci", False), # 316 + ("utf8mb4", "utf8mb4_bs_0900_as_cs", False), # 317 + ("utf8mb4", "utf8mb4_bg_0900_ai_ci", False), # 318 + ("utf8mb4", "utf8mb4_bg_0900_as_cs", False), # 319 + ("utf8mb4", "utf8mb4_gl_0900_ai_ci", False), # 320 + ("utf8mb4", "utf8mb4_gl_0900_as_cs", False), # 321 + ("utf8mb4", "utf8mb4_mn_cyrl_0900_ai_ci", False), # 322 + ("utf8mb4", "utf8mb4_mn_cyrl_0900_as_cs", False), # 323 +] + +MYSQL_CHARACTER_SETS_57: List[Optional[Tuple[str, str, bool]]] = [ + # (character set name, collation, default) + None, + ("big5", "big5_chinese_ci", True), # 1 + ("latin2", "latin2_czech_cs", False), # 2 + ("dec8", "dec8_swedish_ci", True), # 3 + ("cp850", "cp850_general_ci", True), # 4 + ("latin1", "latin1_german1_ci", False), # 5 + ("hp8", "hp8_english_ci", True), # 6 + ("koi8r", "koi8r_general_ci", True), # 7 + ("latin1", "latin1_swedish_ci", True), # 8 + ("latin2", "latin2_general_ci", True), # 9 + ("swe7", "swe7_swedish_ci", True), # 10 + ("ascii", "ascii_general_ci", True), # 11 + ("ujis", "ujis_japanese_ci", True), # 12 + ("sjis", "sjis_japanese_ci", True), # 13 + ("cp1251", "cp1251_bulgarian_ci", False), # 14 + ("latin1", "latin1_danish_ci", False), # 15 + ("hebrew", "hebrew_general_ci", True), # 16 + None, + ("tis620", "tis620_thai_ci", True), # 18 + ("euckr", "euckr_korean_ci", True), # 19 + ("latin7", "latin7_estonian_cs", False), # 20 + ("latin2", "latin2_hungarian_ci", False), # 21 + ("koi8u", "koi8u_general_ci", True), # 22 + ("cp1251", "cp1251_ukrainian_ci", False), # 23 + ("gb2312", "gb2312_chinese_ci", True), # 24 + ("greek", "greek_general_ci", True), # 25 + ("cp1250", "cp1250_general_ci", True), # 26 + ("latin2", "latin2_croatian_ci", False), # 27 + ("gbk", "gbk_chinese_ci", True), # 28 + ("cp1257", "cp1257_lithuanian_ci", False), # 29 + ("latin5", "latin5_turkish_ci", True), # 30 + ("latin1", "latin1_german2_ci", False), # 31 + ("armscii8", "armscii8_general_ci", True), # 32 + ("utf8", "utf8_general_ci", True), # 33 + ("cp1250", "cp1250_czech_cs", False), # 34 + ("ucs2", "ucs2_general_ci", True), # 35 + ("cp866", "cp866_general_ci", True), # 36 + ("keybcs2", "keybcs2_general_ci", True), # 37 + ("macce", "macce_general_ci", True), # 38 + ("macroman", "macroman_general_ci", True), # 39 + ("cp852", "cp852_general_ci", True), # 40 + ("latin7", "latin7_general_ci", True), # 41 + ("latin7", "latin7_general_cs", False), # 42 + ("macce", "macce_bin", False), # 43 + ("cp1250", "cp1250_croatian_ci", False), # 44 + ("utf8mb4", "utf8mb4_general_ci", True), # 45 + ("utf8mb4", "utf8mb4_bin", False), # 46 + ("latin1", "latin1_bin", False), # 47 + ("latin1", "latin1_general_ci", False), # 48 + ("latin1", "latin1_general_cs", False), # 49 + ("cp1251", "cp1251_bin", False), # 50 + ("cp1251", "cp1251_general_ci", True), # 51 + ("cp1251", "cp1251_general_cs", False), # 52 + ("macroman", "macroman_bin", False), # 53 + ("utf16", "utf16_general_ci", True), # 54 + ("utf16", "utf16_bin", False), # 55 + ("utf16le", "utf16le_general_ci", True), # 56 + ("cp1256", "cp1256_general_ci", True), # 57 + ("cp1257", "cp1257_bin", False), # 58 + ("cp1257", "cp1257_general_ci", True), # 59 + ("utf32", "utf32_general_ci", True), # 60 + ("utf32", "utf32_bin", False), # 61 + ("utf16le", "utf16le_bin", False), # 62 + ("binary", "binary", True), # 63 + ("armscii8", "armscii8_bin", False), # 64 + ("ascii", "ascii_bin", False), # 65 + ("cp1250", "cp1250_bin", False), # 66 + ("cp1256", "cp1256_bin", False), # 67 + ("cp866", "cp866_bin", False), # 68 + ("dec8", "dec8_bin", False), # 69 + ("greek", "greek_bin", False), # 70 + ("hebrew", "hebrew_bin", False), # 71 + ("hp8", "hp8_bin", False), # 72 + ("keybcs2", "keybcs2_bin", False), # 73 + ("koi8r", "koi8r_bin", False), # 74 + ("koi8u", "koi8u_bin", False), # 75 + None, + ("latin2", "latin2_bin", False), # 77 + ("latin5", "latin5_bin", False), # 78 + ("latin7", "latin7_bin", False), # 79 + ("cp850", "cp850_bin", False), # 80 + ("cp852", "cp852_bin", False), # 81 + ("swe7", "swe7_bin", False), # 82 + ("utf8", "utf8_bin", False), # 83 + ("big5", "big5_bin", False), # 84 + ("euckr", "euckr_bin", False), # 85 + ("gb2312", "gb2312_bin", False), # 86 + ("gbk", "gbk_bin", False), # 87 + ("sjis", "sjis_bin", False), # 88 + ("tis620", "tis620_bin", False), # 89 + ("ucs2", "ucs2_bin", False), # 90 + ("ujis", "ujis_bin", False), # 91 + ("geostd8", "geostd8_general_ci", True), # 92 + ("geostd8", "geostd8_bin", False), # 93 + ("latin1", "latin1_spanish_ci", False), # 94 + ("cp932", "cp932_japanese_ci", True), # 95 + ("cp932", "cp932_bin", False), # 96 + ("eucjpms", "eucjpms_japanese_ci", True), # 97 + ("eucjpms", "eucjpms_bin", False), # 98 + ("cp1250", "cp1250_polish_ci", False), # 99 + None, + ("utf16", "utf16_unicode_ci", False), # 101 + ("utf16", "utf16_icelandic_ci", False), # 102 + ("utf16", "utf16_latvian_ci", False), # 103 + ("utf16", "utf16_romanian_ci", False), # 104 + ("utf16", "utf16_slovenian_ci", False), # 105 + ("utf16", "utf16_polish_ci", False), # 106 + ("utf16", "utf16_estonian_ci", False), # 107 + ("utf16", "utf16_spanish_ci", False), # 108 + ("utf16", "utf16_swedish_ci", False), # 109 + ("utf16", "utf16_turkish_ci", False), # 110 + ("utf16", "utf16_czech_ci", False), # 111 + ("utf16", "utf16_danish_ci", False), # 112 + ("utf16", "utf16_lithuanian_ci", False), # 113 + ("utf16", "utf16_slovak_ci", False), # 114 + ("utf16", "utf16_spanish2_ci", False), # 115 + ("utf16", "utf16_roman_ci", False), # 116 + ("utf16", "utf16_persian_ci", False), # 117 + ("utf16", "utf16_esperanto_ci", False), # 118 + ("utf16", "utf16_hungarian_ci", False), # 119 + ("utf16", "utf16_sinhala_ci", False), # 120 + ("utf16", "utf16_german2_ci", False), # 121 + ("utf16", "utf16_croatian_ci", False), # 122 + ("utf16", "utf16_unicode_520_ci", False), # 123 + ("utf16", "utf16_vietnamese_ci", False), # 124 + None, + None, + None, + ("ucs2", "ucs2_unicode_ci", False), # 128 + ("ucs2", "ucs2_icelandic_ci", False), # 129 + ("ucs2", "ucs2_latvian_ci", False), # 130 + ("ucs2", "ucs2_romanian_ci", False), # 131 + ("ucs2", "ucs2_slovenian_ci", False), # 132 + ("ucs2", "ucs2_polish_ci", False), # 133 + ("ucs2", "ucs2_estonian_ci", False), # 134 + ("ucs2", "ucs2_spanish_ci", False), # 135 + ("ucs2", "ucs2_swedish_ci", False), # 136 + ("ucs2", "ucs2_turkish_ci", False), # 137 + ("ucs2", "ucs2_czech_ci", False), # 138 + ("ucs2", "ucs2_danish_ci", False), # 139 + ("ucs2", "ucs2_lithuanian_ci", False), # 140 + ("ucs2", "ucs2_slovak_ci", False), # 141 + ("ucs2", "ucs2_spanish2_ci", False), # 142 + ("ucs2", "ucs2_roman_ci", False), # 143 + ("ucs2", "ucs2_persian_ci", False), # 144 + ("ucs2", "ucs2_esperanto_ci", False), # 145 + ("ucs2", "ucs2_hungarian_ci", False), # 146 + ("ucs2", "ucs2_sinhala_ci", False), # 147 + ("ucs2", "ucs2_german2_ci", False), # 148 + ("ucs2", "ucs2_croatian_ci", False), # 149 + ("ucs2", "ucs2_unicode_520_ci", False), # 150 + ("ucs2", "ucs2_vietnamese_ci", False), # 151 + None, + None, + None, + None, + None, + None, + None, + ("ucs2", "ucs2_general_mysql500_ci", False), # 159 + ("utf32", "utf32_unicode_ci", False), # 160 + ("utf32", "utf32_icelandic_ci", False), # 161 + ("utf32", "utf32_latvian_ci", False), # 162 + ("utf32", "utf32_romanian_ci", False), # 163 + ("utf32", "utf32_slovenian_ci", False), # 164 + ("utf32", "utf32_polish_ci", False), # 165 + ("utf32", "utf32_estonian_ci", False), # 166 + ("utf32", "utf32_spanish_ci", False), # 167 + ("utf32", "utf32_swedish_ci", False), # 168 + ("utf32", "utf32_turkish_ci", False), # 169 + ("utf32", "utf32_czech_ci", False), # 170 + ("utf32", "utf32_danish_ci", False), # 171 + ("utf32", "utf32_lithuanian_ci", False), # 172 + ("utf32", "utf32_slovak_ci", False), # 173 + ("utf32", "utf32_spanish2_ci", False), # 174 + ("utf32", "utf32_roman_ci", False), # 175 + ("utf32", "utf32_persian_ci", False), # 176 + ("utf32", "utf32_esperanto_ci", False), # 177 + ("utf32", "utf32_hungarian_ci", False), # 178 + ("utf32", "utf32_sinhala_ci", False), # 179 + ("utf32", "utf32_german2_ci", False), # 180 + ("utf32", "utf32_croatian_ci", False), # 181 + ("utf32", "utf32_unicode_520_ci", False), # 182 + ("utf32", "utf32_vietnamese_ci", False), # 183 + None, + None, + None, + None, + None, + None, + None, + None, + ("utf8", "utf8_unicode_ci", False), # 192 + ("utf8", "utf8_icelandic_ci", False), # 193 + ("utf8", "utf8_latvian_ci", False), # 194 + ("utf8", "utf8_romanian_ci", False), # 195 + ("utf8", "utf8_slovenian_ci", False), # 196 + ("utf8", "utf8_polish_ci", False), # 197 + ("utf8", "utf8_estonian_ci", False), # 198 + ("utf8", "utf8_spanish_ci", False), # 199 + ("utf8", "utf8_swedish_ci", False), # 200 + ("utf8", "utf8_turkish_ci", False), # 201 + ("utf8", "utf8_czech_ci", False), # 202 + ("utf8", "utf8_danish_ci", False), # 203 + ("utf8", "utf8_lithuanian_ci", False), # 204 + ("utf8", "utf8_slovak_ci", False), # 205 + ("utf8", "utf8_spanish2_ci", False), # 206 + ("utf8", "utf8_roman_ci", False), # 207 + ("utf8", "utf8_persian_ci", False), # 208 + ("utf8", "utf8_esperanto_ci", False), # 209 + ("utf8", "utf8_hungarian_ci", False), # 210 + ("utf8", "utf8_sinhala_ci", False), # 211 + ("utf8", "utf8_german2_ci", False), # 212 + ("utf8", "utf8_croatian_ci", False), # 213 + ("utf8", "utf8_unicode_520_ci", False), # 214 + ("utf8", "utf8_vietnamese_ci", False), # 215 + None, + None, + None, + None, + None, + None, + None, + ("utf8", "utf8_general_mysql500_ci", False), # 223 + ("utf8mb4", "utf8mb4_unicode_ci", False), # 224 + ("utf8mb4", "utf8mb4_icelandic_ci", False), # 225 + ("utf8mb4", "utf8mb4_latvian_ci", False), # 226 + ("utf8mb4", "utf8mb4_romanian_ci", False), # 227 + ("utf8mb4", "utf8mb4_slovenian_ci", False), # 228 + ("utf8mb4", "utf8mb4_polish_ci", False), # 229 + ("utf8mb4", "utf8mb4_estonian_ci", False), # 230 + ("utf8mb4", "utf8mb4_spanish_ci", False), # 231 + ("utf8mb4", "utf8mb4_swedish_ci", False), # 232 + ("utf8mb4", "utf8mb4_turkish_ci", False), # 233 + ("utf8mb4", "utf8mb4_czech_ci", False), # 234 + ("utf8mb4", "utf8mb4_danish_ci", False), # 235 + ("utf8mb4", "utf8mb4_lithuanian_ci", False), # 236 + ("utf8mb4", "utf8mb4_slovak_ci", False), # 237 + ("utf8mb4", "utf8mb4_spanish2_ci", False), # 238 + ("utf8mb4", "utf8mb4_roman_ci", False), # 239 + ("utf8mb4", "utf8mb4_persian_ci", False), # 240 + ("utf8mb4", "utf8mb4_esperanto_ci", False), # 241 + ("utf8mb4", "utf8mb4_hungarian_ci", False), # 242 + ("utf8mb4", "utf8mb4_sinhala_ci", False), # 243 + ("utf8mb4", "utf8mb4_german2_ci", False), # 244 + ("utf8mb4", "utf8mb4_croatian_ci", False), # 245 + ("utf8mb4", "utf8mb4_unicode_520_ci", False), # 246 + ("utf8mb4", "utf8mb4_vietnamese_ci", False), # 247 + ("gb18030", "gb18030_chinese_ci", True), # 248 + ("gb18030", "gb18030_bin", False), # 249 + ("gb18030", "gb18030_unicode_520_ci", False), # 250 +] diff --git a/mysql/connector/connection.py b/mysql/connector/connection.py new file mode 100644 index 0000000..850f761 --- /dev/null +++ b/mysql/connector/connection.py @@ -0,0 +1,1741 @@ +# Copyright (c) 2009, 2023, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# mypy: disable-error-code="arg-type,operator,attr-defined,assignment" + +"""Implementing communication with MySQL servers.""" + +import datetime +import getpass +import os +import socket +import struct +import sys +import warnings + +from decimal import Decimal +from io import IOBase +from typing import ( + Any, + BinaryIO, + Dict, + Generator, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + Union, +) + +from . import version +from .abstracts import MySQLConnectionAbstract +from .authentication import get_auth_plugin +from .constants import ( + CharacterSet, + ClientFlag, + FieldType, + ServerCmd, + ServerFlag, + ShutdownType, + flag_is_set, +) +from .conversion import MySQLConverter +from .cursor import ( + CursorBase, + MySQLCursor, + MySQLCursorBuffered, + MySQLCursorBufferedDict, + MySQLCursorBufferedNamedTuple, + MySQLCursorBufferedRaw, + MySQLCursorDict, + MySQLCursorNamedTuple, + MySQLCursorPrepared, + MySQLCursorPreparedDict, + MySQLCursorPreparedNamedTuple, + MySQLCursorPreparedRaw, + MySQLCursorRaw, +) +from .errors import ( + DatabaseError, + Error, + InterfaceError, + InternalError, + NotSupportedError, + OperationalError, + ProgrammingError, + get_exception, +) +from .logger import logger +from .network import MySQLSocket, MySQLTCPSocket, MySQLUnixSocket +from .opentelemetry.constants import OTEL_ENABLED +from .opentelemetry.context_propagation import with_context_propagation +from .plugins import BaseAuthPlugin +from .protocol import MySQLProtocol +from .types import ( + ConnAttrsType, + DescriptionType, + EofPacketType, + HandShakeType, + OkPacketType, + ResultType, + RowType, + StatsPacketType, + StrOrBytes, + SupportedMysqlBinaryProtocolTypes, +) +from .utils import get_platform, int1store, int4store, lc_int + +if OTEL_ENABLED: + from .opentelemetry.instrumentation import end_span, record_exception_event + + +class MySQLConnection(MySQLConnectionAbstract): + """Connection to a MySQL Server""" + + def __init__(self, **kwargs: Any) -> None: + self._protocol: Optional[MySQLProtocol] = None + self._socket: Optional[MySQLSocket] = None + self._handshake: Optional[HandShakeType] = None + super().__init__() + + self._converter_class: Type[MySQLConverter] = MySQLConverter + + self._client_flags: int = ClientFlag.get_default() + self._charset_id: int = 45 + self._sql_mode: Optional[str] = None + self._time_zone: Optional[str] = None + self._autocommit: bool = False + + self._user: str = "" + self._password: str = "" + self._database: str = "" + self._host: str = "127.0.0.1" + self._port: int = 3306 + self._unix_socket: Optional[str] = None + self._client_host: str = "" + self._client_port: int = 0 + self._ssl: Dict[str, Optional[Union[str, bool, List[str]]]] = {} + self._force_ipv6: bool = False + + self._use_unicode: bool = True + self._get_warnings: bool = False + self._raise_on_warnings: bool = False + self._buffered: bool = False + self._unread_result: bool = False + self._have_next_result: bool = False + self._raw: bool = False + self._in_transaction: bool = False + + self._prepared_statements: Any = None + + self._ssl_active: bool = False + self._auth_plugin: Optional[str] = None + self._krb_service_principal: Optional[str] = None + self._pool_config_version: Any = None + self._query_attrs_supported: int = False + + self._columns_desc: List[DescriptionType] = [] + self._mfa_nfactor: int = 1 + + if kwargs: + try: + self.connect(**kwargs) + except Exception: + # Tidy-up underlying socket on failure + self.close() + self._socket = None + raise + + def _add_default_conn_attrs(self) -> None: + """Add the default connection attributes.""" + platform = get_platform() + license_chunks = version.LICENSE.split(" ") + if license_chunks[0] == "GPLv2": + client_license = "GPL-2.0" + else: + client_license = "Commercial" + default_conn_attrs = { + "_pid": str(os.getpid()), + "_platform": platform["arch"], + "_source_host": socket.gethostname(), + "_client_name": "mysql-connector-python", + "_client_license": client_license, + "_client_version": ".".join([str(x) for x in version.VERSION[0:3]]), + "_os": platform["version"], + } + + self._conn_attrs.update((default_conn_attrs)) + + def _do_handshake(self) -> None: + """Get the handshake from the MySQL server""" + packet = self._socket.recv() + if packet[4] == 255: + raise get_exception(packet) + + self._handshake = None + handshake = self._protocol.parse_handshake(packet) + + server_version = handshake["server_version_original"] + + self._server_version = self._check_server_version( + server_version + if isinstance(server_version, (str, bytes, bytearray)) + else "Unknown" + ) + CharacterSet.set_mysql_version(self._server_version) + + if not handshake["capabilities"] & ClientFlag.SSL: + if self._auth_plugin == "mysql_clear_password" and not self.is_secure: + raise InterfaceError( + "Clear password authentication is not supported over " + "insecure channels" + ) + if self._ssl.get("verify_cert"): + raise InterfaceError( + "SSL is required but the server doesn't support it", + errno=2026, + ) + self._client_flags &= ~ClientFlag.SSL + elif not self._ssl_disabled: + self._client_flags |= ClientFlag.SSL + + if handshake["capabilities"] & ClientFlag.PLUGIN_AUTH: + self.set_client_flags([ClientFlag.PLUGIN_AUTH]) + + if handshake["capabilities"] & ClientFlag.CLIENT_QUERY_ATTRIBUTES: + self._query_attrs_supported = True + self.set_client_flags([ClientFlag.CLIENT_QUERY_ATTRIBUTES]) + + if handshake["capabilities"] & ClientFlag.MULTI_FACTOR_AUTHENTICATION: + self.set_client_flags([ClientFlag.MULTI_FACTOR_AUTHENTICATION]) + + self._handshake = handshake + + def _do_auth( + self, + username: Optional[str] = None, + password: Optional[str] = None, + database: Optional[str] = None, + client_flags: int = 0, + charset: int = 45, + ssl_options: Optional[Dict[str, Optional[Union[str, bool, List[str]]]]] = None, + conn_attrs: Optional[ConnAttrsType] = None, + ) -> bool: + """Authenticate with the MySQL server + + Authentication happens in two parts. We first send a response to the + handshake. The MySQL server will then send either an AuthSwitchRequest + or an error packet. + + Raises NotSupportedError when we get the old, insecure password + reply back. Raises any error coming from MySQL. + """ + self._ssl_active = False + if ssl_options is None: + ssl_options = {} + if not self._ssl_disabled and (client_flags & ClientFlag.SSL): + packet = self._protocol.make_auth_ssl( + charset=charset, client_flags=client_flags + ) + self._socket.send(packet) + if ssl_options.get("tls_ciphersuites") is not None: + tls_ciphersuites = ":".join(ssl_options.get("tls_ciphersuites")) + else: + tls_ciphersuites = "" + self._socket.switch_to_ssl( + ssl_options.get("ca"), + ssl_options.get("cert"), + ssl_options.get("key"), + ssl_options.get("verify_cert") or False, + ssl_options.get("verify_identity") or False, + tls_ciphersuites, + ssl_options.get("tls_versions"), + ) + self._ssl_active = True + + if self._password1 and password != self._password1: + password = self._password1 + + logger.debug("# _do_auth(): self._auth_plugin: %s", self._auth_plugin) + if ( + self._auth_plugin.startswith("authentication_oci") + or ( + self._auth_plugin.startswith("authentication_kerberos") + and os.name == "nt" + ) + ) and not username: + username = getpass.getuser() + logger.debug( + "MySQL user is empty, OS user: %s will be used for %s", + username, + self._auth_plugin, + ) + + packet = self._protocol.make_auth( + handshake=self._handshake, + username=username, + password=password, + database=database, + charset=charset, + client_flags=client_flags, + ssl_enabled=self._ssl_active, + auth_plugin=self._auth_plugin, + conn_attrs=conn_attrs, + auth_plugin_class=self._auth_plugin_class, + ) + self._socket.send(packet) + self._auth_switch_request(username, password) + + if not (client_flags & ClientFlag.CONNECT_WITH_DB) and database: + self.cmd_init_db(database) + + return True + + def _auth_switch_request( + self, username: Optional[str] = None, password: Optional[str] = None + ) -> Optional[OkPacketType]: + """Handle second part of authentication + + Raises NotSupportedError when we get the old, insecure password + reply back. Raises any error coming from MySQL. + """ + auth = None + new_auth_plugin: Optional[str] = ( + self._auth_plugin or self._handshake["auth_plugin"] + ) + logger.debug("new_auth_plugin: %s", new_auth_plugin) + packet = self._socket.recv() + if packet[4] == 254 and len(packet) == 5: + raise NotSupportedError( + "Authentication with old (insecure) passwords " + "is not supported. For more information, lookup " + "Password Hashing in the latest MySQL manual" + ) + if packet[4] == 254: + # AuthSwitchRequest + ( + new_auth_plugin, + auth_data, + ) = self._protocol.parse_auth_switch_request(packet) + auth = get_auth_plugin(new_auth_plugin, self._auth_plugin_class)( + auth_data, + username=username or self._user, + password=password, + ssl_enabled=self.is_secure, + ) + packet = self._auth_continue(auth, new_auth_plugin, auth_data) + + if packet[4] == 1: + auth_data = self._protocol.parse_auth_more_data(packet) + auth = get_auth_plugin(new_auth_plugin, self._auth_plugin_class)( + auth_data, password=password, ssl_enabled=self.is_secure + ) + if new_auth_plugin == "caching_sha2_password": + response = auth.auth_response() + if response: + self._socket.send(response) + packet = self._socket.recv() + + if packet[4] == 0: + return self._handle_ok(packet) + if packet[4] == 2: + return self._handle_mfa(packet) + if packet[4] == 255: + raise get_exception(packet) + return None + + def _handle_mfa(self, packet: bytes) -> Optional[OkPacketType]: + """Handle Multi Factor Authentication.""" + self._mfa_nfactor += 1 + if self._mfa_nfactor == 2: + password = self._password2 + elif self._mfa_nfactor == 3: + password = self._password3 + else: + raise InterfaceError( + "Failed Multi Factor Authentication (invalid N factor)" + ) + + logger.debug("# MFA N Factor #%d", self._mfa_nfactor) + + packet, auth_plugin = self._protocol.parse_auth_next_factor(packet[4:]) + auth = get_auth_plugin(auth_plugin, self._auth_plugin_class)( + None, + username=self._user, + password=password, + ssl_enabled=self.is_secure, + ) + packet = self._auth_continue(auth, auth_plugin, packet) + + if packet[4] == 1: + auth_data = self._protocol.parse_auth_more_data(packet) + auth = get_auth_plugin(auth_plugin, self._auth_plugin_class)( + auth_data, password=password, ssl_enabled=self.is_secure + ) + if auth_plugin == "caching_sha2_password": + response = auth.auth_response() + if response: + self._socket.send(response) + packet = self._socket.recv() + + if packet[4] == 0: + return self._handle_ok(packet) + if packet[4] == 2: + return self._handle_mfa(packet) + if packet[4] == 255: + raise get_exception(packet) + return None + + def _auth_continue( + self, auth: BaseAuthPlugin, auth_plugin: str, auth_data: bytes + ) -> bytes: + """Continue with the authentication.""" + if auth_plugin == "authentication_ldap_sasl_client": + logger.debug("# auth_data: %s", auth_data) + response = auth.auth_response(self._krb_service_principal) + elif auth_plugin == "authentication_kerberos_client": + logger.debug("# auth_data: %s", auth_data) + response = auth.auth_response(auth_data) + elif auth_plugin == "authentication_oci_client": + logger.debug("# oci configuration file path: %s", self._oci_config_file) + auth.oci_config_file = self._oci_config_file + auth.oci_config_profile = self._oci_config_profile + response = auth.auth_response() + else: + response = auth.auth_response() + + logger.debug("# request: %s size: %s", response, len(response)) + self._socket.send(response) + packet = self._socket.recv() + logger.debug("# server response packet: %s", packet) + if ( + auth_plugin == "authentication_ldap_sasl_client" + and len(packet) >= 6 + and packet[5] == 114 + and packet[6] == 61 + ): # 'r' and '=' + # Continue with sasl authentication + dec_response = packet[5:] + cresponse = auth.auth_continue(dec_response) + self._socket.send(cresponse) + packet = self._socket.recv() + if packet[5] == 118 and packet[6] == 61: # 'v' and '=' + if auth.auth_finalize(packet[5:]): + # receive packed OK + packet = self._socket.recv() + elif ( + auth_plugin == "authentication_ldap_sasl_client" + and auth_data == b"GSSAPI" + and packet[4] != 255 + ): + rcode_size = 5 # header size for the response status code. + logger.debug("# Continue with sasl GSSAPI authentication") + logger.debug("# response header: %s", packet[: rcode_size + 1]) + logger.debug("# response size: %s", len(packet)) + + logger.debug("# Negotiate a service request") + complete = False + tries = 0 # To avoid a infinite loop attempt no more than feedback messages + while not complete and tries < 5: + logger.debug("%s Attempt %s %s", "-" * 20, tries + 1, "-" * 20) + logger.debug("<< server response: %s", packet) + logger.debug("# response code: %s", packet[: rcode_size + 1]) + step, complete = auth.auth_continue_krb(packet[rcode_size:]) + logger.debug(" >> response to server: %s", step) + self._socket.send(step or b"") + packet = self._socket.recv() + tries += 1 + if not complete: + raise InterfaceError( + f"Unable to fulfill server request after {tries} " + f"attempts. Last server response: {packet}" + ) + logger.debug( + " last GSSAPI response from server: %s length: %d", + packet, + len(packet), + ) + last_step = auth.auth_accept_close_handshake(packet[rcode_size:]) + logger.debug( + " >> last response to server: %s length: %d", + last_step, + len(last_step), + ) + self._socket.send(last_step) + # Receive final handshake from server + packet = self._socket.recv() + logger.debug("<< final handshake from server: %s", packet) + + # receive OK packet from server. + packet = self._socket.recv() + logger.debug("<< ok packet from server: %s", packet) + elif auth_plugin == "authentication_kerberos_client" and packet[4] != 255: + rcode_size = 5 # Reader size for the response status code + logger.debug("# Continue with GSSAPI authentication") + logger.debug("# Response header: %s", packet[: rcode_size + 1]) + logger.debug("# Response size: %s", len(packet)) + logger.debug("# Negotiate a service request") + complete = False + tries = 0 + + while not complete and tries < 5: + logger.debug("%s Attempt %s %s", "-" * 20, tries + 1, "-" * 20) + logger.debug("<< Server response: %s", packet) + logger.debug("# Response code: %s", packet[: rcode_size + 1]) + token, complete = auth.auth_continue(packet[rcode_size:]) + if token: + self._socket.send(token) + if complete: + break + packet = self._socket.recv() + + logger.debug(">> Response to server: %s", token) + tries += 1 + + if not complete: + raise InterfaceError( + f"Unable to fulfill server request after {tries} " + f"attempts. Last server response: {packet}" + ) + + logger.debug( + "Last response from server: %s length: %d", + packet, + len(packet), + ) + + # Receive OK packet from server. + packet = self._socket.recv() + logger.debug("<< Ok packet from server: %s", packet) + + return bytes(packet) + + def _get_connection(self) -> MySQLSocket: + """Get connection based on configuration + + This method will return the appropriated connection object using + the connection parameters. + + Returns subclass of MySQLBaseSocket. + """ + conn: Optional[MySQLSocket] = None + if self._unix_socket and os.name == "posix": + conn = MySQLUnixSocket(unix_socket=self.unix_socket) + else: + conn = MySQLTCPSocket( + host=self.server_host, + port=self.server_port, + force_ipv6=self._force_ipv6, + ) + + conn.set_connection_timeout(self._connection_timeout) + return conn + + def _open_connection(self) -> None: + """Open the connection to the MySQL server + + This method sets up and opens the connection to the MySQL server. + + Raises on errors. + """ + if self._auth_plugin == "authentication_kerberos_client" and not self._user: + cls = get_auth_plugin(self._auth_plugin, self._auth_plugin_class) + self._user = cls.get_user_from_credentials() + + self._protocol = MySQLProtocol() + self._socket = self._get_connection() + try: + self._socket.open_connection() + + # do initial handshake + self._do_handshake() + + # start authentication negotiation + self._do_auth( + self._user, + self._password, + self._database, + self._client_flags, + self._charset_id, + self._ssl, + self._conn_attrs, + ) + self.set_converter_class(self._converter_class) + + if self._client_flags & ClientFlag.COMPRESS: + # update the network layer accordingly + self._socket.switch_to_compressed_mode() + + self._socket.set_connection_timeout(None) + except Exception: + # close socket + self._socket.close_connection() + raise + + if ( + not self._ssl_disabled + and hasattr(self._socket.sock, "version") + and callable(self._socket.sock.version) + ): + # Raise a deprecation warning if TLSv1 or TLSv1.1 is being used + tls_version = self._socket.sock.version() + if tls_version in ("TLSv1", "TLSv1.1"): + warn_msg = ( + f"This connection is using {tls_version} which is now " + "deprecated and will be removed in a future release of " + "MySQL Connector/Python" + ) + warnings.warn(warn_msg, DeprecationWarning) + + def shutdown(self) -> None: + """Shut down connection to MySQL Server.""" + if not self._socket: + return + + try: + self._socket.shutdown() + except (AttributeError, Error): + pass # Getting an exception would mean we are disconnected. + + def close(self) -> None: + """Disconnect from the MySQL server""" + if self._span and self._span.is_recording(): + record_exception_event(self._span, sys.exc_info()[1]) + + if not self._socket: + return + + try: + self.cmd_quit() + except (AttributeError, Error): + pass # Getting an exception would mean we are disconnected. + + try: + self._socket.close_connection() + except Exception as err: + if OTEL_ENABLED: + record_exception_event(self._span, err) + raise + finally: + if OTEL_ENABLED: + end_span(self._span) + + self._handshake = None + + disconnect = close + + def _send_cmd( + self, + command: int, + argument: Optional[bytes] = None, + packet_number: int = 0, + packet: Optional[bytes] = None, + expect_response: bool = True, + compressed_packet_number: int = 0, + ) -> Optional[bytearray]: + """Send a command to the MySQL server + + This method sends a command with an optional argument. + If packet is not None, it will be sent and the argument will be + ignored. + + The packet_number is optional and should usually not be used. + + Some commands might not result in the MySQL server returning + a response. If a command does not return anything, you should + set expect_response to False. The _send_cmd method will then + return None instead of a MySQL packet. + + Returns a MySQL packet or None. + """ + self.handle_unread_result() + + try: + self._socket.send( + self._protocol.make_command(command, packet or argument), + packet_number, + compressed_packet_number, + ) + except AttributeError as err: + raise OperationalError("MySQL Connection not available") from err + + if not expect_response: + return None + return self._socket.recv() + + def _send_data(self, data_file: BinaryIO, send_empty_packet: bool = False) -> bytes: + """Send data to the MySQL server + + This method accepts a file-like object and sends its data + as is to the MySQL server. If the send_empty_packet is + True, it will send an extra empty package (for example + when using LOAD LOCAL DATA INFILE). + + Returns a MySQL packet. + """ + self.handle_unread_result() + + if not hasattr(data_file, "read"): + raise ValueError("expecting a file-like object") + + chunk_size = 131072 # 128 KB + try: + buf = data_file.read(chunk_size - 16) + while buf: + self._socket.send(buf) + buf = data_file.read(chunk_size - 16) + except AttributeError as err: + raise OperationalError("MySQL Connection not available") from err + + if send_empty_packet: + try: + self._socket.send(b"") + except AttributeError as err: + raise OperationalError("MySQL Connection not available") from err + + return bytes(self._socket.recv()) + + def _handle_server_status(self, flags: int) -> None: + """Handle the server flags found in MySQL packets + + This method handles the server flags send by MySQL OK and EOF + packets. It, for example, checks whether there exists more result + sets or whether there is an ongoing transaction. + """ + self._have_next_result = flag_is_set(ServerFlag.MORE_RESULTS_EXISTS, flags) + self._in_transaction = flag_is_set(ServerFlag.STATUS_IN_TRANS, flags) + + @property + def in_transaction(self) -> bool: + """MySQL session has started a transaction""" + return self._in_transaction + + def _handle_ok(self, packet: bytes) -> OkPacketType: + """Handle a MySQL OK packet + + This method handles a MySQL OK packet. When the packet is found to + be an Error packet, an error will be raised. If the packet is neither + an OK or an Error packet, InterfaceError will be raised. + + Returns a dict() + """ + if packet[4] == 0: + ok_pkt = self._protocol.parse_ok(packet) + self._handle_server_status(ok_pkt["status_flag"]) + return ok_pkt + if packet[4] == 255: + raise get_exception(packet) + raise InterfaceError("Expected OK packet") + + def _handle_eof(self, packet: bytes) -> EofPacketType: + """Handle a MySQL EOF packet + + This method handles a MySQL EOF packet. When the packet is found to + be an Error packet, an error will be raised. If the packet is neither + and OK or an Error packet, InterfaceError will be raised. + + Returns a dict() + """ + if packet[4] == 254: + eof = self._protocol.parse_eof(packet) + self._handle_server_status(eof["status_flag"]) + return eof + if packet[4] == 255: + raise get_exception(packet) + raise InterfaceError("Expected EOF packet") + + def _handle_load_data_infile(self, filename: str) -> OkPacketType: + """Handle a LOAD DATA INFILE LOCAL request""" + file_name = os.path.abspath(filename) + if os.path.islink(file_name): + raise OperationalError("Use of symbolic link is not allowed") + if not self._allow_local_infile and not self._allow_local_infile_in_path: + raise DatabaseError( + "LOAD DATA LOCAL INFILE file request rejected due to " + "restrictions on access." + ) + if not self._allow_local_infile and self._allow_local_infile_in_path: + # validate filename is inside of allow_local_infile_in_path path. + infile_path = os.path.abspath(self._allow_local_infile_in_path) + c_path = None + try: + c_path = os.path.commonpath([infile_path, file_name]) + except ValueError as err: + err_msg = ( + "{} while loading file `{}` and path `{}` given" + " in allow_local_infile_in_path" + ) + raise InterfaceError( + err_msg.format(str(err), file_name, infile_path) + ) from err + + if c_path != infile_path: + err_msg = ( + "The file `{}` is not found in the given " + "allow_local_infile_in_path {}" + ) + raise DatabaseError(err_msg.format(file_name, infile_path)) + + try: + data_file = open(file_name, "rb") # pylint: disable=consider-using-with + return self._handle_ok(self._send_data(data_file, send_empty_packet=True)) + except IOError: + # Send a empty packet to cancel the operation + try: + self._socket.send(b"") + except AttributeError as err: + raise OperationalError("MySQL Connection not available") from err + raise InterfaceError(f"File '{file_name}' could not be read") from None + finally: + try: + data_file.close() + except (IOError, NameError): + pass + + def _handle_result(self, packet: bytes) -> ResultType: + """Handle a MySQL Result + + This method handles a MySQL result, for example, after sending the + query command. OK and EOF packets will be handled and returned. If + the packet is an Error packet, an Error-exception will be + raised. + + The dictionary returned of: + - columns: column information + - eof: the EOF-packet information + + Returns a dict() + """ + if not packet or len(packet) < 4: + raise InterfaceError("Empty response") + if packet[4] == 0: + return self._handle_ok(packet) + if packet[4] == 251: + filename = packet[5:].decode() + return self._handle_load_data_infile(filename) + if packet[4] == 254: + return self._handle_eof(packet) + if packet[4] == 255: + raise get_exception(packet) + + # We have a text result set + column_count = self._protocol.parse_column_count(packet) + if not column_count or not isinstance(column_count, int): + raise InterfaceError("Illegal result set") + + self._columns_desc = [ + None, + ] * column_count + for i in range(0, column_count): + self._columns_desc[i] = self._protocol.parse_column( + self._socket.recv(), self.python_charset + ) + + eof = self._handle_eof(self._socket.recv()) + self.unread_result = True + return {"columns": self._columns_desc, "eof": eof} + + def get_row( + self, + binary: bool = False, + columns: Optional[List[DescriptionType]] = None, + raw: Optional[bool] = None, + ) -> Tuple[Optional[RowType], Optional[EofPacketType]]: + """Get the next rows returned by the MySQL server + + This method gets one row from the result set after sending, for + example, the query command. The result is a tuple consisting of the + row and the EOF packet. + If no row was available in the result set, the row data will be None. + + Returns a tuple. + """ + (rows, eof) = self.get_rows(count=1, binary=binary, columns=columns, raw=raw) + if rows: + return (rows[0], eof) + return (None, eof) + + def get_rows( + self, + count: Optional[int] = None, + binary: bool = False, + columns: Optional[List[DescriptionType]] = None, + raw: Optional[bool] = None, + prep_stmt: Any = None, + ) -> Tuple[List[RowType], Optional[EofPacketType]]: + """Get all rows returned by the MySQL server + + This method gets all rows returned by the MySQL server after sending, + for example, the query command. The result is a tuple consisting of + a list of rows and the EOF packet. + + Returns a tuple() + """ + if raw is None: + raw = self._raw + + if not self.unread_result: + raise InternalError("No result set available") + + rows: Tuple[List[Tuple[Any, ...]], Optional[EofPacketType]] = ([], None) + try: + if binary: + charset = self.charset + if charset == "utf8mb4": + charset = "utf8" + rows = self._protocol.read_binary_result( + self._socket, columns, count, charset + ) + else: + rows = self._protocol.read_text_result( + self._socket, self._server_version, count=count + ) + except Error as err: + self.unread_result = False + raise err + + rows, eof_p = rows + if ( + not (binary or raw) + and self._columns_desc is not None + and rows + and hasattr(self, "converter") + ): + row_to_python = self.converter.row_to_python + rows = [row_to_python(row, self._columns_desc) for row in rows] + + if eof_p is not None: + self._handle_server_status( + eof_p["status_flag"] + if "status_flag" in eof_p + else eof_p["server_status"] + ) + self.unread_result = False + + return rows, eof_p + + def consume_results(self) -> None: + """Consume results""" + if self.unread_result: + self.get_rows() + + def cmd_init_db(self, database: str) -> OkPacketType: + """Change the current database + + This method changes the current (default) database by sending the + INIT_DB command. The result is a dictionary containing the OK packet + information. + + Returns a dict() + """ + return self._handle_ok( + self._send_cmd(ServerCmd.INIT_DB, database.encode("utf-8")) + ) + + @with_context_propagation + def cmd_query( + self, + query: StrOrBytes, + raw: bool = False, + buffered: bool = False, + raw_as_string: bool = False, + ) -> ResultType: + """Send a query to the MySQL server + + This method send the query to the MySQL server and returns the result. + + If there was a text result, a tuple will be returned consisting of + the number of columns and a list containing information about these + columns. + + When the query doesn't return a text result, the OK or EOF packet + information as dictionary will be returned. In case the result was + an error, exception Error will be raised. + + Returns a tuple() + """ + if not isinstance(query, bytearray): + if isinstance(query, str): + query = query.encode("utf-8") + query = bytearray(query) + # Prepare query attrs + charset = self.charset if self.charset != "utf8mb4" else "utf8" + packet = bytearray() + if not self._query_attrs_supported and self._query_attrs: + warnings.warn( + "This version of the server does not support Query Attributes", + category=Warning, + ) + if self._client_flags & ClientFlag.CLIENT_QUERY_ATTRIBUTES: + names = [] + types = [] + values: List[bytes] = [] + null_bitmap = [0] * ((len(self._query_attrs) + 7) // 8) + for pos, attr_tuple in enumerate(self._query_attrs.items()): + value = attr_tuple[1] + flags = 0 + if value is None: + null_bitmap[(pos // 8)] |= 1 << (pos % 8) + types.append(int1store(FieldType.NULL) + int1store(flags)) + continue + if isinstance(value, int): + ( + packed, + field_type, + flags, + ) = self._protocol.prepare_binary_integer(value) + values.append(packed) + elif isinstance(value, str): + value = value.encode(charset) + values.append(lc_int(len(value)) + value) + field_type = FieldType.STRING + elif isinstance(value, bytes): + values.append(lc_int(len(value)) + value) + field_type = FieldType.STRING + elif isinstance(value, Decimal): + values.append( + lc_int(len(str(value).encode(charset))) + + str(value).encode(charset) + ) + field_type = FieldType.DECIMAL + elif isinstance(value, float): + values.append(struct.pack(" parameter_count Number of parameters + packet.extend(lc_int(len(self._query_attrs))) + # int parameter_set_count Number of parameter sets. + # Currently always 1 + packet.extend(lc_int(1)) + if values: + packet.extend( + b"".join([struct.pack("B", bit) for bit in null_bitmap]) + + int1store(1) + ) + for _type, name in zip(types, names): + packet.extend(_type) + packet.extend(name) + + for value in values: + packet.extend(value) + + packet.extend(query) + query = bytes(packet) + try: + result = self._handle_result(self._send_cmd(ServerCmd.QUERY, query)) + except ProgrammingError as err: + if err.errno == 3948 and "Loading local data is disabled" in err.msg: + err_msg = ( + "LOAD DATA LOCAL INFILE file request rejected due " + "to restrictions on access." + ) + raise DatabaseError(err_msg) from err + raise + if self._have_next_result: + raise InterfaceError( + "Use cmd_query_iter for statements with multiple queries." + ) + + return result + + def cmd_query_iter( + self, statements: StrOrBytes + ) -> Generator[ResultType, None, None]: + """Send one or more statements to the MySQL server + + Similar to the cmd_query method, but instead returns a generator + object to iterate through results. It sends the statements to the + MySQL server and through the iterator you can get the results. + + statement = 'SELECT 1; INSERT INTO t1 VALUES (); SELECT 2' + for result in cnx.cmd_query(statement, iterate=True): + if 'columns' in result: + columns = result['columns'] + rows = cnx.get_rows() + else: + # do something useful with INSERT result + + Returns a generator. + """ + packet = bytearray() + if not isinstance(statements, bytearray): + if isinstance(statements, str): + statements = statements.encode("utf8") + statements = bytearray(statements) + + if self._client_flags & ClientFlag.CLIENT_QUERY_ATTRIBUTES: + # int parameter_count Number of parameters + packet.extend(lc_int(0)) + # int parameter_set_count Number of parameter sets. + # Currently always 1 + packet.extend(lc_int(1)) + + packet.extend(statements) + query = bytes(packet) + # Handle the first query result + yield self._handle_result(self._send_cmd(ServerCmd.QUERY, query)) + + # Handle next results, if any + while self._have_next_result: + self.handle_unread_result() + yield self._handle_result(self._socket.recv()) + + def cmd_refresh(self, options: int) -> OkPacketType: + """Send the Refresh command to the MySQL server + + This method sends the Refresh command to the MySQL server. The options + argument should be a bitwise value using constants.RefreshOption. + Usage example: + RefreshOption = mysql.connector.RefreshOption + refresh = RefreshOption.LOG | RefreshOption.THREADS + cnx.cmd_refresh(refresh) + + The result is a dictionary with the OK packet information. + + Returns a dict() + """ + return self._handle_ok(self._send_cmd(ServerCmd.REFRESH, int4store(options))) + + def cmd_quit(self) -> bytes: + """Close the current connection with the server + + This method sends the QUIT command to the MySQL server, closing the + current connection. Since the no response can be returned to the + client, cmd_quit() will return the packet it send. + + Returns a str() + """ + self.handle_unread_result() + + packet = self._protocol.make_command(ServerCmd.QUIT) + self._socket.send(packet, 0, 0) + return bytes(packet) + + def cmd_shutdown(self, shutdown_type: Optional[int] = None) -> EofPacketType: + """Shut down the MySQL Server + + This method sends the SHUTDOWN command to the MySQL server and is only + possible if the current user has SUPER privileges. The result is a + dictionary containing the OK packet information. + + Note: Most applications and scripts do not the SUPER privilege. + + Returns a dict() + """ + if shutdown_type: + if not ShutdownType.get_info(shutdown_type): + raise InterfaceError("Invalid shutdown type") + atype = shutdown_type + else: + atype = ShutdownType.SHUTDOWN_DEFAULT + return self._handle_eof(self._send_cmd(ServerCmd.SHUTDOWN, int4store(atype))) + + def cmd_statistics(self) -> StatsPacketType: + """Send the statistics command to the MySQL Server + + This method sends the STATISTICS command to the MySQL server. The + result is a dictionary with various statistical information. + + Returns a dict() + """ + self.handle_unread_result() + + packet = self._protocol.make_command(ServerCmd.STATISTICS) + self._socket.send(packet, 0, 0) + return self._protocol.parse_statistics(self._socket.recv()) + + def cmd_process_kill(self, mysql_pid: int) -> OkPacketType: + """Kill a MySQL process + + This method send the PROCESS_KILL command to the server along with + the process ID. The result is a dictionary with the OK packet + information. + + Returns a dict() + """ + return self._handle_ok( + self._send_cmd(ServerCmd.PROCESS_KILL, int4store(mysql_pid)) + ) + + def cmd_debug(self) -> EofPacketType: + """Send the DEBUG command + + This method sends the DEBUG command to the MySQL server, which + requires the MySQL user to have SUPER privilege. The output will go + to the MySQL server error log and the result of this method is a + dictionary with EOF packet information. + + Returns a dict() + """ + return self._handle_eof(self._send_cmd(ServerCmd.DEBUG)) + + def cmd_ping(self) -> OkPacketType: + """Send the PING command + + This method sends the PING command to the MySQL server. It is used to + check if the the connection is still valid. The result of this + method is dictionary with OK packet information. + + Returns a dict() + """ + return self._handle_ok(self._send_cmd(ServerCmd.PING)) + + def cmd_change_user( + self, + username: str = "", + password: str = "", + database: str = "", + charset: int = 45, + password1: str = "", + password2: str = "", + password3: str = "", + oci_config_file: str = "", + oci_config_profile: str = "", + ) -> Optional[OkPacketType]: + """Change the current logged in user + + This method allows to change the current logged in user information. + The result is a dictionary with OK packet information. + + Returns a dict() + """ + if not isinstance(charset, int): + raise ValueError("charset must be an integer") + if charset < 0: + raise ValueError("charset should be either zero or a postive integer") + + self._mfa_nfactor = 1 + self._user = username + self._password = password + self._password1 = password1 + self._password2 = password2 + self._password3 = password3 + + if self._password1 and password != self._password1: + password = self._password1 + + self.handle_unread_result() + + if self._compress: + raise NotSupportedError("Change user is not supported with compression") + packet = self._protocol.make_change_user( + handshake=self._handshake, + username=username, + password=password, + database=database, + charset=charset, + client_flags=self._client_flags, + ssl_enabled=self._ssl_active, + auth_plugin=self._auth_plugin, + conn_attrs=self._conn_attrs, + ) + self._socket.send(packet, 0, 0) + + if oci_config_file: + self._oci_config_file = oci_config_file + + self._oci_config_profile = oci_config_profile + + ok_packet = self._auth_switch_request(username, password) + + if not (self._client_flags & ClientFlag.CONNECT_WITH_DB) and database: + self.cmd_init_db(database) + + self._charset_id = charset + self._post_connection() + + return ok_packet + + @property + def database(self) -> str: + """Get the current database""" + return self.info_query("SELECT DATABASE()")[0] # type: ignore[return-value] + + @database.setter + def database(self, value: str) -> None: + """Set the current database""" + self.cmd_init_db(value) + + def is_connected(self) -> bool: + """Reports whether the connection to MySQL Server is available + + This method checks whether the connection to MySQL is available. + It is similar to ping(), but unlike the ping()-method, either True + or False is returned and no exception is raised. + + Returns True or False. + """ + try: + self.cmd_ping() + except Error: + return False # This method does not raise + return True + + def set_allow_local_infile_in_path(self, path: str) -> None: + """Set the path that user can upload files. + + Args: + path (str): Path that user can upload files. + """ + self._allow_local_infile_in_path = path + + def reset_session( + self, + user_variables: Optional[Dict[str, Any]] = None, + session_variables: Optional[Dict[str, Any]] = None, + ) -> None: + """Clears the current active session + + This method resets the session state, if the MySQL server is 5.7.3 + or later active session will be reset without re-authenticating. + For other server versions session will be reset by re-authenticating. + + It is possible to provide a sequence of variables and their values to + be set after clearing the session. This is possible for both user + defined variables and session variables. + This method takes two arguments user_variables and session_variables + which are dictionaries. + + Raises OperationalError if not connected, InternalError if there are + unread results and InterfaceError on errors. + """ + if not self.is_connected(): + raise OperationalError("MySQL Connection not available.") + + if not self.cmd_reset_connection(): + try: + self.cmd_change_user( + self._user, + self._password, + self._database, + self._charset_id, + self._password1, + self._password2, + self._password3, + self._oci_config_file, + self._oci_config_profile, + ) + except ProgrammingError: + self.reconnect() + + cur = self.cursor() + if user_variables: + for key, value in user_variables.items(): + cur.execute(f"SET @`{key}` = %s", (value,)) + if session_variables: + for key, value in session_variables.items(): + cur.execute(f"SET SESSION `{key}` = %s", (value,)) + + def ping(self, reconnect: bool = False, attempts: int = 1, delay: int = 0) -> None: + """Check availability of the MySQL server + + When reconnect is set to True, one or more attempts are made to try + to reconnect to the MySQL server using the reconnect()-method. + + delay is the number of seconds to wait between each retry. + + When the connection is not available, an InterfaceError is raised. Use + the is_connected()-method if you just want to check the connection + without raising an error. + + Raises InterfaceError on errors. + """ + try: + self.cmd_ping() + except Error as err: + if reconnect: + self.reconnect(attempts=attempts, delay=delay) + else: + raise InterfaceError("Connection to MySQL is not available") from err + + @property + def connection_id(self) -> Optional[int]: + """MySQL connection ID""" + if self._handshake: + return self._handshake.get("server_threadid") # type: ignore[return-value] + return None + + def cursor( + self, + buffered: Optional[bool] = None, + raw: Optional[bool] = None, + prepared: Optional[bool] = None, + cursor_class: Optional[Type[MySQLCursor]] = None, + dictionary: Optional[bool] = None, + named_tuple: Optional[bool] = None, + ) -> MySQLCursor: + """Instantiates and returns a cursor + + By default, MySQLCursor is returned. Depending on the options + while connecting, a buffered and/or raw cursor is instantiated + instead. Also depending upon the cursor options, rows can be + returned as dictionary or named tuple. + + Dictionary and namedtuple based cursors are available with buffered + output but not raw. + + It is possible to also give a custom cursor through the + cursor_class parameter, but it needs to be a subclass of + mysql.connector.cursor.CursorBase. + + Raises ProgrammingError when cursor_class is not a subclass of + CursorBase. Raises ValueError when cursor is not available. + + Returns a cursor-object + """ + self.handle_unread_result() + + if not self.is_connected(): + raise OperationalError("MySQL Connection not available") + if cursor_class is not None: + if not issubclass(cursor_class, CursorBase): + raise ProgrammingError( + "Cursor class needs be to subclass of cursor.CursorBase" + ) + return (cursor_class)(self) + + buffered = buffered if buffered is not None else self._buffered + raw = raw if raw is not None else self._raw + + cursor_type = 0 + if buffered is True: + cursor_type |= 1 + if raw is True: + cursor_type |= 2 + if dictionary is True: + cursor_type |= 4 + if named_tuple is True: + cursor_type |= 8 + if prepared is True: + cursor_type |= 16 + + types = { + 0: MySQLCursor, # 0 + 1: MySQLCursorBuffered, + 2: MySQLCursorRaw, + 3: MySQLCursorBufferedRaw, + 4: MySQLCursorDict, + 5: MySQLCursorBufferedDict, + 8: MySQLCursorNamedTuple, + 9: MySQLCursorBufferedNamedTuple, + 16: MySQLCursorPrepared, + 18: MySQLCursorPreparedRaw, + 20: MySQLCursorPreparedDict, + 24: MySQLCursorPreparedNamedTuple, + } + try: + return (types[cursor_type])(self) + except KeyError: + args = ("buffered", "raw", "dictionary", "named_tuple", "prepared") + raise ValueError( + "Cursor not available with given criteria: " + + ", ".join([args[i] for i in range(5) if cursor_type & (1 << i) != 0]) + ) from None + + def commit(self) -> None: + """Commit current transaction""" + self._execute_query("COMMIT") + + def rollback(self) -> None: + """Rollback current transaction""" + if self.unread_result: + self.get_rows() + + self._execute_query("ROLLBACK") + + def _execute_query(self, query: StrOrBytes) -> None: + """Execute a query + + This method simply calls cmd_query() after checking for unread + result. If there are still unread result, an InterfaceError + is raised. Otherwise whatever cmd_query() returns is returned. + + Returns a dict() + """ + self.handle_unread_result() + self.cmd_query(query) + + def info_query(self, query: StrOrBytes) -> Optional[RowType]: + """Send a query which only returns 1 row""" + cursor = self.cursor(buffered=True) + cursor.execute(query) + return cursor.fetchone() + + def _handle_binary_ok(self, packet: bytes) -> Dict[str, int]: + """Handle a MySQL Binary Protocol OK packet + + This method handles a MySQL Binary Protocol OK packet. When the + packet is found to be an Error packet, an error will be raised. If + the packet is neither an OK or an Error packet, InterfaceError + will be raised. + + Returns a dict() + """ + if packet[4] == 0: + return self._protocol.parse_binary_prepare_ok(packet) + if packet[4] == 255: + raise get_exception(packet) + raise InterfaceError("Expected Binary OK packet") + + def _handle_binary_result( + self, packet: bytes + ) -> Union[OkPacketType, Tuple[int, List[DescriptionType], EofPacketType]]: + """Handle a MySQL Result + + This method handles a MySQL result, for example, after sending the + query command. OK and EOF packets will be handled and returned. If + the packet is an Error packet, an Error exception will be raised. + + The tuple returned by this method consist of: + - the number of columns in the result, + - a list of tuples with information about the columns, + - the EOF packet information as a dictionary. + + Returns tuple() or dict() + """ + if not packet or len(packet) < 4: + raise InterfaceError("Empty response") + if packet[4] == 0: + return self._handle_ok(packet) + if packet[4] == 254: + return self._handle_eof(packet) + if packet[4] == 255: + raise get_exception(packet) + + # We have a binary result set + column_count = self._protocol.parse_column_count(packet) + if not column_count or not isinstance(column_count, int): + raise InterfaceError("Illegal result set.") + + columns: List[DescriptionType] = [None] * column_count + for i in range(0, column_count): + columns[i] = self._protocol.parse_column( + self._socket.recv(), self.python_charset + ) + + eof = self._handle_eof(self._socket.recv()) + return (column_count, columns, eof) + + def cmd_stmt_fetch(self, statement_id: int, rows: int = 1) -> None: + """Fetch a MySQL statement Result Set + + This method will send the FETCH command to MySQL together with the + given statement id and the number of rows to fetch. + """ + packet = self._protocol.make_stmt_fetch(statement_id, rows) + self.unread_result = False + self._send_cmd(ServerCmd.STMT_FETCH, packet, expect_response=False) + self.unread_result = True + + def cmd_stmt_prepare( + self, statement: bytes + ) -> Mapping[str, Union[int, List[DescriptionType]]]: + """Prepare a MySQL statement + + This method will send the PREPARE command to MySQL together with the + given statement. + + Returns a dict() + """ + packet = self._send_cmd(ServerCmd.STMT_PREPARE, statement) + result = self._handle_binary_ok(packet) + + result["columns"] = [] + result["parameters"] = [] + if result["num_params"] > 0: + for _ in range(0, result["num_params"]): + result["parameters"].append( + self._protocol.parse_column( + self._socket.recv(), self.python_charset + ) + ) + self._handle_eof(self._socket.recv()) + if result["num_columns"] > 0: + for _ in range(0, result["num_columns"]): + result["columns"].append( + self._protocol.parse_column( + self._socket.recv(), self.python_charset + ) + ) + self._handle_eof(self._socket.recv()) + + return result + + @with_context_propagation + def cmd_stmt_execute( + self, + statement_id: int, + data: Sequence[SupportedMysqlBinaryProtocolTypes] = (), + parameters: Sequence[Any] = (), + flags: int = 0, + ) -> Union[OkPacketType, Tuple[int, List[DescriptionType], EofPacketType]]: + """Execute a prepared MySQL statement""" + parameters = list(parameters) + long_data_used = {} + + if data: + for param_id, _ in enumerate(parameters): + if isinstance(data[param_id], IOBase): + binary = True + try: + binary = "b" not in data[param_id].mode # type: ignore[union-attr] + except AttributeError: + pass + self.cmd_stmt_send_long_data(statement_id, param_id, data[param_id]) + long_data_used[param_id] = (binary,) + if not self._query_attrs_supported and self._query_attrs: + warnings.warn( + "This version of the server does not support Query Attributes", + category=Warning, + ) + if self._client_flags & ClientFlag.CLIENT_QUERY_ATTRIBUTES: + execute_packet = self._protocol.make_stmt_execute( + statement_id, + data, + tuple(parameters), + flags, + long_data_used, + self.charset, + self.query_attrs, + self._converter_str_fallback, + ) + else: + execute_packet = self._protocol.make_stmt_execute( + statement_id, + data, + tuple(parameters), + flags, + long_data_used, + self.charset, + converter_str_fallback=self._converter_str_fallback, + ) + packet = self._send_cmd(ServerCmd.STMT_EXECUTE, packet=execute_packet) + result = self._handle_binary_result(packet) + return result + + def cmd_stmt_close(self, statement_id: int) -> None: + """Deallocate a prepared MySQL statement + + This method deallocates the prepared statement using the + statement_id. Note that the MySQL server does not return + anything. + """ + self._send_cmd( + ServerCmd.STMT_CLOSE, + int4store(statement_id), + expect_response=False, + ) + + def cmd_stmt_send_long_data( + self, statement_id: int, param_id: int, data: BinaryIO + ) -> int: + """Send data for a column + + This methods send data for a column (for example BLOB) for statement + identified by statement_id. The param_id indicate which parameter + the data belongs too. + The data argument should be a file-like object. + + Since MySQL does not send anything back, no error is raised. When + the MySQL server is not reachable, an OperationalError is raised. + + cmd_stmt_send_long_data should be called before cmd_stmt_execute. + + The total bytes send is returned. + + Returns int. + """ + chunk_size = 131072 # 128 KB + total_sent = 0 + try: + buf = data.read(chunk_size) + while buf: + packet = self._protocol.prepare_stmt_send_long_data( + statement_id, param_id, buf + ) + self._send_cmd( + ServerCmd.STMT_SEND_LONG_DATA, + packet=packet, + expect_response=False, + ) + total_sent += len(buf) + buf = data.read(chunk_size) + except AttributeError as err: + raise OperationalError("MySQL Connection not available") from err + + return total_sent + + def cmd_stmt_reset(self, statement_id: int) -> None: + """Reset data for prepared statement sent as long data + + The result is a dictionary with OK packet information. + + Returns a dict() + """ + self._handle_ok(self._send_cmd(ServerCmd.STMT_RESET, int4store(statement_id))) + + def cmd_reset_connection(self) -> bool: + """Resets the session state without re-authenticating + + Reset command only works on MySQL server 5.7.3 or later. + The result is True for a successful reset otherwise False. + + Returns bool + """ + try: + self._handle_ok(self._send_cmd(ServerCmd.RESET_CONNECTION)) + self._post_connection() + return True + except (NotSupportedError, OperationalError): + return False + + def handle_unread_result(self) -> None: + """Check whether there is an unread result""" + if self.can_consume_results: + self.consume_results() + elif self.unread_result: + raise InternalError("Unread result found") diff --git a/mysql/connector/connection_cext.py b/mysql/connector/connection_cext.py new file mode 100644 index 0000000..34c618e --- /dev/null +++ b/mysql/connector/connection_cext.py @@ -0,0 +1,1004 @@ +# Copyright (c) 2014, 2023, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# mypy: disable-error-code="arg-type,index" + +"""Connection class using the C Extension.""" + +import os +import platform +import socket +import sys + +from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union + +from . import version +from .abstracts import MySQLConnectionAbstract +from .constants import CharacterSet, ClientFlag, FieldFlag, ServerFlag, ShutdownType +from .conversion import MySQLConverter +from .errors import ( + InterfaceError, + InternalError, + OperationalError, + ProgrammingError, + get_mysql_exception, +) +from .protocol import MySQLProtocol +from .types import ( + CextEofPacketType, + CextResultType, + DescriptionType, + ParamsSequenceOrDictType, + RowType, + StatsPacketType, + StrOrBytes, +) + +HAVE_CMYSQL = False + +try: + import _mysql_connector + + from _mysql_connector import MySQLInterfaceError, MySQLPrepStmt + + from .cursor_cext import ( + CMySQLCursor, + CMySQLCursorBuffered, + CMySQLCursorBufferedDict, + CMySQLCursorBufferedNamedTuple, + CMySQLCursorBufferedRaw, + CMySQLCursorDict, + CMySQLCursorNamedTuple, + CMySQLCursorPrepared, + CMySQLCursorPreparedDict, + CMySQLCursorPreparedNamedTuple, + CMySQLCursorPreparedRaw, + CMySQLCursorRaw, + ) + + HAVE_CMYSQL = True +except ImportError as exc: + raise ImportError( + f"MySQL Connector/Python C Extension not available ({exc})" + ) from exc + +from .opentelemetry.constants import OTEL_ENABLED +from .opentelemetry.context_propagation import with_context_propagation + +if OTEL_ENABLED: + from .opentelemetry.instrumentation import end_span, record_exception_event + + +class CMySQLConnection(MySQLConnectionAbstract): + """Class initiating a MySQL Connection using Connector/C.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialization""" + if not HAVE_CMYSQL: + raise RuntimeError("MySQL Connector/Python C Extension not available") + self._cmysql: Optional[ + _mysql_connector.MySQL # pylint: disable=c-extension-no-member + ] = None + self._columns: List[DescriptionType] = [] + self._plugin_dir: str = os.path.join( + os.path.dirname(os.path.abspath(_mysql_connector.__file__)), + "mysql", + "vendor", + "plugin", + ) + if platform.system() == "Linux": + # Use the authentication plugins from system if they aren't bundled + if not os.path.exists(self._plugin_dir): + self._plugin_dir = ( + "/usr/lib64/mysql/plugin" + if os.path.exists("/usr/lib64/mysql/plugin") + else "/usr/lib/mysql/plugin" + ) + + self.converter: Optional[MySQLConverter] = None + super().__init__() + + if kwargs: + try: + self.connect(**kwargs) + except Exception: + self.close() + raise + + def _add_default_conn_attrs(self) -> None: + """Add default connection attributes""" + license_chunks = version.LICENSE.split(" ") + if license_chunks[0] == "GPLv2": + client_license = "GPL-2.0" + else: + client_license = "Commercial" + + self._conn_attrs.update( + { + "_connector_name": "mysql-connector-python", + "_connector_license": client_license, + "_connector_version": ".".join([str(x) for x in version.VERSION[0:3]]), + "_source_host": socket.gethostname(), + } + ) + + def _do_handshake(self) -> None: + """Gather information of the MySQL server before authentication""" + self._handshake = { + "protocol": self._cmysql.get_proto_info(), + "server_version_original": self._cmysql.get_server_info(), + "server_threadid": self._cmysql.thread_id(), + "charset": None, + "server_status": None, + "auth_plugin": None, + "auth_data": None, + "capabilities": self._cmysql.st_server_capabilities(), + } + + self._server_version = self._check_server_version( + self._handshake["server_version_original"] + ) + CharacterSet.set_mysql_version(self._server_version) + + @property + def _server_status(self) -> int: + """Returns the server status attribute of MYSQL structure""" + return self._cmysql.st_server_status() + + def set_allow_local_infile_in_path(self, path: str) -> None: + """set local_infile_in_path + + Set allow_local_infile_in_path. + """ + + if self._cmysql: + self._cmysql.set_load_data_local_infile_option(path) + + def set_unicode(self, value: bool = True) -> None: + """Toggle unicode mode + + Set whether we return string fields as unicode or not. + Default is True. + """ + self._use_unicode = value + if self._cmysql: + self._cmysql.use_unicode(value) + if self.converter: + self.converter.set_unicode(value) + + @property + def autocommit(self) -> bool: + """Get whether autocommit is on or off""" + value = self.info_query("SELECT @@session.autocommit")[0] + return value == 1 + + @autocommit.setter + def autocommit(self, value: bool) -> None: + """Toggle autocommit""" + try: + self._cmysql.autocommit(value) + self._autocommit = value + except MySQLInterfaceError as err: + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + + @property + def database(self) -> str: + """Get the current database""" + return self.info_query("SELECT DATABASE()")[0] # type: ignore[return-value] + + @database.setter + def database(self, value: str) -> None: + """Set the current database""" + try: + self._cmysql.select_db(value) + except MySQLInterfaceError as err: + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + + @property + def in_transaction(self) -> int: + """MySQL session has started a transaction""" + return self._server_status & ServerFlag.STATUS_IN_TRANS + + def _open_connection(self) -> None: + charset_name = CharacterSet.get_info(self._charset_id)[0] + # pylint: disable=c-extension-no-member + self._cmysql = _mysql_connector.MySQL( + buffered=self._buffered, + raw=self._raw, + charset_name=charset_name, + connection_timeout=(self._connection_timeout or 0), + use_unicode=self._use_unicode, + auth_plugin=self._auth_plugin, + plugin_dir=self._plugin_dir, + ) + # pylint: enable=c-extension-no-member + if not self.isset_client_flag(ClientFlag.CONNECT_ARGS): + self._conn_attrs = {} + cnx_kwargs = { + "host": self._host, + "user": self._user, + "password": self._password, + "password1": self._password1, + "password2": self._password2, + "password3": self._password3, + "database": self._database, + "port": self._port, + "client_flags": self._client_flags, + "unix_socket": self._unix_socket, + "compress": self._compress, + "ssl_disabled": True, + "conn_attrs": self._conn_attrs, + "local_infile": self._allow_local_infile, + "load_data_local_dir": self._allow_local_infile_in_path, + "oci_config_file": self._oci_config_file, + "oci_config_profile": self._oci_config_profile, + "fido_callback": self._fido_callback, + } + + tls_versions = self._ssl.get("tls_versions") + if tls_versions is not None: + tls_versions.sort(reverse=True) # type: ignore[union-attr] + tls_versions = ",".join(tls_versions) + if self._ssl.get("tls_ciphersuites") is not None: + ssl_ciphersuites = self._ssl.get("tls_ciphersuites")[0] + tls_ciphersuites = self._ssl.get("tls_ciphersuites")[1] + else: + ssl_ciphersuites = None + tls_ciphersuites = None + if ( + tls_versions is not None + and "TLSv1.3" in tls_versions + and not tls_ciphersuites + ): + tls_ciphersuites = "TLS_AES_256_GCM_SHA384" + if not self._ssl_disabled: + cnx_kwargs.update( + { + "ssl_ca": self._ssl.get("ca"), + "ssl_cert": self._ssl.get("cert"), + "ssl_key": self._ssl.get("key"), + "ssl_cipher_suites": ssl_ciphersuites, + "tls_versions": tls_versions, + "tls_cipher_suites": tls_ciphersuites, + "ssl_verify_cert": self._ssl.get("verify_cert") or False, + "ssl_verify_identity": self._ssl.get("verify_identity") or False, + "ssl_disabled": self._ssl_disabled, + } + ) + + if os.name == "nt" and self._auth_plugin_class == "MySQLKerberosAuthPlugin": + cnx_kwargs["use_kerberos_gssapi"] = True + + try: + self._cmysql.connect(**cnx_kwargs) + self._cmysql.converter_str_fallback = self._converter_str_fallback + if self.converter: + self.converter.str_fallback = self._converter_str_fallback + except MySQLInterfaceError as err: + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + + self._do_handshake() + + def close(self) -> None: + """Disconnect from the MySQL server""" + if self._span and self._span.is_recording(): + record_exception_event(self._span, sys.exc_info()[1]) + + if not self._cmysql: + return + + try: + self.free_result() + self._cmysql.close() + except MySQLInterfaceError as err: + if OTEL_ENABLED: + record_exception_event(self._span, err) + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + finally: + if OTEL_ENABLED: + end_span(self._span) + + disconnect = close + + def is_closed(self) -> bool: + """Return True if the connection to MySQL Server is closed.""" + return not self._cmysql.connected() + + def is_connected(self) -> bool: + """Reports whether the connection to MySQL Server is available""" + if self._cmysql: + self.handle_unread_result() + return self._cmysql.ping() + + return False + + def ping(self, reconnect: bool = False, attempts: int = 1, delay: int = 0) -> None: + """Check availability of the MySQL server + + When reconnect is set to True, one or more attempts are made to try + to reconnect to the MySQL server using the reconnect()-method. + + delay is the number of seconds to wait between each retry. + + When the connection is not available, an InterfaceError is raised. Use + the is_connected()-method if you just want to check the connection + without raising an error. + + Raises InterfaceError on errors. + """ + self.handle_unread_result() + + try: + connected = self._cmysql.ping() + except AttributeError: + pass # Raise or reconnect later + else: + if connected: + return + + if reconnect: + self.reconnect(attempts=attempts, delay=delay) + else: + raise InterfaceError("Connection to MySQL is not available") + + def set_character_set_name(self, charset: str) -> None: + """Sets the default character set name for current connection.""" + self._cmysql.set_character_set(charset) + + def info_query(self, query: StrOrBytes) -> Optional[RowType]: + """Send a query which only returns 1 row""" + first_row = () + try: + self._cmysql.query(query) + if self._cmysql.have_result_set: + first_row = self._cmysql.fetch_row() + if self._cmysql.fetch_row(): + self._cmysql.free_result() + raise InterfaceError("Query should not return more than 1 row") + self._cmysql.free_result() + except MySQLInterfaceError as err: + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + + return first_row + + @property + def connection_id(self) -> Optional[int]: + """MySQL connection ID""" + try: + return self._cmysql.thread_id() + except MySQLInterfaceError: + pass # Just return None + + return None + + def get_rows( + self, + count: Optional[int] = None, + binary: bool = False, + columns: Optional[List[DescriptionType]] = None, + raw: Optional[bool] = None, + prep_stmt: Optional[MySQLPrepStmt] = None, + ) -> Tuple[List[RowType], Optional[CextEofPacketType]]: + """Get all or a subset of rows returned by the MySQL server""" + unread_result = prep_stmt.have_result_set if prep_stmt else self.unread_result + if not (self._cmysql and unread_result): + raise InternalError("No result set available") + + if raw is None: + raw = self._raw + + rows: List[Tuple[Any, ...]] = [] + if count is not None and count <= 0: + raise AttributeError("count should be 1 or higher, or None") + + counter = 0 + try: + fetch_row = prep_stmt.fetch_row if prep_stmt else self._cmysql.fetch_row + if self.converter: + # When using a converter class, the C extension should not + # convert the values. This can be accomplished by setting + # the raw option to True. + self._cmysql.raw(True) + row = fetch_row() + while row: + if not self._raw and self.converter: + row = list(row) + for i, _ in enumerate(row): + if not raw: + row[i] = self.converter.to_python(self._columns[i], row[i]) + row = tuple(row) + rows.append(row) + counter += 1 + if count and counter == count: + break + row = fetch_row() + if not row: + _eof: Optional[CextEofPacketType] = self.fetch_eof_columns(prep_stmt)[ + "eof" + ] # type: ignore[assignment] + if prep_stmt: + prep_stmt.free_result() + self._unread_result = False + else: + self.free_result() + else: + _eof = None + except MySQLInterfaceError as err: + if prep_stmt: + prep_stmt.free_result() + raise InterfaceError(str(err)) from err + self.free_result() + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + + return rows, _eof + + def get_row( + self, + binary: bool = False, + columns: Optional[List[DescriptionType]] = None, + raw: Optional[bool] = None, + prep_stmt: Optional[MySQLPrepStmt] = None, + ) -> Tuple[Optional[RowType], CextEofPacketType]: + """Get the next rows returned by the MySQL server""" + try: + rows, eof = self.get_rows( + count=1, + binary=binary, + columns=columns, + raw=raw, + prep_stmt=prep_stmt, + ) + if rows: + return (rows[0], eof) + return (None, eof) + except IndexError: + # No row available + return (None, None) + + def next_result(self) -> Optional[bool]: + """Reads the next result""" + if self._cmysql: + self._cmysql.consume_result() + return self._cmysql.next_result() + return None + + def free_result(self) -> None: + """Frees the result""" + if self._cmysql: + self._cmysql.free_result() + + def commit(self) -> None: + """Commit current transaction""" + if self._cmysql: + self.handle_unread_result() + self._cmysql.commit() + + def rollback(self) -> None: + """Rollback current transaction""" + if self._cmysql: + self._cmysql.consume_result() + self._cmysql.rollback() + + def cmd_init_db(self, database: str) -> None: + """Change the current database""" + try: + self._cmysql.select_db(database) + except MySQLInterfaceError as err: + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + + def fetch_eof_columns( + self, prep_stmt: Optional[MySQLPrepStmt] = None + ) -> CextResultType: + """Fetch EOF and column information""" + have_result_set = ( + prep_stmt.have_result_set if prep_stmt else self._cmysql.have_result_set + ) + if not have_result_set: + raise InterfaceError("No result set") + + fields = prep_stmt.fetch_fields() if prep_stmt else self._cmysql.fetch_fields() + self._columns = [] + for col in fields: + self._columns.append( + ( + col[4], + int(col[8]), + None, + None, + None, + None, + ~int(col[9]) & FieldFlag.NOT_NULL, + int(col[9]), + int(col[6]), + ) + ) + + return { + "eof": { + "status_flag": self._server_status, + "warning_count": self._cmysql.st_warning_count(), + }, + "columns": self._columns, + } + + def fetch_eof_status(self) -> Optional[CextEofPacketType]: + """Fetch EOF and status information""" + if self._cmysql: + return { + "warning_count": self._cmysql.st_warning_count(), + "field_count": self._cmysql.st_field_count(), + "insert_id": self._cmysql.insert_id(), + "affected_rows": self._cmysql.affected_rows(), + "server_status": self._server_status, + } + + return None + + def cmd_stmt_prepare(self, statement: bytes) -> MySQLPrepStmt: + """Prepares the SQL statement""" + if not self._cmysql: + raise OperationalError("MySQL Connection not available") + + try: + stmt = self._cmysql.stmt_prepare(statement) + stmt.converter_str_fallback = self._converter_str_fallback + return stmt + except MySQLInterfaceError as err: + raise InterfaceError(str(err)) from err + + def cmd_stmt_execute( + self, statement_id: MySQLPrepStmt, *args: Any + ) -> Optional[Union[CextEofPacketType, CextResultType]]: + """Executes the prepared statement""" + try: + statement_id.stmt_execute(*args) + except MySQLInterfaceError as err: + raise InterfaceError(str(err)) from err + + self._columns = [] + if not statement_id.have_result_set: + # No result + self._unread_result = False + return self.fetch_eof_status() + + self._unread_result = True + return self.fetch_eof_columns(statement_id) + + def cmd_stmt_close(self, statement_id: MySQLPrepStmt) -> None: + """Closes the prepared statement""" + if self._unread_result: + raise InternalError("Unread result found") + statement_id.stmt_close() + + def cmd_stmt_reset(self, statement_id: MySQLPrepStmt) -> None: + """Resets the prepared statement""" + if self._unread_result: + raise InternalError("Unread result found") + statement_id.stmt_reset() + + @with_context_propagation + def cmd_query( + self, + query: StrOrBytes, + raw: Optional[bool] = None, + buffered: bool = False, + raw_as_string: bool = False, + ) -> Optional[Union[CextEofPacketType, CextResultType]]: + """Send a query to the MySQL server""" + self.handle_unread_result() + if raw is None: + raw = self._raw + try: + if not isinstance(query, bytes): + query = query.encode("utf-8") + self._cmysql.query( + query, + raw=raw, + buffered=buffered, + raw_as_string=raw_as_string, + query_attrs=self.query_attrs, + ) + except MySQLInterfaceError as err: + raise get_mysql_exception( + err.errno, msg=err.msg, sqlstate=err.sqlstate + ) from err + except AttributeError as err: + addr = ( + self._unix_socket if self._unix_socket else f"{self._host}:{self._port}" + ) + raise OperationalError( + errno=2055, values=(addr, "Connection not available.") + ) from err + + self._columns = [] + if not self._cmysql.have_result_set: + # No result + return self.fetch_eof_status() + + return self.fetch_eof_columns() + + _execute_query = cmd_query + + def cursor( + self, + buffered: Optional[bool] = None, + raw: Optional[bool] = None, + prepared: Optional[bool] = None, + cursor_class: Optional[Type[CMySQLCursor]] = None, + dictionary: Optional[bool] = None, + named_tuple: Optional[bool] = None, + ) -> CMySQLCursor: + """Instantiates and returns a cursor using C Extension + + By default, CMySQLCursor is returned. Depending on the options + while connecting, a buffered and/or raw cursor is instantiated + instead. Also depending upon the cursor options, rows can be + returned as dictionary or named tuple. + + Dictionary and namedtuple based cursors are available with buffered + output but not raw. + + It is possible to also give a custom cursor through the + cursor_class parameter, but it needs to be a subclass of + mysql.connector.cursor_cext.CMySQLCursor. + + Raises ProgrammingError when cursor_class is not a subclass of + CursorBase. Raises ValueError when cursor is not available. + + Returns instance of CMySQLCursor or subclass. + + :param buffered: Return a buffering cursor + :param raw: Return a raw cursor + :param prepared: Return a cursor which uses prepared statements + :param cursor_class: Use a custom cursor class + :param dictionary: Rows are returned as dictionary + :param named_tuple: Rows are returned as named tuple + :return: Subclass of CMySQLCursor + :rtype: CMySQLCursor or subclass + """ + self.handle_unread_result(prepared) + if not self.is_connected(): + raise OperationalError("MySQL Connection not available.") + if cursor_class is not None: + if not issubclass(cursor_class, CMySQLCursor): + raise ProgrammingError( + "Cursor class needs be to subclass of cursor_cext.CMySQLCursor" + ) + return (cursor_class)(self) + + buffered = buffered or self._buffered + raw = raw or self._raw + + cursor_type = 0 + if buffered is True: + cursor_type |= 1 + if raw is True: + cursor_type |= 2 + if dictionary is True: + cursor_type |= 4 + if named_tuple is True: + cursor_type |= 8 + if prepared is True: + cursor_type |= 16 + + types = { + 0: CMySQLCursor, # 0 + 1: CMySQLCursorBuffered, + 2: CMySQLCursorRaw, + 3: CMySQLCursorBufferedRaw, + 4: CMySQLCursorDict, + 5: CMySQLCursorBufferedDict, + 8: CMySQLCursorNamedTuple, + 9: CMySQLCursorBufferedNamedTuple, + 16: CMySQLCursorPrepared, + 18: CMySQLCursorPreparedRaw, + 20: CMySQLCursorPreparedDict, + 24: CMySQLCursorPreparedNamedTuple, + } + try: + return (types[cursor_type])(self) + except KeyError: + args = ("buffered", "raw", "dictionary", "named_tuple", "prepared") + raise ValueError( + "Cursor not available with given criteria: " + + ", ".join([args[i] for i in range(5) if cursor_type & (1 << i) != 0]) + ) from None + + @property + def num_rows(self) -> int: + """Returns number of rows of current result set""" + if not self._cmysql.have_result_set: + raise InterfaceError("No result set") + + return self._cmysql.num_rows() + + @property + def warning_count(self) -> int: + """Returns number of warnings""" + if not self._cmysql: + return 0 + + return self._cmysql.warning_count() + + @property + def result_set_available(self) -> bool: + """Check if a result set is available""" + if not self._cmysql: + return False + + return self._cmysql.have_result_set + + @property # type: ignore[misc] + def unread_result(self) -> bool: + """Check if there are unread results or rows""" + return self.result_set_available + + @property + def more_results(self) -> bool: + """Check if there are more results""" + return self._cmysql.more_results() + + def prepare_for_mysql( + self, params: ParamsSequenceOrDictType + ) -> Union[Sequence[bytes], Dict[str, bytes],]: + """Prepare parameters for statements + + This method is use by cursors to prepared parameters found in the + list (or tuple) params. + + Returns dict. + """ + result: Union[List[Any], Dict[str, Any]] = [] + if isinstance(params, (list, tuple)): + if self.converter: + result = [ + self.converter.quote( + self.converter.escape( + self.converter.to_mysql(value), self._sql_mode + ) + ) + for value in params + ] + else: + result = self._cmysql.convert_to_mysql(*params) + elif isinstance(params, dict): + result = {} + if self.converter: + for key, value in params.items(): + result[key] = self.converter.quote( + self.converter.escape( + self.converter.to_mysql(value), self._sql_mode + ) + ) + else: + for key, value in params.items(): + result[key] = self._cmysql.convert_to_mysql(value)[0] + else: + raise ProgrammingError( + f"Could not process parameters: {type(params).__name__}({params})," + " it must be of type list, tuple or dict" + ) + + return result + + def consume_results(self) -> None: + """Consume the current result + + This method consume the result by reading (consuming) all rows. + """ + self._cmysql.consume_result() + + def cmd_change_user( + self, + username: str = "", + password: str = "", + database: str = "", + charset: int = 45, + password1: str = "", + password2: str = "", + password3: str = "", + oci_config_file: Optional[str] = None, + oci_config_profile: Optional[str] = None, + ) -> None: + """Change the current logged in user""" + try: + self._cmysql.change_user( + username, + password, + database, + password1, + password2, + password3, + oci_config_file, + oci_config_profile, + ) + + except MySQLInterfaceError as err: + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + + self._charset_id = charset + self._user = username # updating user accordingly + self._post_connection() + + def cmd_reset_connection(self) -> bool: + """Resets the session state without re-authenticating + + Reset command only works on MySQL server 5.7.3 or later. + The result is True for a successful reset otherwise False. + + Returns bool + """ + res = self._cmysql.reset_connection() + if res: + self._post_connection() + return res + + def cmd_refresh(self, options: int) -> Optional[CextEofPacketType]: + """Send the Refresh command to the MySQL server""" + try: + self.handle_unread_result() + self._cmysql.refresh(options) + except MySQLInterfaceError as err: + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + + return self.fetch_eof_status() + + def cmd_quit(self) -> None: + """Close the current connection with the server""" + self.close() + + def cmd_shutdown(self, shutdown_type: Optional[int] = None) -> None: + """Shut down the MySQL Server""" + if not self._cmysql: + raise OperationalError("MySQL Connection not available") + + if shutdown_type: + if not ShutdownType.get_info(shutdown_type): + raise InterfaceError("Invalid shutdown type") + level = shutdown_type + else: + level = ShutdownType.SHUTDOWN_DEFAULT + + try: + self._cmysql.shutdown(level) + except MySQLInterfaceError as err: + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + self.close() + + def cmd_statistics(self) -> StatsPacketType: + """Return statistics from the MySQL server""" + self.handle_unread_result() + + try: + stat = self._cmysql.stat() + return MySQLProtocol().parse_statistics(stat, with_header=False) + except (MySQLInterfaceError, InterfaceError) as err: + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + + def cmd_process_kill(self, mysql_pid: int) -> None: + """Kill a MySQL process""" + if not isinstance(mysql_pid, int): + raise ValueError("MySQL PID must be int") + self.info_query(f"KILL {mysql_pid}") + + def cmd_debug(self) -> Any: + """Send the DEBUG command""" + raise NotImplementedError + + def cmd_ping(self) -> Any: + """Send the PING command""" + raise NotImplementedError + + def cmd_query_iter(self, statements: Any) -> Any: + """Send one or more statements to the MySQL server""" + raise NotImplementedError + + def cmd_stmt_send_long_data( + self, statement_id: Any, param_id: Any, data: Any + ) -> Any: + """Send data for a column""" + raise NotImplementedError + + def handle_unread_result(self, prepared: bool = False) -> None: + """Check whether there is an unread result""" + unread_result = self._unread_result if prepared is True else self.unread_result + if self.can_consume_results: + self.consume_results() + elif unread_result: + raise InternalError("Unread result found") + + def reset_session( + self, + user_variables: Optional[Dict[str, Any]] = None, + session_variables: Optional[Dict[str, Any]] = None, + ) -> None: + """Clears the current active session + + This method resets the session state, if the MySQL server is 5.7.3 + or later active session will be reset without re-authenticating. + For other server versions session will be reset by re-authenticating. + + It is possible to provide a sequence of variables and their values to + be set after clearing the session. This is possible for both user + defined variables and session variables. + This method takes two arguments user_variables and session_variables + which are dictionaries. + + Raises OperationalError if not connected, InternalError if there are + unread results and InterfaceError on errors. + """ + if not self.is_connected(): + raise OperationalError("MySQL Connection not available.") + + if not self.cmd_reset_connection(): + try: + self.cmd_change_user( + self._user, + self._password, + self._database, + self._charset_id, + self._password1, + self._password2, + self._password3, + self._oci_config_file, + self._oci_config_profile, + ) + except ProgrammingError: + self.reconnect() + + if user_variables or session_variables: + cur = self.cursor() + if user_variables: + for key, value in user_variables.items(): + cur.execute(f"SET @`{key}` = %s", (value,)) + if session_variables: + for key, value in session_variables.items(): + cur.execute(f"SET SESSION `{key}` = %s", (value,)) + cur.close() diff --git a/mysql/connector/constants.py b/mysql/connector/constants.py new file mode 100644 index 0000000..f25da42 --- /dev/null +++ b/mysql/connector/constants.py @@ -0,0 +1,1148 @@ +# Copyright (c) 2009, 2023, Oracle and/or its affiliates. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Various MySQL constants and character sets.""" + +import warnings + +from abc import ABC, ABCMeta +from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, ValuesView + +from .charsets import MYSQL_CHARACTER_SETS, MYSQL_CHARACTER_SETS_57 +from .errors import ProgrammingError + +NET_BUFFER_LENGTH: int = 8192 +MAX_MYSQL_TABLE_COLUMNS: int = 4096 +# Flag used to send the Query Attributes with 0 (or more) parameters. +PARAMETER_COUNT_AVAILABLE: int = 8 + +DEFAULT_CONFIGURATION: Dict[str, Optional[Union[str, bool, int]]] = { + "database": None, + "user": "", + "password": "", + "password1": "", + "password2": "", + "password3": "", + "host": "127.0.0.1", + "port": 3306, + "unix_socket": None, + "use_unicode": True, + "charset": "utf8mb4", + "collation": None, + "converter_class": None, + "converter_str_fallback": False, + "autocommit": False, + "time_zone": None, + "sql_mode": None, + "get_warnings": False, + "raise_on_warnings": False, + "connection_timeout": None, + "client_flags": 0, + "compress": False, + "buffered": False, + "raw": False, + "ssl_ca": None, + "ssl_cert": None, + "ssl_key": None, + "ssl_verify_cert": False, + "ssl_verify_identity": False, + "ssl_cipher": None, + "tls_ciphersuites": None, + "ssl_disabled": False, + "tls_versions": None, + "passwd": None, + "db": None, + "connect_timeout": None, + "dsn": None, + "force_ipv6": False, + "auth_plugin": None, + "allow_local_infile": False, + "allow_local_infile_in_path": None, + "consume_results": False, + "conn_attrs": None, + "dns_srv": False, + "use_pure": False, + "krb_service_principal": None, + "oci_config_file": None, + "oci_config_profile": None, + "fido_callback": None, + "kerberos_auth_mode": None, + "init_command": None, +} + +CNX_POOL_ARGS: Tuple[str, str, str] = ("pool_name", "pool_size", "pool_reset_session") + +TLS_VERSIONS: List[str] = ["TLSv1.2", "TLSv1.3"] + +DEPRECATED_TLS_VERSIONS: List[str] = ["TLSv1", "TLSv1.1"] + + +def flag_is_set(flag: int, flags: int) -> bool: + """Checks if the flag is set + + Returns boolean""" + if (flags & flag) > 0: + return True + return False + + +def _obsolete_option(name: str, new_name: str, value: int) -> int: + """Raise a deprecation warning and advise a new option name. + + Args: + name (str): The name of the option. + new_name (str): The new option name. + value (int): The value of the option. + + Returns: + int: The value of the option. + """ + warnings.warn( + f"The option '{name}' has been deprecated, use '{new_name}' instead.", + category=DeprecationWarning, + ) + return value + + +class _Constants(ABC): + """Base class for constants.""" + + prefix: str = "" + desc: Dict[str, Tuple[int, str]] = {} + + @classmethod + def get_desc(cls, name: str) -> Optional[str]: + """Get description of given constant""" + try: + return cls.desc[name][1] + except (IndexError, KeyError): + return None + + @classmethod + def get_info(cls, setid: int) -> Union[Optional[str], Tuple[str, str]]: + """Get information about given constant""" + for name, info in cls.desc.items(): + if info[0] == setid: + return name + return None + + @classmethod + def get_full_info(cls) -> Union[str, Sequence[str]]: + """get full information about given constant""" + res: Union[str, List[str]] = [] + try: + res = [f"{k} : {v[1]}" for k, v in cls.desc.items()] + except (AttributeError, IndexError) as err: + res = f"No information found in constant class. {err}" + + return res + + +class _Flags(_Constants): + """Base class for classes describing flags""" + + @classmethod + def get_bit_info(cls, value: int) -> List[str]: + """Get the name of all bits set + + Returns a list of strings.""" + res = [] + for name, info in cls.desc.items(): + if value & info[0]: + res.append(name) + return res + + +class FieldType(_Constants): + """MySQL Field Types""" + + prefix: str = "FIELD_TYPE_" + DECIMAL: int = 0x00 + TINY: int = 0x01 + SHORT: int = 0x02 + LONG: int = 0x03 + FLOAT: int = 0x04 + DOUBLE: int = 0x05 + NULL: int = 0x06 + TIMESTAMP: int = 0x07 + LONGLONG: int = 0x08 + INT24: int = 0x09 + DATE: int = 0x0A + TIME: int = 0x0B + DATETIME: int = 0x0C + YEAR: int = 0x0D + NEWDATE: int = 0x0E + VARCHAR: int = 0x0F + BIT: int = 0x10 + JSON: int = 0xF5 + NEWDECIMAL: int = 0xF6 + ENUM: int = 0xF7 + SET: int = 0xF8 + TINY_BLOB: int = 0xF9 + MEDIUM_BLOB: int = 0xFA + LONG_BLOB: int = 0xFB + BLOB: int = 0xFC + VAR_STRING: int = 0xFD + STRING: int = 0xFE + GEOMETRY: int = 0xFF + + desc: Dict[str, Tuple[int, str]] = { + "DECIMAL": (0x00, "DECIMAL"), + "TINY": (0x01, "TINY"), + "SHORT": (0x02, "SHORT"), + "LONG": (0x03, "LONG"), + "FLOAT": (0x04, "FLOAT"), + "DOUBLE": (0x05, "DOUBLE"), + "NULL": (0x06, "NULL"), + "TIMESTAMP": (0x07, "TIMESTAMP"), + "LONGLONG": (0x08, "LONGLONG"), + "INT24": (0x09, "INT24"), + "DATE": (0x0A, "DATE"), + "TIME": (0x0B, "TIME"), + "DATETIME": (0x0C, "DATETIME"), + "YEAR": (0x0D, "YEAR"), + "NEWDATE": (0x0E, "NEWDATE"), + "VARCHAR": (0x0F, "VARCHAR"), + "BIT": (0x10, "BIT"), + "JSON": (0xF5, "JSON"), + "NEWDECIMAL": (0xF6, "NEWDECIMAL"), + "ENUM": (0xF7, "ENUM"), + "SET": (0xF8, "SET"), + "TINY_BLOB": (0xF9, "TINY_BLOB"), + "MEDIUM_BLOB": (0xFA, "MEDIUM_BLOB"), + "LONG_BLOB": (0xFB, "LONG_BLOB"), + "BLOB": (0xFC, "BLOB"), + "VAR_STRING": (0xFD, "VAR_STRING"), + "STRING": (0xFE, "STRING"), + "GEOMETRY": (0xFF, "GEOMETRY"), + } + + @classmethod + def get_string_types(cls) -> List[int]: + """Get the list of all string types""" + return [ + cls.VARCHAR, + cls.ENUM, + cls.VAR_STRING, + cls.STRING, + ] + + @classmethod + def get_binary_types(cls) -> List[int]: + """Get the list of all binary types""" + return [ + cls.TINY_BLOB, + cls.MEDIUM_BLOB, + cls.LONG_BLOB, + cls.BLOB, + ] + + @classmethod + def get_number_types(cls) -> List[int]: + """Get the list of all number types""" + return [ + cls.DECIMAL, + cls.NEWDECIMAL, + cls.TINY, + cls.SHORT, + cls.LONG, + cls.FLOAT, + cls.DOUBLE, + cls.LONGLONG, + cls.INT24, + cls.BIT, + cls.YEAR, + ] + + @classmethod + def get_timestamp_types(cls) -> List[int]: + """Get the list of all timestamp types""" + return [ + cls.DATETIME, + cls.TIMESTAMP, + ] + + +class FieldFlag(_Flags): + """MySQL Field Flags + + Field flags as found in MySQL sources mysql-src/include/mysql_com.h + """ + + _prefix: str = "" + NOT_NULL: int = 1 << 0 + PRI_KEY: int = 1 << 1 + UNIQUE_KEY: int = 1 << 2 + MULTIPLE_KEY: int = 1 << 3 + BLOB: int = 1 << 4 + UNSIGNED: int = 1 << 5 + ZEROFILL: int = 1 << 6 + BINARY: int = 1 << 7 + + ENUM: int = 1 << 8 + AUTO_INCREMENT: int = 1 << 9 + TIMESTAMP: int = 1 << 10 + SET: int = 1 << 11 + + NO_DEFAULT_VALUE: int = 1 << 12 + ON_UPDATE_NOW: int = 1 << 13 + NUM: int = 1 << 14 + PART_KEY: int = 1 << 15 + GROUP: int = 1 << 14 # SAME AS NUM !!!!!!!???? + UNIQUE: int = 1 << 16 + BINCMP: int = 1 << 17 + + GET_FIXED_FIELDS: int = 1 << 18 + FIELD_IN_PART_FUNC: int = 1 << 19 + FIELD_IN_ADD_INDEX: int = 1 << 20 + FIELD_IS_RENAMED: int = 1 << 21 + + desc: Dict[str, Tuple[int, str]] = { + "NOT_NULL": (1 << 0, "Field can't be NULL"), + "PRI_KEY": (1 << 1, "Field is part of a primary key"), + "UNIQUE_KEY": (1 << 2, "Field is part of a unique key"), + "MULTIPLE_KEY": (1 << 3, "Field is part of a key"), + "BLOB": (1 << 4, "Field is a blob"), + "UNSIGNED": (1 << 5, "Field is unsigned"), + "ZEROFILL": (1 << 6, "Field is zerofill"), + "BINARY": (1 << 7, "Field is binary "), + "ENUM": (1 << 8, "field is an enum"), + "AUTO_INCREMENT": (1 << 9, "field is a autoincrement field"), + "TIMESTAMP": (1 << 10, "Field is a timestamp"), + "SET": (1 << 11, "field is a set"), + "NO_DEFAULT_VALUE": (1 << 12, "Field doesn't have default value"), + "ON_UPDATE_NOW": (1 << 13, "Field is set to NOW on UPDATE"), + "NUM": (1 << 14, "Field is num (for clients)"), + "PART_KEY": (1 << 15, "Intern; Part of some key"), + "GROUP": (1 << 14, "Intern: Group field"), # Same as NUM + "UNIQUE": (1 << 16, "Intern: Used by sql_yacc"), + "BINCMP": (1 << 17, "Intern: Used by sql_yacc"), + "GET_FIXED_FIELDS": (1 << 18, "Used to get fields in item tree"), + "FIELD_IN_PART_FUNC": (1 << 19, "Field part of partition func"), + "FIELD_IN_ADD_INDEX": (1 << 20, "Intern: Field used in ADD INDEX"), + "FIELD_IS_RENAMED": (1 << 21, "Intern: Field is being renamed"), + } + + +class ServerCmdMeta(ABCMeta): + """ClientFlag Metaclass.""" + + def __getattribute__(cls, name: str) -> Any: + deprecated_options = ( + "FIELD_LIST", + "REFRESH", + "SHUTDOWN", + "PROCESS_INFO", + "PROCESS_KILL", + ) + if name in deprecated_options: + warnings.warn( + f"The option 'ServerCmd.{name}' is deprecated and will be removed in " + "a future release.", + category=DeprecationWarning, + ) + return super().__getattribute__(name) + + +class ServerCmd(_Constants, metaclass=ServerCmdMeta): + """MySQL Server Commands""" + + _prefix: str = "COM_" + SLEEP: int = 0 + QUIT: int = 1 + INIT_DB: int = 2 + QUERY: int = 3 + FIELD_LIST: int = 4 + CREATE_DB: int = 5 + DROP_DB: int = 6 + REFRESH: int = 7 + SHUTDOWN: int = 8 + STATISTICS: int = 9 + PROCESS_INFO: int = 10 + CONNECT: int = 11 + PROCESS_KILL: int = 12 + DEBUG: int = 13 + PING: int = 14 + TIME: int = 15 + DELAYED_INSERT: int = 16 + CHANGE_USER: int = 17 + BINLOG_DUMP: int = 18 + TABLE_DUMP: int = 19 + CONNECT_OUT: int = 20 + REGISTER_REPLICA: int = 21 + STMT_PREPARE: int = 22 + STMT_EXECUTE: int = 23 + STMT_SEND_LONG_DATA: int = 24 + STMT_CLOSE: int = 25 + STMT_RESET: int = 26 + SET_OPTION: int = 27 + STMT_FETCH: int = 28 + DAEMON: int = 29 + BINLOG_DUMP_GTID: int = 30 + RESET_CONNECTION: int = 31 + + desc: Dict[str, Tuple[int, str]] = { + "SLEEP": (0, "SLEEP"), + "QUIT": (1, "QUIT"), + "INIT_DB": (2, "INIT_DB"), + "QUERY": (3, "QUERY"), + "FIELD_LIST": (4, "FIELD_LIST"), + "CREATE_DB": (5, "CREATE_DB"), + "DROP_DB": (6, "DROP_DB"), + "REFRESH": (7, "REFRESH"), + "SHUTDOWN": (8, "SHUTDOWN"), + "STATISTICS": (9, "STATISTICS"), + "PROCESS_INFO": (10, "PROCESS_INFO"), + "CONNECT": (11, "CONNECT"), + "PROCESS_KILL": (12, "PROCESS_KILL"), + "DEBUG": (13, "DEBUG"), + "PING": (14, "PING"), + "TIME": (15, "TIME"), + "DELAYED_INSERT": (16, "DELAYED_INSERT"), + "CHANGE_USER": (17, "CHANGE_USER"), + "BINLOG_DUMP": (18, "BINLOG_DUMP"), + "TABLE_DUMP": (19, "TABLE_DUMP"), + "CONNECT_OUT": (20, "CONNECT_OUT"), + "REGISTER_REPLICA": (21, "REGISTER_REPLICA"), + "STMT_PREPARE": (22, "STMT_PREPARE"), + "STMT_EXECUTE": (23, "STMT_EXECUTE"), + "STMT_SEND_LONG_DATA": (24, "STMT_SEND_LONG_DATA"), + "STMT_CLOSE": (25, "STMT_CLOSE"), + "STMT_RESET": (26, "STMT_RESET"), + "SET_OPTION": (27, "SET_OPTION"), + "STMT_FETCH": (28, "STMT_FETCH"), + "DAEMON": (29, "DAEMON"), + "BINLOG_DUMP_GTID": (30, "BINLOG_DUMP_GTID"), + "RESET_CONNECTION": (31, "RESET_CONNECTION"), + } + + +class ClientFlag(_Flags): + """MySQL Client Flags + + Client options as found in the MySQL sources mysql-src/include/mysql_com.h + """ + + LONG_PASSWD: int = 1 << 0 + FOUND_ROWS: int = 1 << 1 + LONG_FLAG: int = 1 << 2 + CONNECT_WITH_DB: int = 1 << 3 + NO_SCHEMA: int = 1 << 4 + COMPRESS: int = 1 << 5 + ODBC: int = 1 << 6 + LOCAL_FILES: int = 1 << 7 + IGNORE_SPACE: int = 1 << 8 + PROTOCOL_41: int = 1 << 9 + INTERACTIVE: int = 1 << 10 + SSL: int = 1 << 11 + IGNORE_SIGPIPE: int = 1 << 12 + TRANSACTIONS: int = 1 << 13 + RESERVED: int = 1 << 14 + SECURE_CONNECTION: int = 1 << 15 + MULTI_STATEMENTS: int = 1 << 16 + MULTI_RESULTS: int = 1 << 17 + PS_MULTI_RESULTS: int = 1 << 18 + PLUGIN_AUTH: int = 1 << 19 + CONNECT_ARGS: int = 1 << 20 + PLUGIN_AUTH_LENENC_CLIENT_DATA: int = 1 << 21 + CAN_HANDLE_EXPIRED_PASSWORDS: int = 1 << 22 + SESION_TRACK: int = 1 << 23 # deprecated + SESSION_TRACK: int = 1 << 23 + DEPRECATE_EOF: int = 1 << 24 + CLIENT_QUERY_ATTRIBUTES: int = 1 << 27 + SSL_VERIFY_SERVER_CERT: int = 1 << 30 + REMEMBER_OPTIONS: int = 1 << 31 + MULTI_FACTOR_AUTHENTICATION: int = 1 << 28 + + desc: Dict[str, Tuple[int, str]] = { + "LONG_PASSWD": (1 << 0, "New more secure passwords"), + "FOUND_ROWS": (1 << 1, "Found instead of affected rows"), + "LONG_FLAG": (1 << 2, "Get all column flags"), + "CONNECT_WITH_DB": (1 << 3, "One can specify db on connect"), + "NO_SCHEMA": (1 << 4, "Don't allow database.table.column"), + "COMPRESS": (1 << 5, "Can use compression protocol"), + "ODBC": (1 << 6, "ODBC client"), + "LOCAL_FILES": (1 << 7, "Can use LOAD DATA LOCAL"), + "IGNORE_SPACE": (1 << 8, "Ignore spaces before ''"), + "PROTOCOL_41": (1 << 9, "New 4.1 protocol"), + "INTERACTIVE": (1 << 10, "This is an interactive client"), + "SSL": (1 << 11, "Switch to SSL after handshake"), + "IGNORE_SIGPIPE": (1 << 12, "IGNORE sigpipes"), + "TRANSACTIONS": (1 << 13, "Client knows about transactions"), + "RESERVED": (1 << 14, "Old flag for 4.1 protocol"), + "SECURE_CONNECTION": (1 << 15, "New 4.1 authentication"), + "MULTI_STATEMENTS": (1 << 16, "Enable/disable multi-stmt support"), + "MULTI_RESULTS": (1 << 17, "Enable/disable multi-results"), + "PS_MULTI_RESULTS": (1 << 18, "Multi-results in PS-protocol"), + "PLUGIN_AUTH": (1 << 19, "Client supports plugin authentication"), + "CONNECT_ARGS": (1 << 20, "Client supports connection attributes"), + "PLUGIN_AUTH_LENENC_CLIENT_DATA": ( + 1 << 21, + "Enable authentication response packet to be larger than 255 bytes", + ), + "CAN_HANDLE_EXPIRED_PASSWORDS": ( + 1 << 22, + "Don't close the connection for a connection with expired password", + ), + "SESION_TRACK": ( # deprecated + 1 << 23, + "Capable of handling server state change information", + ), + "SESSION_TRACK": ( + 1 << 23, + "Capable of handling server state change information", + ), + "DEPRECATE_EOF": (1 << 24, "Client no longer needs EOF packet"), + "CLIENT_QUERY_ATTRIBUTES": ( + 1 << 27, + "Support optional extension for query parameters", + ), + "SSL_VERIFY_SERVER_CERT": (1 << 30, ""), + "REMEMBER_OPTIONS": (1 << 31, ""), + } + + default: List[int] = [ + LONG_PASSWD, + LONG_FLAG, + CONNECT_WITH_DB, + PROTOCOL_41, + TRANSACTIONS, + SECURE_CONNECTION, + MULTI_STATEMENTS, + MULTI_RESULTS, + CONNECT_ARGS, + ] + + @classmethod + def get_default(cls) -> int: + """Get the default client options set + + Returns a flag with all the default client options set""" + flags = 0 + for option in cls.default: + flags |= option + return flags + + +class ServerFlag(_Flags): + """MySQL Server Flags + + Server flags as found in the MySQL sources mysql-src/include/mysql_com.h + """ + + _prefix: str = "SERVER_" + STATUS_IN_TRANS: int = 1 << 0 + STATUS_AUTOCOMMIT: int = 1 << 1 + MORE_RESULTS_EXISTS: int = 1 << 3 + QUERY_NO_GOOD_INDEX_USED: int = 1 << 4 + QUERY_NO_INDEX_USED: int = 1 << 5 + STATUS_CURSOR_EXISTS: int = 1 << 6 + STATUS_LAST_ROW_SENT: int = 1 << 7 + STATUS_DB_DROPPED: int = 1 << 8 + STATUS_NO_BACKSLASH_ESCAPES: int = 1 << 9 + SERVER_STATUS_METADATA_CHANGED: int = 1 << 10 + SERVER_QUERY_WAS_SLOW: int = 1 << 11 + SERVER_PS_OUT_PARAMS: int = 1 << 12 + SERVER_STATUS_IN_TRANS_READONLY: int = 1 << 13 + SERVER_SESSION_STATE_CHANGED: int = 1 << 14 + + desc: Dict[str, Tuple[int, str]] = { + "SERVER_STATUS_IN_TRANS": (1 << 0, "Transaction has started"), + "SERVER_STATUS_AUTOCOMMIT": (1 << 1, "Server in auto_commit mode"), + "SERVER_MORE_RESULTS_EXISTS": ( + 1 << 3, + "Multi query - next query exists", + ), + "SERVER_QUERY_NO_GOOD_INDEX_USED": (1 << 4, ""), + "SERVER_QUERY_NO_INDEX_USED": (1 << 5, ""), + "SERVER_STATUS_CURSOR_EXISTS": ( + 1 << 6, + "Set when server opened a read-only non-scrollable cursor for a query.", + ), + "SERVER_STATUS_LAST_ROW_SENT": ( + 1 << 7, + "Set when a read-only cursor is exhausted", + ), + "SERVER_STATUS_DB_DROPPED": (1 << 8, "A database was dropped"), + "SERVER_STATUS_NO_BACKSLASH_ESCAPES": (1 << 9, ""), + "SERVER_STATUS_METADATA_CHANGED": ( + 1024, + "Set if after a prepared statement " + "reprepare we discovered that the " + "new statement returns a different " + "number of result set columns.", + ), + "SERVER_QUERY_WAS_SLOW": (2048, ""), + "SERVER_PS_OUT_PARAMS": ( + 4096, + "To mark ResultSet containing output parameter values.", + ), + "SERVER_STATUS_IN_TRANS_READONLY": ( + 8192, + "Set if multi-statement transaction is a read-only transaction.", + ), + "SERVER_SESSION_STATE_CHANGED": ( + 1 << 14, + "Session state has changed on the " + "server because of the execution of " + "the last statement", + ), + } + + +class RefreshOptionMeta(ABCMeta): + """RefreshOption Metaclass.""" + + @property + def SLAVE(self) -> int: # pylint: disable=bad-mcs-method-argument,invalid-name + """Return the deprecated alias of RefreshOption.REPLICA. + + Raises a warning about this attribute deprecation. + """ + return _obsolete_option( + "RefreshOption.SLAVE", + "RefreshOption.REPLICA", + RefreshOption.REPLICA, + ) + + +class RefreshOption(_Constants, metaclass=RefreshOptionMeta): + """MySQL Refresh command options. + + Options used when sending the COM_REFRESH server command. + """ + + _prefix: str = "REFRESH_" + GRANT: int = 1 << 0 + LOG: int = 1 << 1 + TABLES: int = 1 << 2 + HOST: int = 1 << 3 + STATUS: int = 1 << 4 + THREADS: int = 1 << 5 + REPLICA: int = 1 << 6 + + desc: Dict[str, Tuple[int, str]] = { + "GRANT": (1 << 0, "Refresh grant tables"), + "LOG": (1 << 1, "Start on new log file"), + "TABLES": (1 << 2, "close all tables"), + "HOST": (1 << 3, "Flush host cache"), + "STATUS": (1 << 4, "Flush status variables"), + "THREADS": (1 << 5, "Flush thread cache"), + "REPLICA": (1 << 6, "Reset source info and restart replica thread"), + "SLAVE": (1 << 6, "Deprecated option; use REPLICA instead."), + } + + +class ShutdownType(_Constants): + """MySQL Shutdown types + + Shutdown types used by the COM_SHUTDOWN server command. + """ + + _prefix: str = "" + SHUTDOWN_DEFAULT: int = 0 + SHUTDOWN_WAIT_CONNECTIONS: int = 1 + SHUTDOWN_WAIT_TRANSACTIONS: int = 2 + SHUTDOWN_WAIT_UPDATES: int = 8 + SHUTDOWN_WAIT_ALL_BUFFERS: int = 16 + SHUTDOWN_WAIT_CRITICAL_BUFFERS: int = 17 + KILL_QUERY: int = 254 + KILL_CONNECTION: int = 255 + + desc: Dict[str, Tuple[int, str]] = { + "SHUTDOWN_DEFAULT": ( + SHUTDOWN_DEFAULT, + "defaults to SHUTDOWN_WAIT_ALL_BUFFERS", + ), + "SHUTDOWN_WAIT_CONNECTIONS": ( + SHUTDOWN_WAIT_CONNECTIONS, + "wait for existing connections to finish", + ), + "SHUTDOWN_WAIT_TRANSACTIONS": ( + SHUTDOWN_WAIT_TRANSACTIONS, + "wait for existing trans to finish", + ), + "SHUTDOWN_WAIT_UPDATES": ( + SHUTDOWN_WAIT_UPDATES, + "wait for existing updates to finish", + ), + "SHUTDOWN_WAIT_ALL_BUFFERS": ( + SHUTDOWN_WAIT_ALL_BUFFERS, + "flush InnoDB and other storage engine buffers", + ), + "SHUTDOWN_WAIT_CRITICAL_BUFFERS": ( + SHUTDOWN_WAIT_CRITICAL_BUFFERS, + "don't flush InnoDB buffers, flush other storage engines' buffers", + ), + "KILL_QUERY": (KILL_QUERY, "(no description)"), + "KILL_CONNECTION": (KILL_CONNECTION, "(no description)"), + } + + +class CharacterSet(_Constants): + """MySQL supported character sets and collations + + List of character sets with their collations supported by MySQL. This + maps to the character set we get from the server within the handshake + packet. + + The list is hardcode so we avoid a database query when getting the + name of the used character set or collation. + """ + + # Use LTS character set as default + desc: List[ + Optional[Tuple[str, str, bool]] + ] = MYSQL_CHARACTER_SETS_57 # type: ignore[assignment] + mysql_version: Tuple[int, ...] = (5, 7) + + # Multi-byte character sets which use 5c (backslash) in characters + slash_charsets: Tuple[int, ...] = (1, 13, 28, 84, 87, 88) + + @classmethod + def set_mysql_version(cls, version: Tuple[int, ...]) -> None: + """Set the MySQL major version and change the charset mapping if is 5.7. + + Args: + version (tuple): MySQL version tuple. + """ + cls.mysql_version = version[:2] + if cls.mysql_version >= (8, 0): + cls.desc = MYSQL_CHARACTER_SETS + + @classmethod + def get_info(cls, setid: int) -> Tuple[str, str]: + """Retrieves character set information as tuple using an ID + + Retrieves character set and collation information based on the + given MySQL ID. + + Raises ProgrammingError when character set is not supported. + + Returns a tuple. + """ + try: + return cls.desc[setid][0:2] + except IndexError: + raise ProgrammingError(f"Character set '{setid}' unsupported") from None + + @classmethod + def get_desc(cls, name: int) -> str: # type: ignore[override] + """Retrieves character set information as string using an ID + + Retrieves character set and collation information based on the + given MySQL ID. + + Returns a tuple. + """ + charset, collation = cls.get_info(name) + return f"{charset}/{collation}" + + @classmethod + def get_default_collation(cls, charset: Union[int, str]) -> Tuple[str, str, int]: + """Retrieves the default collation for given character set + + Raises ProgrammingError when character set is not supported. + + Returns list (collation, charset, index) + """ + if isinstance(charset, int): + try: + info = cls.desc[charset] + return info[1], info[0], charset + except (IndexError, KeyError) as err: + raise ProgrammingError( + f"Character set ID '{charset}' unsupported" + ) from err + + for cid, info in enumerate(cls.desc): + if info is None: + continue + if info[0] == charset and info[2] is True: + return info[1], info[0], cid + + raise ProgrammingError(f"Character set '{charset}' unsupported") + + @classmethod + def get_charset_info( + cls, charset: Optional[Union[int, str]] = None, collation: Optional[str] = None + ) -> Tuple[int, str, str]: + """Get character set information using charset name and/or collation + + Retrieves character set and collation information given character + set name and/or a collation name. + If charset is an integer, it will look up the character set based + on the MySQL's ID. + For example: + get_charset_info('utf8',None) + get_charset_info(collation='utf8_general_ci') + get_charset_info(47) + + Raises ProgrammingError when character set is not supported. + + Returns a tuple with (id, characterset name, collation) + """ + info: Optional[Union[Tuple[str, str, bool], Tuple[str, str, int]]] = None + if isinstance(charset, int): + try: + info = cls.desc[charset] + return (charset, info[0], info[1]) + except IndexError as err: + raise ProgrammingError(f"Character set ID {charset} unknown") from err + + if charset in ("utf8", "utf-8") and cls.mysql_version >= (8, 0): + charset = "utf8mb4" + if charset is not None and collation is None: + info = cls.get_default_collation(charset) + return (info[2], info[1], info[0]) + if charset is None and collation is not None: + for cid, info in enumerate(cls.desc): + if info is None: + continue + if collation == info[1]: + return (cid, info[0], info[1]) + raise ProgrammingError(f"Collation '{collation}' unknown") + for cid, info in enumerate(cls.desc): + if info is None: + continue + if info[0] == charset and info[1] == collation: + return (cid, info[0], info[1]) + _ = cls.get_default_collation(charset) + raise ProgrammingError(f"Collation '{collation}' unknown") + + @classmethod + def get_supported(cls) -> Tuple[str, ...]: + """Retrieves a list with names of all supproted character sets + + Returns a tuple. + """ + res = [] + for info in cls.desc: + if info and info[0] not in res: + res.append(info[0]) + return tuple(res) + + +class SQLMode(_Constants): + """MySQL SQL Modes + + The numeric values of SQL Modes are not interesting, only the names + are used when setting the SQL_MODE system variable using the MySQL + SET command. + + See http://dev.mysql.com/doc/refman/5.6/en/server-sql-mode.html + """ + + _prefix: str = "MODE_" + REAL_AS_FLOAT: str = "REAL_AS_FLOAT" + PIPES_AS_CONCAT: str = "PIPES_AS_CONCAT" + ANSI_QUOTES: str = "ANSI_QUOTES" + IGNORE_SPACE: str = "IGNORE_SPACE" + NOT_USED: str = "NOT_USED" + ONLY_FULL_GROUP_BY: str = "ONLY_FULL_GROUP_BY" + NO_UNSIGNED_SUBTRACTION: str = "NO_UNSIGNED_SUBTRACTION" + NO_DIR_IN_CREATE: str = "NO_DIR_IN_CREATE" + POSTGRESQL: str = "POSTGRESQL" + ORACLE: str = "ORACLE" + MSSQL: str = "MSSQL" + DB2: str = "DB2" + MAXDB: str = "MAXDB" + NO_KEY_OPTIONS: str = "NO_KEY_OPTIONS" + NO_TABLE_OPTIONS: str = "NO_TABLE_OPTIONS" + NO_FIELD_OPTIONS: str = "NO_FIELD_OPTIONS" + MYSQL323: str = "MYSQL323" + MYSQL40: str = "MYSQL40" + ANSI: str = "ANSI" + NO_AUTO_VALUE_ON_ZERO: str = "NO_AUTO_VALUE_ON_ZERO" + NO_BACKSLASH_ESCAPES: str = "NO_BACKSLASH_ESCAPES" + STRICT_TRANS_TABLES: str = "STRICT_TRANS_TABLES" + STRICT_ALL_TABLES: str = "STRICT_ALL_TABLES" + NO_ZERO_IN_DATE: str = "NO_ZERO_IN_DATE" + NO_ZERO_DATE: str = "NO_ZERO_DATE" + INVALID_DATES: str = "INVALID_DATES" + ERROR_FOR_DIVISION_BY_ZERO: str = "ERROR_FOR_DIVISION_BY_ZERO" + TRADITIONAL: str = "TRADITIONAL" + NO_AUTO_CREATE_USER: str = "NO_AUTO_CREATE_USER" + HIGH_NOT_PRECEDENCE: str = "HIGH_NOT_PRECEDENCE" + NO_ENGINE_SUBSTITUTION: str = "NO_ENGINE_SUBSTITUTION" + PAD_CHAR_TO_FULL_LENGTH: str = "PAD_CHAR_TO_FULL_LENGTH" + + @classmethod + def get_desc(cls, name: str) -> Optional[str]: + raise NotImplementedError + + @classmethod + def get_info(cls, setid: int) -> Optional[str]: + raise NotImplementedError + + @classmethod + def get_full_info(cls) -> Tuple[str, ...]: + """Returns a sequence of all available SQL Modes + + This class method returns a tuple containing all SQL Mode names. The + names will be alphabetically sorted. + + Returns a tuple. + """ + res = [] + for key in vars(cls).keys(): + if not key.startswith("_") and not hasattr(getattr(cls, key), "__call__"): + res.append(key) + return tuple(sorted(res)) + + +CONN_ATTRS_DN: List[str] = [ + "_pid", + "_platform", + "_source_host", + "_client_name", + "_client_license", + "_client_version", + "_os", + "_connector_name", + "_connector_license", + "_connector_version", +] + +# TLS v1.0 cipher suites IANI to OpenSSL name translation +TLSV1_CIPHER_SUITES: Dict[str, str] = { + "TLS_RSA_WITH_NULL_MD5": "NULL-MD5", + "TLS_RSA_WITH_NULL_SHA": "NULL-SHA", + "TLS_RSA_WITH_RC4_128_MD5": "RC4-MD5", + "TLS_RSA_WITH_RC4_128_SHA": "RC4-SHA", + "TLS_RSA_WITH_IDEA_CBC_SHA": "IDEA-CBC-SHA", + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": "DES-CBC3-SHA", + "TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA": "Not implemented.", + "TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA": "Not implemented.", + "TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA": "DHE-DSS-DES-CBC3-SHA", + "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA": "DHE-RSA-DES-CBC3-SHA", + "TLS_DH_anon_WITH_RC4_128_MD5": "ADH-RC4-MD5", + "TLS_DH_anon_WITH_3DES_EDE_CBC_SHA": "ADH-DES-CBC3-SHA", + # AES cipher suites from RFC3268, extending TLS v1.0 + "TLS_RSA_WITH_AES_128_CBC_SHA": "AES128-SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA": "AES256-SHA", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA": "DH-DSS-AES128-SHA", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA": "DH-DSS-AES256-SHA", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA": "DH-RSA-AES128-SHA", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA": "DH-RSA-AES256-SHA", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA": "DHE-DSS-AES128-SHA", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA": "DHE-DSS-AES256-SHA", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA": "DHE-RSA-AES128-SHA", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA": "DHE-RSA-AES256-SHA", + "TLS_DH_anon_WITH_AES_128_CBC_SHA": "ADH-AES128-SHA", + "TLS_DH_anon_WITH_AES_256_CBC_SHA": "ADH-AES256-SHA", + # Camellia cipher suites from RFC4132, extending TLS v1.0 + "TLS_RSA_WITH_CAMELLIA_128_CBC_SHA": "CAMELLIA128-SHA", + "TLS_RSA_WITH_CAMELLIA_256_CBC_SHA": "CAMELLIA256-SHA", + "TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA": "DH-DSS-CAMELLIA128-SHA", + "TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA": "DH-DSS-CAMELLIA256-SHA", + "TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA": "DH-RSA-CAMELLIA128-SHA", + "TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA": "DH-RSA-CAMELLIA256-SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA": "DHE-DSS-CAMELLIA128-SHA", + "TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA": "DHE-DSS-CAMELLIA256-SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA": "DHE-RSA-CAMELLIA128-SHA", + "TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA": "DHE-RSA-CAMELLIA256-SHA", + "TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA": "ADH-CAMELLIA128-SHA", + "TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA": "ADH-CAMELLIA256-SHA", + # SEED cipher suites from RFC4162, extending TLS v1.0 + "TLS_RSA_WITH_SEED_CBC_SHA": "SEED-SHA", + "TLS_DH_DSS_WITH_SEED_CBC_SHA": "DH-DSS-SEED-SHA", + "TLS_DH_RSA_WITH_SEED_CBC_SHA": "DH-RSA-SEED-SHA", + "TLS_DHE_DSS_WITH_SEED_CBC_SHA": "DHE-DSS-SEED-SHA", + "TLS_DHE_RSA_WITH_SEED_CBC_SHA": "DHE-RSA-SEED-SHA", + "TLS_DH_anon_WITH_SEED_CBC_SHA": "ADH-SEED-SHA", + # GOST cipher suites from draft-chudov-cryptopro-cptls, extending TLS v1.0 + "TLS_GOSTR341094_WITH_28147_CNT_IMIT": "GOST94-GOST89-GOST89", + "TLS_GOSTR341001_WITH_28147_CNT_IMIT": "GOST2001-GOST89-GOST89", + "TLS_GOSTR341094_WITH_NULL_GOSTR3411": "GOST94-NULL-GOST94", + "TLS_GOSTR341001_WITH_NULL_GOSTR3411": "GOST2001-NULL-GOST94", +} + +# TLS v1.1 cipher suites IANI to OpenSSL name translation +TLSV1_1_CIPHER_SUITES: Dict[str, str] = TLSV1_CIPHER_SUITES + +# TLS v1.2 cipher suites IANI to OpenSSL name translation +TLSV1_2_CIPHER_SUITES: Dict[str, str] = { + "TLS_RSA_WITH_NULL_SHA256": "NULL-SHA256", + "TLS_RSA_WITH_AES_128_CBC_SHA256": "AES128-SHA256", + "TLS_RSA_WITH_AES_256_CBC_SHA256": "AES256-SHA256", + "TLS_RSA_WITH_AES_128_GCM_SHA256": "AES128-GCM-SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384": "AES256-GCM-SHA384", + "TLS_DH_RSA_WITH_AES_128_CBC_SHA256": "DH-RSA-AES128-SHA256", + "TLS_DH_RSA_WITH_AES_256_CBC_SHA256": "DH-RSA-AES256-SHA256", + "TLS_DH_RSA_WITH_AES_128_GCM_SHA256": "DH-RSA-AES128-GCM-SHA256", + "TLS_DH_RSA_WITH_AES_256_GCM_SHA384": "DH-RSA-AES256-GCM-SHA384", + "TLS_DH_DSS_WITH_AES_128_CBC_SHA256": "DH-DSS-AES128-SHA256", + "TLS_DH_DSS_WITH_AES_256_CBC_SHA256": "DH-DSS-AES256-SHA256", + "TLS_DH_DSS_WITH_AES_128_GCM_SHA256": "DH-DSS-AES128-GCM-SHA256", + "TLS_DH_DSS_WITH_AES_256_GCM_SHA384": "DH-DSS-AES256-GCM-SHA384", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256": "DHE-RSA-AES128-SHA256", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256": "DHE-RSA-AES256-SHA256", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256": "DHE-RSA-AES128-GCM-SHA256", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384": "DHE-RSA-AES256-GCM-SHA384", + "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256": "DHE-DSS-AES128-SHA256", + "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256": "DHE-DSS-AES256-SHA256", + "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256": "DHE-DSS-AES128-GCM-SHA256", + "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384": "DHE-DSS-AES256-GCM-SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": "ECDHE-RSA-AES128-SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384": "ECDHE-RSA-AES256-SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": "ECDHE-RSA-AES128-GCM-SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": "ECDHE-RSA-AES256-GCM-SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": "ECDHE-ECDSA-AES128-SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384": "ECDHE-ECDSA-AES256-SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": "ECDHE-ECDSA-AES128-GCM-SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": "ECDHE-ECDSA-AES256-GCM-SHA384", + "TLS_DH_anon_WITH_AES_128_CBC_SHA256": "ADH-AES128-SHA256", + "TLS_DH_anon_WITH_AES_256_CBC_SHA256": "ADH-AES256-SHA256", + "TLS_DH_anon_WITH_AES_128_GCM_SHA256": "ADH-AES128-GCM-SHA256", + "TLS_DH_anon_WITH_AES_256_GCM_SHA384": "ADH-AES256-GCM-SHA384", + "RSA_WITH_AES_128_CCM": "AES128-CCM", + "RSA_WITH_AES_256_CCM": "AES256-CCM", + "DHE_RSA_WITH_AES_128_CCM": "DHE-RSA-AES128-CCM", + "DHE_RSA_WITH_AES_256_CCM": "DHE-RSA-AES256-CCM", + "RSA_WITH_AES_128_CCM_8": "AES128-CCM8", + "RSA_WITH_AES_256_CCM_8": "AES256-CCM8", + "DHE_RSA_WITH_AES_128_CCM_8": "DHE-RSA-AES128-CCM8", + "DHE_RSA_WITH_AES_256_CCM_8": "DHE-RSA-AES256-CCM8", + "ECDHE_ECDSA_WITH_AES_128_CCM": "ECDHE-ECDSA-AES128-CCM", + "ECDHE_ECDSA_WITH_AES_256_CCM": "ECDHE-ECDSA-AES256-CCM", + "ECDHE_ECDSA_WITH_AES_128_CCM_8": "ECDHE-ECDSA-AES128-CCM8", + "ECDHE_ECDSA_WITH_AES_256_CCM_8": "ECDHE-ECDSA-AES256-CCM8", + # ARIA cipher suites from RFC6209, extending TLS v1.2 + "TLS_RSA_WITH_ARIA_128_GCM_SHA256": "ARIA128-GCM-SHA256", + "TLS_RSA_WITH_ARIA_256_GCM_SHA384": "ARIA256-GCM-SHA384", + "TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256": "DHE-RSA-ARIA128-GCM-SHA256", + "TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384": "DHE-RSA-ARIA256-GCM-SHA384", + "TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256": "DHE-DSS-ARIA128-GCM-SHA256", + "TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384": "DHE-DSS-ARIA256-GCM-SHA384", + "TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256": "ECDHE-ECDSA-ARIA128-GCM-SHA256", + "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384": "ECDHE-ECDSA-ARIA256-GCM-SHA384", + "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256": "ECDHE-ARIA128-GCM-SHA256", + "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384": "ECDHE-ARIA256-GCM-SHA384", + "TLS_PSK_WITH_ARIA_128_GCM_SHA256": "PSK-ARIA128-GCM-SHA256", + "TLS_PSK_WITH_ARIA_256_GCM_SHA384": "PSK-ARIA256-GCM-SHA384", + "TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256": "DHE-PSK-ARIA128-GCM-SHA256", + "TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384": "DHE-PSK-ARIA256-GCM-SHA384", + "TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256": "RSA-PSK-ARIA128-GCM-SHA256", + "TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384": "RSA-PSK-ARIA256-GCM-SHA384", + # Camellia HMAC-Based cipher suites from RFC6367, extending TLS v1.2 + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256": "ECDHE-ECDSA-CAMELLIA128-SHA256", + "TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384": "ECDHE-ECDSA-CAMELLIA256-SHA384", + "TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256": "ECDHE-RSA-CAMELLIA128-SHA256", + "TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384": "ECDHE-RSA-CAMELLIA256-SHA384", + # Pre-shared keying (PSK) cipher suites", + "PSK_WITH_NULL_SHA": "PSK-NULL-SHA", + "DHE_PSK_WITH_NULL_SHA": "DHE-PSK-NULL-SHA", + "RSA_PSK_WITH_NULL_SHA": "RSA-PSK-NULL-SHA", + "PSK_WITH_RC4_128_SHA": "PSK-RC4-SHA", + "PSK_WITH_3DES_EDE_CBC_SHA": "PSK-3DES-EDE-CBC-SHA", + "PSK_WITH_AES_128_CBC_SHA": "PSK-AES128-CBC-SHA", + "PSK_WITH_AES_256_CBC_SHA": "PSK-AES256-CBC-SHA", + "DHE_PSK_WITH_RC4_128_SHA": "DHE-PSK-RC4-SHA", + "DHE_PSK_WITH_3DES_EDE_CBC_SHA": "DHE-PSK-3DES-EDE-CBC-SHA", + "DHE_PSK_WITH_AES_128_CBC_SHA": "DHE-PSK-AES128-CBC-SHA", + "DHE_PSK_WITH_AES_256_CBC_SHA": "DHE-PSK-AES256-CBC-SHA", + "RSA_PSK_WITH_RC4_128_SHA": "RSA-PSK-RC4-SHA", + "RSA_PSK_WITH_3DES_EDE_CBC_SHA": "RSA-PSK-3DES-EDE-CBC-SHA", + "RSA_PSK_WITH_AES_128_CBC_SHA": "RSA-PSK-AES128-CBC-SHA", + "RSA_PSK_WITH_AES_256_CBC_SHA": "RSA-PSK-AES256-CBC-SHA", + "PSK_WITH_AES_128_GCM_SHA256": "PSK-AES128-GCM-SHA256", + "PSK_WITH_AES_256_GCM_SHA384": "PSK-AES256-GCM-SHA384", + "DHE_PSK_WITH_AES_128_GCM_SHA256": "DHE-PSK-AES128-GCM-SHA256", + "DHE_PSK_WITH_AES_256_GCM_SHA384": "DHE-PSK-AES256-GCM-SHA384", + "RSA_PSK_WITH_AES_128_GCM_SHA256": "RSA-PSK-AES128-GCM-SHA256", + "RSA_PSK_WITH_AES_256_GCM_SHA384": "RSA-PSK-AES256-GCM-SHA384", + "PSK_WITH_AES_128_CBC_SHA256": "PSK-AES128-CBC-SHA256", + "PSK_WITH_AES_256_CBC_SHA384": "PSK-AES256-CBC-SHA384", + "PSK_WITH_NULL_SHA256": "PSK-NULL-SHA256", + "PSK_WITH_NULL_SHA384": "PSK-NULL-SHA384", + "DHE_PSK_WITH_AES_128_CBC_SHA256": "DHE-PSK-AES128-CBC-SHA256", + "DHE_PSK_WITH_AES_256_CBC_SHA384": "DHE-PSK-AES256-CBC-SHA384", + "DHE_PSK_WITH_NULL_SHA256": "DHE-PSK-NULL-SHA256", + "DHE_PSK_WITH_NULL_SHA384": "DHE-PSK-NULL-SHA384", + "RSA_PSK_WITH_AES_128_CBC_SHA256": "RSA-PSK-AES128-CBC-SHA256", + "RSA_PSK_WITH_AES_256_CBC_SHA384": "RSA-PSK-AES256-CBC-SHA384", + "RSA_PSK_WITH_NULL_SHA256": "RSA-PSK-NULL-SHA256", + "RSA_PSK_WITH_NULL_SHA384": "RSA-PSK-NULL-SHA384", + "ECDHE_PSK_WITH_RC4_128_SHA": "ECDHE-PSK-RC4-SHA", + "ECDHE_PSK_WITH_3DES_EDE_CBC_SHA": "ECDHE-PSK-3DES-EDE-CBC-SHA", + "ECDHE_PSK_WITH_AES_128_CBC_SHA": "ECDHE-PSK-AES128-CBC-SHA", + "ECDHE_PSK_WITH_AES_256_CBC_SHA": "ECDHE-PSK-AES256-CBC-SHA", + "ECDHE_PSK_WITH_AES_128_CBC_SHA256": "ECDHE-PSK-AES128-CBC-SHA256", + "ECDHE_PSK_WITH_AES_256_CBC_SHA384": "ECDHE-PSK-AES256-CBC-SHA384", + "ECDHE_PSK_WITH_NULL_SHA": "ECDHE-PSK-NULL-SHA", + "ECDHE_PSK_WITH_NULL_SHA256": "ECDHE-PSK-NULL-SHA256", + "ECDHE_PSK_WITH_NULL_SHA384": "ECDHE-PSK-NULL-SHA384", + "PSK_WITH_CAMELLIA_128_CBC_SHA256": "PSK-CAMELLIA128-SHA256", + "PSK_WITH_CAMELLIA_256_CBC_SHA384": "PSK-CAMELLIA256-SHA384", + "DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256": "DHE-PSK-CAMELLIA128-SHA256", + "DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384": "DHE-PSK-CAMELLIA256-SHA384", + "RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256": "RSA-PSK-CAMELLIA128-SHA256", + "RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384": "RSA-PSK-CAMELLIA256-SHA384", + "ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256": "ECDHE-PSK-CAMELLIA128-SHA256", + "ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384": "ECDHE-PSK-CAMELLIA256-SHA384", + "PSK_WITH_AES_128_CCM": "PSK-AES128-CCM", + "PSK_WITH_AES_256_CCM": "PSK-AES256-CCM", + "DHE_PSK_WITH_AES_128_CCM": "DHE-PSK-AES128-CCM", + "DHE_PSK_WITH_AES_256_CCM": "DHE-PSK-AES256-CCM", + "PSK_WITH_AES_128_CCM_8": "PSK-AES128-CCM8", + "PSK_WITH_AES_256_CCM_8": "PSK-AES256-CCM8", + "DHE_PSK_WITH_AES_128_CCM_8": "DHE-PSK-AES128-CCM8", + "DHE_PSK_WITH_AES_256_CCM_8": "DHE-PSK-AES256-CCM8", + # ChaCha20-Poly1305 cipher suites, extending TLS v1.2 + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": "ECDHE-RSA-CHACHA20-POLY1305", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": "ECDHE-ECDSA-CHACHA20-POLY1305", + "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256": "DHE-RSA-CHACHA20-POLY1305", + "TLS_PSK_WITH_CHACHA20_POLY1305_SHA256": "PSK-CHACHA20-POLY1305", + "TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256": "ECDHE-PSK-CHACHA20-POLY1305", + "TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256": "DHE-PSK-CHACHA20-POLY1305", + "TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256": "RSA-PSK-CHACHA20-POLY1305", +} + +# TLS v1.3 cipher suites IANI to OpenSSL name translation +TLSV1_3_CIPHER_SUITES: Dict[str, str] = { + "TLS_AES_128_GCM_SHA256": "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384": "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256": "TLS_CHACHA20_POLY1305_SHA256", + "TLS_AES_128_CCM_SHA256": "TLS_AES_128_CCM_SHA256", + "TLS_AES_128_CCM_8_SHA256": "TLS_AES_128_CCM_8_SHA256", +} + +TLS_CIPHER_SUITES: Dict[str, Dict[str, str]] = { + "TLSv1": TLSV1_CIPHER_SUITES, + "TLSv1.1": TLSV1_1_CIPHER_SUITES, + "TLSv1.2": TLSV1_2_CIPHER_SUITES, + "TLSv1.3": TLSV1_3_CIPHER_SUITES, +} + +OPENSSL_CS_NAMES: Dict[str, ValuesView[str]] = { + "TLSv1": TLSV1_CIPHER_SUITES.values(), + "TLSv1.1": TLSV1_1_CIPHER_SUITES.values(), + "TLSv1.2": TLSV1_2_CIPHER_SUITES.values(), + "TLSv1.3": TLSV1_3_CIPHER_SUITES.values(), +} diff --git a/mysql/connector/conversion.py b/mysql/connector/conversion.py new file mode 100644 index 0000000..dc0886b --- /dev/null +++ b/mysql/connector/conversion.py @@ -0,0 +1,740 @@ +# Copyright (c) 2009, 2023, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Converting MySQL and Python types +""" + +import datetime +import math +import struct +import time + +from decimal import Decimal +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union + +from .constants import CharacterSet, FieldFlag, FieldType +from .custom_types import HexLiteral +from .types import ( + DescriptionType, + RowType, + StrOrBytes, + ToMysqlInputTypes, + ToMysqlOutputTypes, + ToPythonOutputTypes, +) +from .utils import NUMERIC_TYPES + +CONVERT_ERROR = "Could not convert '{value}' to python {pytype}" + + +class MySQLConverterBase: + """Base class for conversion classes + + All class dealing with converting to and from MySQL data types must + be a subclass of this class. + """ + + def __init__( + self, + charset: Optional[str] = "utf8", + use_unicode: bool = True, + str_fallback: bool = False, + ) -> None: + self.python_types: Optional[Tuple[Any, ...]] = None + self.mysql_types: Optional[Tuple[Any, ...]] = None + self.charset: Optional[str] = None + self.charset_id: int = 0 + self.set_charset(charset) + self.use_unicode: bool = use_unicode + self.str_fallback: bool = str_fallback + self._cache_field_types: Dict[ + int, + Callable[[bytes, DescriptionType], ToPythonOutputTypes], + ] = {} + + def set_charset(self, charset: Optional[str]) -> None: + """Set character set""" + if charset in ("utf8mb4", "utf8mb3"): + charset = "utf8" + if charset is not None: + self.charset = charset + else: + # default to utf8 + self.charset = "utf8" + self.charset_id = CharacterSet.get_charset_info(self.charset)[0] + + def set_unicode(self, value: bool = True) -> None: + """Set whether to use Unicode""" + self.use_unicode = value + + def to_mysql( + self, value: ToMysqlInputTypes + ) -> Union[ToMysqlInputTypes, HexLiteral]: + """Convert Python data type to MySQL""" + type_name = value.__class__.__name__.lower() + try: + converted: ToMysqlOutputTypes = getattr(self, f"_{type_name}_to_mysql")( + value + ) + return converted + except AttributeError: + return value + + def to_python( + self, vtype: DescriptionType, value: Optional[bytes] + ) -> ToPythonOutputTypes: + """Convert MySQL data type to Python""" + + if (value == b"\x00" or value is None) and vtype[1] != FieldType.BIT: + # Don't go further when we hit a NULL value + return None + + if not self._cache_field_types: + self._cache_field_types = {} + for name, info in FieldType.desc.items(): + try: + self._cache_field_types[info[0]] = getattr( + self, f"_{name.lower()}_to_python" + ) + except AttributeError: + # We ignore field types which has no method + pass + if value is None: + return None + try: + return self._cache_field_types[vtype[1]](value, vtype) + except KeyError: + return value + + @staticmethod + def escape( + value: Any, + sql_mode: Optional[str] = None, # pylint: disable=unused-argument + ) -> Any: + """Escape buffer for sending to MySQL""" + return value + + @staticmethod + def quote(buf: Any) -> StrOrBytes: + """Quote buffer for sending to MySQL""" + return str(buf) + + +class MySQLConverter(MySQLConverterBase): + """Default conversion class for MySQL Connector/Python. + + o escape method: for escaping values send to MySQL + o quoting method: for quoting values send to MySQL in statements + o conversion mapping: maps Python and MySQL data types to + function for converting them. + + Whenever one needs to convert values differently, a converter_class + argument can be given while instantiating a new connection like + cnx.connect(converter_class=CustomMySQLConverterClass). + + """ + + def __init__( + self, + charset: Optional[str] = None, + use_unicode: bool = True, + str_fallback: bool = False, + ) -> None: + MySQLConverterBase.__init__(self, charset, use_unicode, str_fallback) + self._cache_field_types: Dict[ + int, + Callable[[bytes, DescriptionType], ToPythonOutputTypes], + ] = {} + + @staticmethod + def escape(value: Any, sql_mode: Optional[str] = None) -> Any: + """ + Escapes special characters as they are expected to by when MySQL + receives them. + As found in MySQL source mysys/charset.c + + Returns the value if not a string, or the escaped string. + """ + if isinstance(value, (bytes, bytearray)): + if sql_mode == "NO_BACKSLASH_ESCAPES": + return value.replace(b"'", b"''") + value = value.replace(b"\\", b"\\\\") + value = value.replace(b"\n", b"\\n") + value = value.replace(b"\r", b"\\r") + value = value.replace(b"\047", b"\134\047") # single quotes + value = value.replace(b"\042", b"\134\042") # double quotes + value = value.replace(b"\032", b"\134\032") # for Win32 + elif isinstance(value, str) and not isinstance(value, HexLiteral): + if sql_mode == "NO_BACKSLASH_ESCAPES": + return value.replace("'", "''") + value = value.replace("\\", "\\\\") + value = value.replace("\n", "\\n") + value = value.replace("\r", "\\r") + value = value.replace("\047", "\134\047") # single quotes + value = value.replace("\042", "\134\042") # double quotes + value = value.replace("\032", "\134\032") # for Win32 + return value + + @staticmethod + def quote(buf: Optional[Union[float, int, Decimal, HexLiteral, bytes]]) -> bytes: + """ + Quote the parameters for commands. General rules: + o numbers are returns as bytes using ascii codec + o None is returned as bytearray(b'NULL') + o Everything else is single quoted '' + + Returns a bytearray object. + """ + if isinstance(buf, NUMERIC_TYPES): + return str(buf).encode("ascii") + if isinstance(buf, type(None)): + return bytearray(b"NULL") + return bytearray(b"'" + buf + b"'") # type: ignore[operator] + + def to_mysql(self, value: ToMysqlInputTypes) -> ToMysqlOutputTypes: + """Convert Python data type to MySQL""" + type_name = value.__class__.__name__.lower() + try: + converted: ToMysqlOutputTypes = getattr(self, f"_{type_name}_to_mysql")( + value + ) + return converted + except AttributeError: + if self.str_fallback: + return str(value).encode() + raise TypeError( + f"Python '{type_name}' cannot be converted to a MySQL type" + ) from None + + def to_python( + self, + vtype: DescriptionType, + value: Optional[bytes], + ) -> ToPythonOutputTypes: + """Convert MySQL data type to Python""" + # \x00 + if value == 0 and vtype[1] != FieldType.BIT: + # Don't go further when we hit a NULL value + return None + if value is None: + return None + + if not self._cache_field_types: + self._cache_field_types = {} + for name, info in FieldType.desc.items(): + try: + self._cache_field_types[info[0]] = getattr( + self, f"_{name.lower()}_to_python" + ) + except AttributeError: + # We ignore field types which has no method + pass + + try: + return self._cache_field_types[vtype[1]](value, vtype) + except KeyError: + # If one type is not defined, we just return the value as str + try: + return value.decode("utf-8") + except UnicodeDecodeError: + return value + except ValueError as err: + raise ValueError(f"{err} (field {vtype[0]})") from err + except TypeError as err: + raise TypeError(f"{err} (field {vtype[0]})") from err + + @staticmethod + def _int_to_mysql(value: int) -> int: + """Convert value to int""" + return int(value) + + @staticmethod + def _long_to_mysql(value: int) -> int: + """Convert value to int + + Note: There is no type "long" in Python 3 since integers are of unlimited size. + Since Python 2 is no longer supported, this method should be deprecated. + """ + return int(value) + + @staticmethod + def _float_to_mysql(value: float) -> Optional[float]: + """Convert value to float""" + if math.isnan(value): + return None + return float(value) + + def _str_to_mysql(self, value: str) -> Union[bytes, HexLiteral]: + """Convert value to string""" + return self._unicode_to_mysql(value) + + def _unicode_to_mysql(self, value: str) -> Union[bytes, HexLiteral]: + """Convert unicode""" + charset = self.charset + charset_id = self.charset_id + if charset == "binary": + charset = "utf8" + charset_id = CharacterSet.get_charset_info(charset)[0] + encoded = value.encode(charset) + if charset_id in CharacterSet.slash_charsets: + if b"\x5c" in encoded: + return HexLiteral(value, charset) + return encoded + + @staticmethod + def _bytes_to_mysql(value: bytes) -> bytes: + """Convert value to bytes""" + return value + + @staticmethod + def _bytearray_to_mysql(value: bytearray) -> bytes: + """Convert value to bytes""" + return bytes(value) + + @staticmethod + def _bool_to_mysql(value: bool) -> int: + """Convert value to boolean""" + return 1 if value else 0 + + @staticmethod + def _nonetype_to_mysql(value: None) -> None: # pylint: disable=unused-argument + """ + This would return what None would be in MySQL, but instead we + leave it None and return it right away. The actual conversion + from None to NULL happens in the quoting functionality. + + Return None. + """ + return None + + @staticmethod + def _datetime_to_mysql(value: datetime.datetime) -> bytes: + """ + Converts a datetime instance to a string suitable for MySQL. + The returned string has format: %Y-%m-%d %H:%M:%S[.%f] + + If the instance isn't a datetime.datetime type, it return None. + + Returns a bytes. + """ + if value.microsecond: + fmt = "{0:04d}-{1:02d}-{2:02d} {3:02d}:{4:02d}:{5:02d}.{6:06d}" + return fmt.format( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.microsecond, + ).encode("ascii") + + fmt = "{0:04d}-{1:02d}-{2:02d} {3:02d}:{4:02d}:{5:02d}" + return fmt.format( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + ).encode("ascii") + + @staticmethod + def _date_to_mysql(value: datetime.date) -> bytes: + """ + Converts a date instance to a string suitable for MySQL. + The returned string has format: %Y-%m-%d + + If the instance isn't a datetime.date type, it return None. + + Returns a bytes. + """ + return f"{value.year:04d}-{value.month:02d}-{value.day:02d}".encode("ascii") + + @staticmethod + def _time_to_mysql(value: datetime.time) -> bytes: + """ + Converts a time instance to a string suitable for MySQL. + The returned string has format: %H:%M:%S[.%f] + + If the instance isn't a datetime.time type, it return None. + + Returns a bytes. + """ + if value.microsecond: + return value.strftime("%H:%M:%S.%f").encode("ascii") + return value.strftime("%H:%M:%S").encode("ascii") + + @staticmethod + def _struct_time_to_mysql(value: time.struct_time) -> bytes: + """ + Converts a time.struct_time sequence to a string suitable + for MySQL. + The returned string has format: %Y-%m-%d %H:%M:%S + + Returns a bytes or None when not valid. + """ + return time.strftime("%Y-%m-%d %H:%M:%S", value).encode("ascii") + + @staticmethod + def _timedelta_to_mysql(value: datetime.timedelta) -> bytes: + """ + Converts a timedelta instance to a string suitable for MySQL. + The returned string has format: %H:%M:%S + + Returns a bytes. + """ + seconds = abs(value.days * 86400 + value.seconds) + + if value.microseconds: + fmt = "{0:02d}:{1:02d}:{2:02d}.{3:06d}" + if value.days < 0: + mcs = 1000000 - value.microseconds + seconds -= 1 + else: + mcs = value.microseconds + else: + fmt = "{0:02d}:{1:02d}:{2:02d}" + + if value.days < 0: + fmt = "-" + fmt + + (hours, remainder) = divmod(seconds, 3600) + (mins, secs) = divmod(remainder, 60) + + if value.microseconds: + result = fmt.format(hours, mins, secs, mcs) + else: + result = fmt.format(hours, mins, secs) + + return result.encode("ascii") + + @staticmethod + def _decimal_to_mysql(value: Decimal) -> Optional[bytes]: + """ + Converts a decimal.Decimal instance to a string suitable for + MySQL. + + Returns a bytes or None when not valid. + """ + if isinstance(value, Decimal): + return str(value).encode("ascii") + + return None + + def row_to_python( + self, row: Tuple[bytes, ...], fields: List[DescriptionType] + ) -> RowType: + """Convert a MySQL text result row to Python types + + The row argument is a sequence containing text result returned + by a MySQL server. Each value of the row is converted to the + using the field type information in the fields argument. + + Returns a tuple. + """ + i = 0 + result: List[ToPythonOutputTypes] = [None] * len(fields) + + if not self._cache_field_types: + self._cache_field_types = {} + for name, info in FieldType.desc.items(): + try: + self._cache_field_types[info[0]] = getattr( + self, f"_{name.lower()}_to_python" + ) + except AttributeError: + # We ignore field types which has no method + pass + + for field in fields: + field_type = field[1] + + if (row[i] == 0 and field_type != FieldType.BIT) or row[i] is None: + # Don't convert NULL value + i += 1 + continue + + try: + result[i] = self._cache_field_types[field_type](row[i], field) + except KeyError: + # If one type is not defined, we just return the value as str + try: + result[i] = row[i].decode("utf-8") + except UnicodeDecodeError: + result[i] = row[i] + except (ValueError, TypeError) as err: + # Item "ValueError" of "Union[ValueError, TypeError]" has no attribute "message" + err.message = f"{err} (field {field[0]})" # type: ignore[union-attr] + raise + + i += 1 + + return tuple(result) + + # pylint: disable=unused-argument + @staticmethod + def _float_to_python(value: bytes, desc: Optional[DescriptionType] = None) -> float: + """ + Returns value as float type. + """ + return float(value) + + _double_to_python = _float_to_python + + @staticmethod + def _int_to_python(value: bytes, desc: Optional[DescriptionType] = None) -> int: + """ + Returns value as int type. + """ + return int(value) + + _tiny_to_python = _int_to_python + _short_to_python = _int_to_python + _int24_to_python = _int_to_python + _long_to_python = _int_to_python + _longlong_to_python = _int_to_python + + def _decimal_to_python( + self, value: bytes, desc: Optional[DescriptionType] = None + ) -> Decimal: + """ + Returns value as a decimal.Decimal. + """ + val = value.decode(self.charset) + return Decimal(val) + + _newdecimal_to_python = _decimal_to_python + + @staticmethod + def _str(value: bytes, desc: Optional[DescriptionType] = None) -> str: + """ + Returns value as str type. + """ + return str(value) + + @staticmethod + def _bit_to_python(value: bytes, dsc: Optional[DescriptionType] = None) -> int: + """Returns BIT columntype as integer""" + int_val = value + if len(int_val) < 8: + int_val = b"\x00" * (8 - len(int_val)) + int_val + return int(struct.unpack(">Q", int_val)[0]) + + @staticmethod + def _date_to_python( + value: bytes, dsc: Optional[DescriptionType] = None + ) -> Optional[datetime.date]: + """Converts TIME column MySQL to a python datetime.datetime type. + + Raises ValueError if the value can not be converted. + + Returns DATE column type as datetime.date type. + """ + if isinstance(value, datetime.date): + return value + try: + parts = value.split(b"-") + if len(parts) != 3: + raise ValueError(f"invalid datetime format: {parts} len: {len(parts)}") + try: + return datetime.date(int(parts[0]), int(parts[1]), int(parts[2])) + except ValueError: + return None + except (IndexError, ValueError): + raise ValueError( + f"Could not convert {repr(value)} to python datetime.timedelta" + ) from None + + _NEWDATE_to_python = _date_to_python + + @staticmethod + def _time_to_python( + value: bytes, dsc: Optional[DescriptionType] = None + ) -> datetime.timedelta: + """Converts TIME column value to python datetime.time value type. + + Converts the TIME column MySQL type passed as bytes to a python + datetime.datetime type. + + Raises ValueError if the value can not be converted. + + Returns datetime.timedelta type. + """ + mcs: Optional[Union[int, bytes]] = None + try: + (hms, mcs) = value.split(b".") + mcs = int(mcs.ljust(6, b"0")) + except (TypeError, ValueError): + hms = value + mcs = 0 + try: + (hours, mins, secs) = [int(d) for d in hms.split(b":")] + if value[0] == 45 or value[0] == "-": + mins, secs, mcs = ( + -mins, + -secs, + -mcs, # pylint: disable=invalid-unary-operand-type + ) + return datetime.timedelta( + hours=hours, minutes=mins, seconds=secs, microseconds=mcs + ) + except (IndexError, TypeError, ValueError): + raise ValueError( + CONVERT_ERROR.format(value=value, pytype="datetime.timedelta") + ) from None + + @staticmethod + def _datetime_to_python( + value: bytes, dsc: Optional[DescriptionType] = None + ) -> Optional[datetime.datetime]: + """Converts DATETIME column value to python datetime.time value type. + + Converts the DATETIME column MySQL type passed as bytes to a python + datetime.datetime type. + + Returns: datetime.datetime type. + """ + if isinstance(value, datetime.datetime): + return value + datetime_val = None + mcs: Optional[Union[int, bytes]] = None + try: + (date_, time_) = value.split(b" ") + if len(time_) > 8: + (hms, mcs) = time_.split(b".") + mcs = int(mcs.ljust(6, b"0")) + else: + hms = time_ + mcs = 0 + dtval = ( + [int(i) for i in date_.split(b"-")] + + [int(i) for i in hms.split(b":")] + + [ + mcs, + ] + ) + if len(dtval) < 6: + raise ValueError(f"invalid datetime format: {dtval} len: {len(dtval)}") + # Note that by default MySQL accepts invalid timestamps + # (this is also backward compatibility). + # Traditionaly C/py returns None for this well formed but + # invalid datetime for python like '0000-00-00 HH:MM:SS'. + try: + datetime_val = datetime.datetime(*dtval) # type: ignore[arg-type] + except ValueError: + return None + except (IndexError, TypeError): + raise ValueError( + CONVERT_ERROR.format(value=value, pytype="datetime.timedelta") + ) from None + + return datetime_val + + _timestamp_to_python = _datetime_to_python + + @staticmethod + def _year_to_python(value: bytes, dsc: Optional[DescriptionType] = None) -> int: + """Returns YEAR column type as integer""" + try: + year = int(value) + except ValueError as err: + raise ValueError(f"Failed converting YEAR to int ({repr(value)})") from err + + return year + + def _set_to_python( + self, value: bytes, dsc: Optional[DescriptionType] = None + ) -> Set[str]: + """Returns SET column type as set + + Actually, MySQL protocol sees a SET as a string type field. So this + code isn't called directly, but used by STRING_to_python() method. + + Returns SET column type as a set. + """ + set_type = None + val = value.decode(self.charset) + if not val: + return set() + try: + set_type = set(val.split(",")) + except ValueError as err: + raise ValueError( + f"Could not convert set {repr(value)} to a sequence" + ) from err + return set_type + + def _string_to_python( + self, value: bytes, dsc: Optional[DescriptionType] = None + ) -> Union[StrOrBytes, Set[str]]: + """ + Note that a SET is a string too, but using the FieldFlag we can see + whether we have to split it. + + Returns string typed columns as string type. + """ + if self.charset == "binary": + return value + if dsc is not None: + if dsc[1] == FieldType.JSON and self.use_unicode: + return value.decode(self.charset) + if dsc[7] & FieldFlag.SET: + return self._set_to_python(value, dsc) + # 'binary' charset + if dsc[8] == 63: + return value + if isinstance(value, (bytes, bytearray)) and self.use_unicode: + try: + return value.decode(self.charset) + except UnicodeDecodeError: + return value + + return value + + _var_string_to_python = _string_to_python + _json_to_python = _string_to_python + + def _blob_to_python( + self, value: bytes, dsc: Optional[DescriptionType] = None + ) -> Union[StrOrBytes, Set[str]]: + """Convert BLOB data type to Python.""" + if dsc is not None: + if ( + dsc[7] & FieldFlag.BLOB + and dsc[7] & FieldFlag.BINARY + # 'binary' charset + and dsc[8] == 63 + ): + return bytes(value) + return self._string_to_python(value, dsc) + + _long_blob_to_python = _blob_to_python + _medium_blob_to_python = _blob_to_python + _tiny_blob_to_python = _blob_to_python + # pylint: enable=unused-argument diff --git a/mysql/connector/cursor.py b/mysql/connector/cursor.py new file mode 100644 index 0000000..0248615 --- /dev/null +++ b/mysql/connector/cursor.py @@ -0,0 +1,1756 @@ +# Copyright (c) 2009, 2023, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# mypy: disable-error-code="assignment,arg-type,attr-defined,index,override,call-overload" + +"""Cursor classes.""" +from __future__ import annotations + +import re +import warnings +import weakref + +from collections import namedtuple +from decimal import Decimal +from typing import ( + Any, + Dict, + Generator, + Iterator, + List, + NoReturn, + Optional, + Sequence, + Tuple, + Type, + Union, +) +from weakref import CallableProxyType + +from .abstracts import NAMED_TUPLE_CACHE, MySQLConnectionAbstract, MySQLCursorAbstract +from .constants import ServerFlag +from .errors import ( + Error, + InterfaceError, + NotSupportedError, + ProgrammingError, + get_mysql_exception, +) +from .types import ( + DescriptionType, + EofPacketType, + ParamsDictType, + ParamsSequenceOrDictType, + ParamsSequenceType, + ResultType, + RowType, + StrOrBytes, + ToPythonOutputTypes, + WarningType, +) + +SQL_COMMENT = r"\/\*.*?\*\/" +RE_SQL_COMMENT = re.compile( + rf"""({SQL_COMMENT})|(["'`][^"'`]*?({SQL_COMMENT})[^"'`]*?["'`])""", + re.I | re.M | re.S, +) +RE_SQL_ON_DUPLICATE = re.compile( + r"""\s*ON\s+DUPLICATE\s+KEY(?:[^"'`]*["'`][^"'`]*["'`])*[^"'`]*$""", + re.I | re.M | re.S, +) +RE_SQL_INSERT_STMT = re.compile( + rf"({SQL_COMMENT}|\s)*INSERT({SQL_COMMENT}|\s)" + r"*(?:IGNORE\s+)?INTO\s+[`'\"]?.+[`'\"]?(?:\.[`'\"]?.+[`'\"]?)" + r"{0,2}\s+VALUES\s*(\(.+\)).*", + re.I | re.M | re.S, +) +RE_SQL_INSERT_VALUES = re.compile(r".*VALUES\s*(\(.+\)).*", re.I | re.M | re.S) +RE_PY_PARAM = re.compile(b"(%s)") +RE_PY_MAPPING_PARAM = re.compile( + rb""" + % + \((?P[^)]+)\) + (?P[diouxXeEfFgGcrs%]) + """, + re.X, +) +RE_SQL_SPLIT_STMTS = re.compile( + b""";(?=(?:[^"'`]*(?:"[^"]*"|'[^']*'|`[^`]*`))*[^"'`]*$)""" +) +RE_SQL_FIND_PARAM = re.compile(b"""%s(?=(?:[^"'`]*["'`][^"'`]*["'`])*[^"'`]*$)""") +RE_SQL_PYTHON_REPLACE_PARAM = re.compile(r"%\(.*?\)s") +RE_SQL_PYTHON_CAPTURE_PARAM_NAME = re.compile(r"%\((.*?)\)s") + +ERR_NO_RESULT_TO_FETCH = "No result set to fetch from" + +MAX_RESULTS = 4294967295 + + +class _ParamSubstitutor: + """ + Substitutes parameters into SQL statement. + """ + + def __init__(self, params: Sequence[bytes]) -> None: + self.params: Sequence[bytes] = params + self.index: int = 0 + + def __call__(self, matchobj: re.Match) -> bytes: + index = self.index + self.index += 1 + try: + return bytes(self.params[index]) + except IndexError: + raise ProgrammingError( + "Not enough parameters for the SQL statement" + ) from None + + @property + def remaining(self) -> int: + """Returns number of parameters remaining to be substituted""" + return len(self.params) - self.index + + +def _bytestr_format_dict(bytestr: bytes, value_dict: Dict[bytes, bytes]) -> bytes: + """ + >>> _bytestr_format_dict(b'%(a)s', {b'a': b'foobar'}) + b'foobar + >>> _bytestr_format_dict(b'%%(a)s', {b'a': b'foobar'}) + b'%%(a)s' + >>> _bytestr_format_dict(b'%%%(a)s', {b'a': b'foobar'}) + b'%%foobar' + >>> _bytestr_format_dict(b'%(x)s %(y)s', + ... {b'x': b'x=%(y)s', b'y': b'y=%(x)s'}) + b'x=%(y)s y=%(x)s' + """ + + def replace(matchobj: re.Match) -> bytes: + """Replace pattern.""" + value: Optional[bytes] = None + groups = matchobj.groupdict() + if groups["conversion_type"] == b"%": + value = b"%" + if groups["conversion_type"] == b"s": + key = groups["mapping_key"] + value = value_dict[key] + if value is None: + raise ValueError( + f"Unsupported conversion_type: {groups['conversion_type']}" + ) + return value + + stmt = RE_PY_MAPPING_PARAM.sub(replace, bytestr) + return stmt + + +class CursorBase(MySQLCursorAbstract): + """ + Base for defining MySQLCursor. This class is a skeleton and defines + methods and members as required for the Python Database API + Specification v2.0. + + It's better to inherite from MySQLCursor. + """ + + _raw: bool = False + + def __init__(self) -> None: + self._description: Optional[List[DescriptionType]] = None + self._rowcount: int = -1 + self.arraysize: int = 1 + super().__init__() + + def callproc(self, procname: str, args: Sequence[Any] = ()) -> Any: + """Calls a stored procedue with the given arguments + + The arguments will be set during this session, meaning + they will be called like ___arg where + is an enumeration (+1) of the arguments. + + Coding Example: + 1) Definining the Stored Routine in MySQL: + CREATE PROCEDURE multiply(IN pFac1 INT, IN pFac2 INT, OUT pProd INT) + BEGIN + SET pProd := pFac1 * pFac2; + END + + 2) Executing in Python: + args = (5,5,0) # 0 is to hold pprod + cursor.callproc('multiply', args) + print(cursor.fetchone()) + + Does not return a value, but a result set will be + available when the CALL-statement execute successfully. + Raises exceptions when something is wrong. + """ + + def close(self) -> Any: + """Close the cursor.""" + + def execute( + self, + operation: Any, + params: Union[Sequence[Any], Dict[str, Any]] = (), + multi: bool = False, + ) -> Any: + """Executes the given operation + + Executes the given operation substituting any markers with + the given parameters. + + For example, getting all rows where id is 5: + cursor.execute("SELECT * FROM t1 WHERE id = %s", (5,)) + + The multi argument should be set to True when executing multiple + statements in one operation. If not set and multiple results are + found, an InterfaceError will be raised. + + If warnings where generated, and connection.get_warnings is True, then + self._warnings will be a list containing these warnings. + + Returns an iterator when multi is True, otherwise None. + """ + + def executemany( + self, operation: Any, seq_params: Sequence[Union[Sequence[Any], Dict[str, Any]]] + ) -> Any: + """Execute the given operation multiple times + + The executemany() method will execute the operation iterating + over the list of parameters in seq_params. + + Example: Inserting 3 new employees and their phone number + + data = [ + ('Jane','555-001'), + ('Joe', '555-001'), + ('John', '555-003') + ] + stmt = "INSERT INTO employees (name, phone) VALUES ('%s','%s')" + cursor.executemany(stmt, data) + + INSERT statements are optimized by batching the data, that is + using the MySQL multiple rows syntax. + + Results are discarded. If they are needed, consider looping over + data using the execute() method. + """ + + def fetchone(self) -> Optional[Sequence[Any]]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + + def fetchmany(self, size: int = 1) -> List[Sequence[Any]]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set. + """ + + def fetchall(self) -> List[Sequence[Any]]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + + def nextset(self) -> Any: + """Not Implemented.""" + + def setinputsizes(self, sizes: Any) -> Any: + """Not Implemented.""" + + def setoutputsize(self, size: Any, column: Any = None) -> Any: + """Not Implemented.""" + + def reset(self, free: bool = True) -> Any: + """Reset the cursor to default""" + + @property + def description(self) -> Optional[List[DescriptionType]]: + """Returns description of columns in a result + + This property returns a list of tuples describing the columns in + in a result set. A tuple is described as follows:: + + (column_name, + type, + None, + None, + None, + None, + null_ok, + column_flags) # Addition to PEP-249 specs + + Returns a list of tuples. + """ + return self._description + + @property + def rowcount(self) -> int: + """Returns the number of rows produced or affected + + This property returns the number of rows produced by queries + such as a SELECT, or affected rows when executing DML statements + like INSERT or UPDATE. + + Note that for non-buffered cursors it is impossible to know the + number of rows produced before having fetched them all. For those, + the number of rows will be -1 right after execution, and + incremented when fetching rows. + + Returns an integer. + """ + return self._rowcount + + +class MySQLCursor(CursorBase): + """Default cursor for interacting with MySQL + + This cursor will execute statements and handle the result. It will + not automatically fetch all rows. + + MySQLCursor should be inherited whenever other functionallity is + required. An example would to change the fetch* member functions + to return dictionaries instead of lists of values. + + Implements the Python Database API Specification v2.0 (PEP-249) + """ + + def __init__( + self, connection: Optional[Type[MySQLConnectionAbstract]] = None + ) -> None: + CursorBase.__init__(self) + self._connection: CallableProxyType[Type[MySQLConnectionAbstract]] = None + self._nextrow: Tuple[Optional[RowType], Optional[EofPacketType]] = ( + None, + None, + ) + self._binary: bool = False + + if connection is not None: + self._set_connection(connection) + + def __iter__(self) -> Iterator[RowType]: + """ + Iteration over the result set which calls self.fetchone() + and returns the next row. + """ + return iter(self.fetchone, None) + + def _set_connection(self, connection: Type[MySQLConnectionAbstract]) -> None: + """Set the connection""" + try: + self._connection = weakref.proxy(connection) + self._connection.is_connected() + except (AttributeError, TypeError): + raise InterfaceError(errno=2048) from None + + def _reset_result(self) -> None: + """Reset the cursor to default""" + self._rowcount: int = -1 + self._nextrow = (None, None) + self._stored_results: List[MySQLCursor] = [] + self._warnings: Optional[List[WarningType]] = None + self._warning_count: int = 0 + self._description: Optional[List[DescriptionType]] = None + self._executed: Optional[StrOrBytes] = None + self._executed_list: List[StrOrBytes] = [] + self.reset() + + def _have_unread_result(self) -> bool: + """Check whether there is an unread result""" + try: + return self._connection.unread_result + except AttributeError: + return False + + def _check_executed(self) -> None: + """Check if the statement has been executed. + + Raises an error if the statement has not been executed. + """ + if self._executed is None: + raise InterfaceError(ERR_NO_RESULT_TO_FETCH) + + def __next__(self) -> RowType: + """ + Used for iterating over the result set. Calles self.fetchone() + to get the next row. + """ + try: + row = self.fetchone() + except InterfaceError: + raise StopIteration from None + if not row: + raise StopIteration + return row + + def close(self) -> bool: + """Close the cursor + + Returns True when successful, otherwise False. + """ + if self._connection is None: + return False + + self._connection.handle_unread_result() + self._reset_result() + self._connection = None + + return True + + def _process_params_dict( + self, params: ParamsDictType + ) -> Dict[bytes, Union[bytes, Decimal]]: + """Process query parameters given as dictionary""" + res: Dict[bytes, Any] = {} + try: + sql_mode = self._connection.sql_mode + to_mysql = self._connection.converter.to_mysql + escape = self._connection.converter.escape + quote = self._connection.converter.quote + for key, value in params.items(): + conv = value + conv = to_mysql(conv) + conv = escape(conv, sql_mode) + if not isinstance(value, Decimal): + conv = quote(conv) + res[key.encode()] = conv + except Exception as err: + raise ProgrammingError( + f"Failed processing pyformat-parameters; {err}" + ) from err + return res + + def _process_params( + self, params: ParamsSequenceType + ) -> Tuple[Union[bytes, Decimal], ...]: + """Process query parameters.""" + res = params[:] + try: + sql_mode = self._connection.sql_mode + to_mysql = self._connection.converter.to_mysql + escape = self._connection.converter.escape + quote = self._connection.converter.quote + res = [to_mysql(value) for value in res] + res = [escape(value, sql_mode) for value in res] + res = [ + quote(value) if not isinstance(params[i], Decimal) else value + for i, value in enumerate(res) + ] + except Exception as err: + raise ProgrammingError( + f"Failed processing format-parameters; {err}" + ) from err + return tuple(res) + + def _handle_noresultset(self, res: ResultType) -> None: + """Handles result of execute() when there is no result set""" + try: + self._rowcount = res["affected_rows"] + self._last_insert_id = res["insert_id"] + self._warning_count = res["warning_count"] + except (KeyError, TypeError) as err: + raise ProgrammingError(f"Failed handling non-resultset; {err}") from None + + self._handle_warnings() + + def _handle_resultset(self) -> None: + """Handles result set + + This method handles the result set and is called after reading + and storing column information in _handle_result(). For non-buffering + cursors, this method is usually doing nothing. + """ + + def _handle_result(self, result: ResultType) -> None: + """ + Handle the result after a command was send. The result can be either + an OK-packet or a dictionary containing column/eof information. + + Raises InterfaceError when result is not a dict() or result is + invalid. + """ + if not isinstance(result, dict): + raise InterfaceError("Result was not a dict()") + + if "columns" in result: + # Weak test, must be column/eof information + self._description = result["columns"] + self._connection.unread_result = True + self._handle_resultset() + elif "affected_rows" in result: + # Weak test, must be an OK-packet + self._connection.unread_result = False + self._handle_noresultset(result) + else: + raise InterfaceError("Invalid result") + + def _execute_iter( + self, query_iter: Generator[ResultType, None, None] + ) -> Generator[MySQLCursor, None, None]: + """Generator returns MySQLCursor objects for multiple statements + + This method is only used when multiple statements are executed + by the execute() method. It uses zip() to make an iterator from the + given query_iter (result of MySQLConnection.cmd_query_iter()) and + the list of statements that were executed. + """ + executed_list = RE_SQL_SPLIT_STMTS.split(self._executed) + + i = 0 + while True: + try: + result = next(query_iter) + self._reset_result() + self._handle_result(result) + try: + self._executed = executed_list[i].strip() + i += 1 + except IndexError: + self._executed = executed_list[0] + + yield self + except StopIteration: + return + + def execute( + self, + operation: StrOrBytes, + params: Optional[ParamsSequenceOrDictType] = None, + multi: bool = False, + ) -> Optional[Generator[MySQLCursor, None, None]]: + """Executes the given operation + + Executes the given operation substituting any markers with + the given parameters. + + For example, getting all rows where id is 5: + cursor.execute("SELECT * FROM t1 WHERE id = %s", (5,)) + + The multi argument should be set to True when executing multiple + statements in one operation. If not set and multiple results are + found, an InterfaceError will be raised. + + If warnings where generated, and connection.get_warnings is True, then + self._warnings will be a list containing these warnings. + + Returns an iterator when multi is True, otherwise None. + """ + if not operation: + return None + + try: + if not self._connection: + raise ProgrammingError + except (ProgrammingError, ReferenceError) as err: + raise ProgrammingError("Cursor is not connected") from err + + self._connection.handle_unread_result() + + self._reset_result() + stmt: StrOrBytes = "" + + try: + if not isinstance(operation, (bytes, bytearray)): + stmt = operation.encode(self._connection.python_charset) + else: + stmt = operation + except (UnicodeDecodeError, UnicodeEncodeError) as err: + raise ProgrammingError(str(err)) from err + + if params: + if isinstance(params, dict): + stmt = _bytestr_format_dict(stmt, self._process_params_dict(params)) + elif isinstance(params, (list, tuple)): + psub = _ParamSubstitutor(self._process_params(params)) + stmt = RE_PY_PARAM.sub(psub, stmt) + if psub.remaining != 0: + raise ProgrammingError( + "Not all parameters were used in the SQL statement" + ) + else: + raise ProgrammingError( + f"Could not process parameters: {type(params).__name__}({params})," + " it must be of type list, tuple or dict" + ) + + self._executed = stmt + if multi: + self._executed_list = [] + return self._execute_iter(self._connection.cmd_query_iter(stmt)) + + try: + self._handle_result(self._connection.cmd_query(stmt)) + except InterfaceError as err: + if self._connection.have_next_result: + raise InterfaceError( + "Use multi=True when executing multiple statements" + ) from err + raise + return None + + def _batch_insert( + self, operation: str, seq_params: Sequence[ParamsSequenceOrDictType] + ) -> Optional[bytes]: + """Implements multi row insert""" + + def remove_comments(match: re.Match) -> str: + """Remove comments from INSERT statements. + + This function is used while removing comments from INSERT + statements. If the matched string is a comment not enclosed + by quotes, it returns an empty string, else the string itself. + """ + if match.group(1): + return "" + return match.group(2) + + tmp = re.sub( + RE_SQL_ON_DUPLICATE, + "", + re.sub(RE_SQL_COMMENT, remove_comments, operation), + ) + + matches = re.search(RE_SQL_INSERT_VALUES, tmp) + if not matches: + raise InterfaceError( + "Failed rewriting statement for multi-row INSERT. Check SQL syntax" + ) + fmt = matches.group(1).encode(self._connection.python_charset) + values = [] + + try: + stmt = operation.encode(self._connection.python_charset) + for params in seq_params: + tmp = fmt + if isinstance(params, dict): + tmp = _bytestr_format_dict(tmp, self._process_params_dict(params)) + else: + psub = _ParamSubstitutor(self._process_params(params)) + tmp = RE_PY_PARAM.sub(psub, tmp) + if psub.remaining != 0: + raise ProgrammingError( + "Not all parameters were used in the SQL statement" + ) + values.append(tmp) + if fmt in stmt: + stmt = stmt.replace(fmt, b",".join(values), 1) + self._executed = stmt + return stmt + return None + except (UnicodeDecodeError, UnicodeEncodeError) as err: + raise ProgrammingError(str(err)) from err + except Error: + raise + except Exception as err: + raise InterfaceError(f"Failed executing the operation; {err}") from None + + def executemany( + self, operation: str, seq_params: Sequence[ParamsSequenceOrDictType] + ) -> Optional[Generator[MySQLCursor, None, None]]: + """Execute the given operation multiple times + + The executemany() method will execute the operation iterating + over the list of parameters in seq_params. + + Example: Inserting 3 new employees and their phone number + + data = [ + ('Jane','555-001'), + ('Joe', '555-001'), + ('John', '555-003') + ] + stmt = "INSERT INTO employees (name, phone) VALUES ('%s','%s)" + cursor.executemany(stmt, data) + + INSERT statements are optimized by batching the data, that is + using the MySQL multiple rows syntax. + + Results are discarded. If they are needed, consider looping over + data using the execute() method. + """ + if not operation or not seq_params: + return None + self._connection.handle_unread_result() + + try: + _ = iter(seq_params) + except TypeError as err: + raise ProgrammingError("Parameters for query must be an Iterable") from err + + # Optimize INSERTs by batching them + if re.match(RE_SQL_INSERT_STMT, operation): + if not seq_params: + self._rowcount = 0 + return None + stmt = self._batch_insert(operation, seq_params) + if stmt is not None: + self._executed = stmt + return self.execute(stmt) + + rowcnt = 0 + try: + for params in seq_params: + self.execute(operation, params) + if self.with_rows and self._have_unread_result(): + self.fetchall() + rowcnt += self._rowcount + except (ValueError, TypeError) as err: + raise InterfaceError(f"Failed executing the operation; {err}") from None + self._rowcount = rowcnt + return None + + def stored_results(self) -> Iterator[MySQLCursor]: + """Returns an iterator for stored results + + This method returns an iterator over results which are stored when + callproc() is called. The iterator will provide MySQLCursorBuffered + instances. + + Returns a iterator. + """ + return iter(self._stored_results) + + def callproc( + self, + procname: str, + args: Sequence[Any] = (), + ) -> Optional[Union[Dict[str, ToPythonOutputTypes], RowType]]: + """Calls a stored procedure with the given arguments + + The arguments will be set during this session, meaning + they will be called like ___arg where + is an enumeration (+1) of the arguments. + + Coding Example: + 1) Defining the Stored Routine in MySQL: + CREATE PROCEDURE multiply(IN pFac1 INT, IN pFac2 INT, OUT pProd INT) + BEGIN + SET pProd := pFac1 * pFac2; + END + + 2) Executing in Python: + args = (5, 5, 0) # 0 is to hold pprod + cursor.callproc('multiply', args) + print(cursor.fetchone()) + + For OUT and INOUT parameters the user should provide the + type of the parameter as well. The argument should be a + tuple with first item as the value of the parameter to pass + and second argument the type of the argument. + + In the above example, one can call callproc method like: + args = (5, 5, (0, 'INT')) + cursor.callproc('multiply', args) + + The type of the argument given in the tuple will be used by + the MySQL CAST function to convert the values in the corresponding + MySQL type (See CAST in MySQL Reference for more information) + + Does not return a value, but a result set will be + available when the CALL-statement execute successfully. + Raises exceptions when something is wrong. + """ + if not procname or not isinstance(procname, str): + raise ValueError("procname must be a string") + + if not isinstance(args, (tuple, list)): + raise ValueError("args must be a sequence") + + argfmt = "@_{name}_arg{index}" + self._stored_results = [] + + results = [] + try: + argnames = [] + argtypes = [] + + # MySQL itself does support calling procedures with their full + # name .. It's necessary to split + # by '.' and grab the procedure name from procname. + procname_abs = procname.split(".")[-1] + if args: + argvalues = [] + for idx, arg in enumerate(args): + argname = argfmt.format(name=procname_abs, index=idx + 1) + argnames.append(argname) + if isinstance(arg, tuple): + argtypes.append(f" CAST({argname} AS {arg[1]})") + argvalues.append(arg[0]) + else: + argtypes.append(argname) + argvalues.append(arg) + + placeholders = ",".join(f"{arg}=%s" for arg in argnames) + self.execute(f"SET {placeholders}", argvalues) + + call = f"CALL {procname}({','.join(argnames)})" + + # We disable consuming results temporary to make sure we + # getting all results + can_consume_results = self._connection.can_consume_results + for result in self._connection.cmd_query_iter(call): + self._connection.can_consume_results = False + if isinstance(self, (MySQLCursorDict, MySQLCursorBufferedDict)): + cursor_class = MySQLCursorBufferedDict + elif isinstance( + self, + (MySQLCursorNamedTuple, MySQLCursorBufferedNamedTuple), + ): + cursor_class = MySQLCursorBufferedNamedTuple + elif self._raw: + cursor_class = MySQLCursorBufferedRaw + else: + cursor_class = MySQLCursorBuffered + # pylint: disable=protected-access + cur = cursor_class(self._connection.get_self()) + cur._executed = f"(a result of {call})" + cur._handle_result(result) + # pylint: enable=protected-access + if cur.warnings is not None: + self._warnings = cur.warnings + if "columns" in result: + results.append(cur) + self._connection.can_consume_results = can_consume_results + + if argnames: + # Create names aliases to be compatible with namedtuples + args = [ + f"{name} AS {alias}" + for name, alias in zip( + argtypes, [arg.lstrip("@_") for arg in argnames] + ) + ] + select = f"SELECT {','.join(args)}" + self.execute(select) + self._stored_results = results + return self.fetchone() + + self._stored_results = results + return tuple() + + except Error: + raise + except Exception as err: + raise InterfaceError(f"Failed calling stored routine; {err}") from None + + def getlastrowid(self) -> Optional[int]: + """Returns the value generated for an AUTO_INCREMENT column + + Returns the value generated for an AUTO_INCREMENT column by + the previous INSERT or UPDATE statement. + + Returns a long value or None. + """ + return self._last_insert_id + + def _fetch_warnings(self) -> Optional[List[WarningType]]: + """ + Fetch warnings doing a SHOW WARNINGS. Can be called after getting + the result. + + Returns a result set or None when there were no warnings. + """ + res = [] + try: + cur = self._connection.cursor(raw=False) + cur.execute("SHOW WARNINGS") + res = cur.fetchall() + cur.close() + except Exception as err: + raise InterfaceError(f"Failed getting warnings; {err}") from None + + if res: + return res + + return None + + def _handle_warnings(self) -> None: + """Handle possible warnings after all results are consumed. + + Raises: + Error: Also raises exceptions if raise_on_warnings is set. + """ + if self._connection.get_warnings and self._warning_count: + self._warnings = self._fetch_warnings() + + if not self._warnings: + return + + err = get_mysql_exception( + self._warnings[0][1], + self._warnings[0][2], + warning=not self._connection.raise_on_warnings, + ) + + if self._connection.raise_on_warnings: + raise err + + warnings.warn(err, stacklevel=4) + + def _handle_eof(self, eof: EofPacketType) -> None: + """Handle EOF packet""" + self._connection.unread_result = False + self._nextrow = (None, None) + self._warning_count = eof["warning_count"] + self._handle_warnings() + + def _fetch_row(self, raw: bool = False) -> Optional[RowType]: + """Returns the next row in the result set + + Returns a tuple or None. + """ + if not self._have_unread_result(): + return None + row = None + + if self._nextrow == (None, None): + (row, eof) = self._connection.get_row( + binary=self._binary, columns=self.description, raw=raw + ) + else: + (row, eof) = self._nextrow + + if row: + self._nextrow = self._connection.get_row( + binary=self._binary, columns=self.description, raw=raw + ) + eof = self._nextrow[1] + if eof is not None: + self._handle_eof(eof) + if self._rowcount == -1: + self._rowcount = 1 + else: + self._rowcount += 1 + if eof: + self._handle_eof(eof) + + return row + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + self._check_executed() + return self._fetch_row() + + def fetchmany(self, size: Optional[int] = None) -> List[RowType]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set. + """ + self._check_executed() + res = [] + cnt = size or self.arraysize + while cnt > 0 and self._have_unread_result(): + cnt -= 1 + row = self.fetchone() + if row: + res.append(row) + return res + + def fetchall(self) -> List[RowType]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + self._check_executed() + if not self._have_unread_result(): + return [] + + (rows, eof) = self._connection.get_rows() + if self._nextrow[0]: + rows.insert(0, self._nextrow[0]) + + self._handle_eof(eof) + rowcount = len(rows) + if rowcount >= 0 and self._rowcount == -1: + self._rowcount = 0 + self._rowcount += rowcount + return rows + + @property + def column_names(self) -> Tuple[str, ...]: + """Returns column names + + This property returns the columns names as a tuple. + + Returns a tuple. + """ + if not self.description: + return tuple() + return tuple(d[0] for d in self.description) + + @property + def statement(self) -> Optional[str]: + """Returns the executed statement + + This property returns the executed statement. When multiple + statements were executed, the current statement in the iterator + will be returned. + """ + if self._executed is None: + return None + try: + return self._executed.strip().decode("utf-8") # type: ignore[union-attr] + except (AttributeError, UnicodeDecodeError): + return self._executed.strip() # type: ignore[return-value] + + @property + def with_rows(self) -> bool: + """Returns whether the cursor could have rows returned + + This property returns True when column descriptions are available + and possibly also rows, which will need to be fetched. + + Returns True or False. + """ + if not self.description: + return False + return True + + def __str__(self) -> str: + fmt = "{class_name}: {stmt}" + if self._executed: + try: + executed = self._executed.decode("utf-8") # type: ignore[union-attr] + except AttributeError: + executed = self._executed + if len(executed) > 40: + executed = executed[:40] + ".." + else: + executed = "(Nothing executed yet)" + return fmt.format(class_name=self.__class__.__name__, stmt=executed) + + +class MySQLCursorBuffered(MySQLCursor): + """Cursor which fetches rows within execute()""" + + def __init__( + self, connection: Optional[Type[MySQLConnectionAbstract]] = None + ) -> None: + super().__init__(connection) + self._rows: Optional[List[RowType]] = None + self._next_row: int = 0 + + def _handle_resultset(self) -> None: + (self._rows, eof) = self._connection.get_rows() + self._rowcount = len(self._rows) + self._handle_eof(eof) + self._next_row = 0 + try: + self._connection.unread_result = False + except AttributeError: + pass + + def reset(self, free: bool = True) -> None: + self._rows = None + + def _fetch_row(self, raw: bool = False) -> Optional[RowType]: + row = None + try: + row = self._rows[self._next_row] + except (IndexError, TypeError): + return None + self._next_row += 1 + return row + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + self._check_executed() + return self._fetch_row() + + def fetchall(self) -> List[RowType]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + if self._executed is None or self._rows is None: + raise InterfaceError(ERR_NO_RESULT_TO_FETCH) + res = [] + res = self._rows[self._next_row :] + self._next_row = len(self._rows) + return res + + def fetchmany(self, size: Optional[int] = None) -> List[RowType]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set. + """ + self._check_executed() + res = [] + cnt = size or self.arraysize + while cnt > 0: + cnt -= 1 + row = self.fetchone() + if row: + res.append(row) + + return res + + @property + def with_rows(self) -> bool: + return self._rows is not None + + +class MySQLCursorRaw(MySQLCursor): + """ + Skips conversion from MySQL datatypes to Python types when fetching rows. + """ + + _raw: bool = True + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + self._check_executed() + return self._fetch_row(raw=True) + + def fetchall(self) -> List[RowType]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + self._check_executed() + if not self._have_unread_result(): + return [] + (rows, eof) = self._connection.get_rows(raw=True) + if self._nextrow[0]: + rows.insert(0, self._nextrow[0]) + self._handle_eof(eof) + rowcount = len(rows) + if rowcount >= 0 and self._rowcount == -1: + self._rowcount = 0 + self._rowcount += rowcount + return rows + + +class MySQLCursorBufferedRaw(MySQLCursorBuffered): + """ + Cursor which skips conversion from MySQL datatypes to Python types when + fetching rows and fetches rows within execute(). + """ + + _raw: bool = True + + def _handle_resultset(self) -> None: + (self._rows, eof) = self._connection.get_rows(raw=self._raw) + self._rowcount = len(self._rows) + self._handle_eof(eof) + self._next_row = 0 + try: + self._connection.unread_result = False + except AttributeError: + pass + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + self._check_executed() + return self._fetch_row() + + def fetchall(self) -> List[RowType]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + self._check_executed() + return list(self._rows[self._next_row :]) + + @property + def with_rows(self) -> bool: + return self._rows is not None + + +class MySQLCursorPrepared(MySQLCursor): + """Cursor using MySQL Prepared Statements""" + + def __init__(self, connection: Optional[Type[MySQLConnectionAbstract]] = None): + super().__init__(connection) + self._rows: Optional[List[RowType]] = None + self._next_row: int = 0 + self._prepared: Optional[Dict[str, Union[int, List[DescriptionType]]]] = None + self._binary: bool = True + self._have_result: Optional[bool] = None + self._last_row_sent: bool = False + self._cursor_exists: bool = False + + def reset(self, free: bool = True) -> None: + if self._prepared: + try: + self._connection.cmd_stmt_close(self._prepared["statement_id"]) + except Error: + # We tried to deallocate, but it's OK when we fail. + pass + self._prepared = None + self._last_row_sent = False + self._cursor_exists = False + + def _handle_noresultset(self, res: ResultType) -> None: + self._handle_server_status(res.get("status_flag", res.get("server_status", 0))) + super()._handle_noresultset(res) + + def _handle_server_status(self, flags: int) -> None: + """Check for SERVER_STATUS_CURSOR_EXISTS and + SERVER_STATUS_LAST_ROW_SENT flags set by the server. + """ + self._cursor_exists = flags & ServerFlag.STATUS_CURSOR_EXISTS != 0 + self._last_row_sent = flags & ServerFlag.STATUS_LAST_ROW_SENT != 0 + + def _handle_eof(self, eof: EofPacketType) -> None: + self._handle_server_status(eof.get("status_flag", eof.get("server_status", 0))) + super()._handle_eof(eof) + + def callproc(self, procname: Any, args: Any = ()) -> NoReturn: + """Calls a stored procedue + + Not supported with MySQLCursorPrepared. + """ + raise NotSupportedError() + + def close(self) -> None: + """Close the cursor + + This method will try to deallocate the prepared statement and close + the cursor. + """ + self.reset() + super().close() + + def _row_to_python(self, rowdata: Any, desc: Any = None) -> Any: + """Convert row data from MySQL to Python types + + The conversion is done while reading binary data in the + protocol module. + """ + + def _handle_result(self, result: ResultType) -> None: + """Handle result after execution""" + if isinstance(result, dict): + self._connection.unread_result = False + self._have_result = False + self._handle_noresultset(result) + else: + self._description = result[1] + self._connection.unread_result = True + self._have_result = True + + if "status_flag" in result[2]: # type: ignore[operator] + self._handle_server_status(result[2]["status_flag"]) + elif "server_status" in result[2]: # type: ignore[operator] + self._handle_server_status(result[2]["server_status"]) + + def execute( + self, + operation: StrOrBytes, + params: Optional[ParamsSequenceOrDictType] = None, + multi: bool = False, + ) -> None: # multi is unused + """Prepare and execute a MySQL Prepared Statement + + This method will prepare the given operation and execute it using + the optionally given parameters. + + If the cursor instance already had a prepared statement, it is + first closed. + + Note: argument "multi" is unused. + """ + charset = self._connection.charset + if charset == "utf8mb4": + charset = "utf8" + + if not isinstance(operation, str): + try: + operation = operation.decode(charset) + except UnicodeDecodeError as err: + raise ProgrammingError(str(err)) from err + + if isinstance(params, dict): + replacement_keys = re.findall(RE_SQL_PYTHON_CAPTURE_PARAM_NAME, operation) + try: + # Replace params dict with params tuple in correct order. + params = tuple(params[key] for key in replacement_keys) + except KeyError as err: + raise ProgrammingError( + "Not all placeholders were found in the parameters dict" + ) from err + # Convert %(name)s to ? before sending it to MySQL + operation = re.sub(RE_SQL_PYTHON_REPLACE_PARAM, "?", operation) + + if operation is not self._executed: + if self._prepared: + self._connection.cmd_stmt_close(self._prepared["statement_id"]) + self._executed = operation + + try: + operation = operation.encode(charset) + except UnicodeEncodeError as err: + raise ProgrammingError(str(err)) from err + + if b"%s" in operation: + # Convert %s to ? before sending it to MySQL + operation = re.sub(RE_SQL_FIND_PARAM, b"?", operation) + + try: + self._prepared = self._connection.cmd_stmt_prepare(operation) + except Error: + self._executed = None + raise + + self._connection.cmd_stmt_reset(self._prepared["statement_id"]) + + if self._prepared["parameters"] and not params: + return + if params: + if not isinstance(params, (tuple, list)): + raise ProgrammingError( + errno=1210, + msg=f"Incorrect type of argument: {type(params).__name__}({params})" + ", it must be of type tuple or list the argument given to " + "the prepared statement", + ) + if len(self._prepared["parameters"]) != len(params): + raise ProgrammingError( + errno=1210, + msg="Incorrect number of arguments executing prepared statement", + ) + + if params is None: + params = () + res = self._connection.cmd_stmt_execute( + self._prepared["statement_id"], + data=params, + parameters=self._prepared["parameters"], + ) + self._handle_result(res) + + def executemany( + self, + operation: str, + seq_params: Sequence[ParamsSequenceType], + ) -> None: + """Prepare and execute a MySQL Prepared Statement many times + + This method will prepare the given operation and execute with each + tuple found the list seq_params. + + If the cursor instance already had a prepared statement, it is + first closed. + + executemany() simply calls execute(). + """ + rowcnt = 0 + try: + for params in seq_params: + self.execute(operation, params) + if self.with_rows and self._have_unread_result(): + self.fetchall() + rowcnt += self._rowcount + except (ValueError, TypeError) as err: + raise InterfaceError(f"Failed executing the operation; {err}") from None + self._rowcount = rowcnt + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + self._check_executed() + if self._cursor_exists: + self._connection.cmd_stmt_fetch(self._prepared["statement_id"]) + return self._fetch_row() or None + + def fetchmany(self, size: Optional[int] = None) -> List[RowType]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set. + """ + self._check_executed() + res = [] + cnt = size or self.arraysize + while cnt > 0 and self._have_unread_result(): + cnt -= 1 + row = self._fetch_row() + if row: + res.append(row) + return res + + def fetchall(self) -> List[RowType]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + self._check_executed() + rows = [] + if self._nextrow[0]: + rows.append(self._nextrow[0]) + while self._have_unread_result(): + if self._cursor_exists: + self._connection.cmd_stmt_fetch( + self._prepared["statement_id"], MAX_RESULTS + ) + (tmp, eof) = self._connection.get_rows( + binary=self._binary, columns=self.description + ) + rows.extend(tmp) + self._handle_eof(eof) + self._rowcount = len(rows) + return rows + + +class MySQLCursorDict(MySQLCursor): + """ + Cursor fetching rows as dictionaries. + + The fetch methods of this class will return dictionaries instead of tuples. + Each row is a dictionary that looks like: + row = { + "col1": value1, + "col2": value2 + } + """ + + def _row_to_python( + self, + rowdata: RowType, + desc: Optional[List[DescriptionType]] = None, # pylint: disable=unused-argument + ) -> Optional[Dict[str, ToPythonOutputTypes]]: + """Convert a MySQL text result row to Python types + + Returns a dictionary. + """ + return dict(zip(self.column_names, rowdata)) if rowdata else None + + def fetchone(self) -> Optional[Dict[str, ToPythonOutputTypes]]: + """Return next row of a query result set. + + Returns: + dict or None: A dict from query result set. + """ + return self._row_to_python(super().fetchone(), self.description) + + def fetchall(self) -> List[Optional[Dict[str, ToPythonOutputTypes]]]: + """Return all rows of a query result set. + + Returns: + list: A list of dictionaries with all rows of a query + result set where column names are used as keys. + """ + return [ + self._row_to_python(row, self.description) + for row in super().fetchall() + if row + ] + + +class MySQLCursorNamedTuple(MySQLCursor): + """ + Cursor fetching rows as named tuple. + + The fetch methods of this class will return namedtuples instead of tuples. + Each row is returned as a namedtuple and the values can be accessed as: + row.col1, row.col2 + """ + + def _row_to_python( + self, + rowdata: RowType, + desc: Optional[List[DescriptionType]] = None, # pylint: disable=unused-argument + ) -> Optional[RowType]: + """Convert a MySQL text result row to Python types + + Returns a named tuple. + """ + row = rowdata + + if row: + columns = tuple(self.column_names) + try: + named_tuple = NAMED_TUPLE_CACHE[columns] + except KeyError: + named_tuple = namedtuple("Row", columns) # type:ignore[no-redef, misc] + NAMED_TUPLE_CACHE[columns] = named_tuple + return named_tuple(*row) + return None + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + row = super().fetchone() + if not row: + return None + return ( + self._row_to_python(row, self.description) + if hasattr(self._connection, "converter") + else row + ) + + def fetchall(self) -> List[Optional[RowType]]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + return [ + self._row_to_python(row, self.description) + for row in super().fetchall() + if row + ] + + +class MySQLCursorBufferedDict(MySQLCursorDict, MySQLCursorBuffered): + """ + Buffered Cursor fetching rows as dictionaries. + """ + + def fetchone(self) -> Optional[Dict[str, ToPythonOutputTypes]]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + self._check_executed() + row = self._fetch_row() + if row: + return self._row_to_python(row, self.description) + return None + + def fetchall(self) -> List[Optional[Dict[str, ToPythonOutputTypes]]]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + if self._executed is None or self._rows is None: + raise InterfaceError(ERR_NO_RESULT_TO_FETCH) + res = [] + for row in self._rows[self._next_row :]: + res.append(self._row_to_python(row, self.description)) + self._next_row = len(self._rows) + return res + + +class MySQLCursorBufferedNamedTuple(MySQLCursorNamedTuple, MySQLCursorBuffered): + """ + Buffered Cursor fetching rows as named tuple. + """ + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + self._check_executed() + row = self._fetch_row() + if row: + return self._row_to_python(row, self.description) + return None + + def fetchall(self) -> List[Optional[RowType]]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + if self._executed is None or self._rows is None: + raise InterfaceError(ERR_NO_RESULT_TO_FETCH) + res = [] + for row in self._rows[self._next_row :]: + res.append(self._row_to_python(row, self.description)) + self._next_row = len(self._rows) + return res + + +class MySQLCursorPreparedDict(MySQLCursorDict, MySQLCursorPrepared): # type: ignore[misc] + """ + This class is a blend of features from MySQLCursorDict and MySQLCursorPrepared + + Multiple inheritance in python is allowed but care must be taken + when assuming methods resolution. In the case of multiple + inheritance, a given attribute is first searched in the current + class if it's not found then it's searched in the parent classes. + The parent classes are searched in a left-right fashion and each + class is searched once. + Based on python's attribute resolution, in this case, attributes + are searched as follows: + 1. MySQLCursorPreparedDict (current class) + 2. MySQLCursorDict (left parent class) + 3. MySQLCursorPrepared (right parent class) + 4. MySQLCursor (base class) + """ + + def fetchmany( + self, size: Optional[int] = None + ) -> List[Dict[str, ToPythonOutputTypes]]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set represented + as a list of dictionaries where column names are used as keys. + """ + return [ + self._row_to_python(row, self.description) + for row in super().fetchmany(size=size) + if row + ] + + +class MySQLCursorPreparedNamedTuple(MySQLCursorNamedTuple, MySQLCursorPrepared): + """ + This class is a blend of features from MySQLCursorNamedTuple and MySQLCursorPrepared + """ + + def fetchmany(self, size: Optional[int] = None) -> List[RowType]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set represented + as a list of named tuples where column names are used as names. + """ + return [ + self._row_to_python(row, self.description) + for row in super().fetchmany(size=size) + if row + ] + + +class MySQLCursorPreparedRaw(MySQLCursorPrepared): + """ + This class is a blend of features from MySQLCursorRaw and MySQLCursorPrepared + """ + + _raw: bool = True + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + self._check_executed() + if self._cursor_exists: + self._connection.cmd_stmt_fetch(self._prepared["statement_id"]) + return self._fetch_row(raw=self._raw) or None + + def fetchmany(self, size: Optional[int] = None) -> List[RowType]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set. + """ + self._check_executed() + res = [] + cnt = size or self.arraysize + while cnt > 0 and self._have_unread_result(): + cnt -= 1 + row = self._fetch_row(raw=self._raw) + if row: + res.append(row) + return res + + def fetchall(self) -> List[RowType]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + self._check_executed() + rows = [] + if self._nextrow[0]: + rows.append(self._nextrow[0]) + while self._have_unread_result(): + if self._cursor_exists: + self._connection.cmd_stmt_fetch( + self._prepared["statement_id"], MAX_RESULTS + ) + (tmp, eof) = self._connection.get_rows( + raw=self._raw, binary=self._binary, columns=self.description + ) + rows.extend(tmp) + self._handle_eof(eof) + self._rowcount = len(rows) + return rows diff --git a/mysql/connector/cursor_cext.py b/mysql/connector/cursor_cext.py new file mode 100644 index 0000000..33295cf --- /dev/null +++ b/mysql/connector/cursor_cext.py @@ -0,0 +1,1288 @@ +# Copyright (c) 2014, 2023, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# mypy: disable-error-code="assignment,arg-type,override,union-attr" + +"""Cursor classes using the C Extension.""" +from __future__ import annotations + +import re +import warnings +import weakref + +from collections import namedtuple +from typing import ( + Any, + Dict, + Generator, + Iterator, + List, + NoReturn, + Optional, + Sequence, + Tuple, + Type, + Union, +) +from weakref import CallableProxyType + +# pylint: disable=import-error,no-name-in-module +from _mysql_connector import MySQLInterfaceError, MySQLPrepStmt + +from .types import ( + CextEofPacketType, + CextResultType, + DescriptionType, + ParamsSequenceOrDictType, + ParamsSequenceType, + RowType, + StrOrBytes, + ToPythonOutputTypes, + WarningType, +) + +# pylint: enable=import-error,no-name-in-module +# isort: split + +from .abstracts import NAMED_TUPLE_CACHE, MySQLConnectionAbstract, MySQLCursorAbstract +from .cursor import ( + RE_PY_PARAM, + RE_SQL_COMMENT, + RE_SQL_FIND_PARAM, + RE_SQL_INSERT_STMT, + RE_SQL_INSERT_VALUES, + RE_SQL_ON_DUPLICATE, + RE_SQL_PYTHON_CAPTURE_PARAM_NAME, + RE_SQL_PYTHON_REPLACE_PARAM, + RE_SQL_SPLIT_STMTS, +) +from .errorcode import CR_NO_RESULT_SET +from .errors import ( + Error, + InterfaceError, + NotSupportedError, + ProgrammingError, + get_mysql_exception, +) + +ERR_NO_RESULT_TO_FETCH = "No result set to fetch from" + + +class _ParamSubstitutor: + + """ + Substitutes parameters into SQL statement. + """ + + def __init__(self, params: Sequence[bytes]) -> None: + self.params: Sequence[bytes] = params + self.index: int = 0 + + def __call__(self, matchobj: object) -> bytes: + index = self.index + self.index += 1 + try: + return self.params[index] + except IndexError: + raise ProgrammingError( + "Not enough parameters for the SQL statement" + ) from None + + @property + def remaining(self) -> int: + """Returns number of parameters remaining to be substituted""" + return len(self.params) - self.index + + +class CMySQLCursor(MySQLCursorAbstract): + + """Default cursor for interacting with MySQL using C Extension""" + + _raw: bool = False + _buffered: bool = False + _raw_as_string: bool = False + + def __init__(self, connection: Type[MySQLConnectionAbstract]) -> None: + """Initialize""" + MySQLCursorAbstract.__init__(self) + + self._affected_rows: int = -1 + self._rowcount: int = -1 + self._nextrow: Tuple[Optional[RowType], Optional[CextEofPacketType]] = ( + None, + None, + ) + + if not isinstance(connection, MySQLConnectionAbstract): + raise InterfaceError(errno=2048) + self._cnx: CallableProxyType[Type[MySQLConnectionAbstract]] = weakref.proxy( + connection + ) + + def reset(self, free: bool = True) -> None: + """Reset the cursor + + When free is True (default) the result will be freed. + """ + self._rowcount = -1 + self._nextrow = None + self._affected_rows = -1 + self._last_insert_id: int = 0 + self._warning_count: int = 0 + self._warnings: Optional[List[WarningType]] = None + self._warnings = None + self._warning_count = 0 + self._description: Optional[List[DescriptionType]] = None + self._executed_list: List[StrOrBytes] = [] + if free and self._cnx: + self._cnx.free_result() + super().reset() + + def _check_executed(self) -> None: + """Check if the statement has been executed. + + Raises an error if the statement has not been executed. + """ + if self._executed is None: + raise InterfaceError(ERR_NO_RESULT_TO_FETCH) + + def _fetch_warnings(self) -> Optional[List[WarningType]]: + """Fetch warnings + + Fetch warnings doing a SHOW WARNINGS. Can be called after getting + the result. + + Returns a result set or None when there were no warnings. + + Raises Error (or subclass) on errors. + + Returns list of tuples or None. + """ + warns = [] + try: + # force freeing result + self._cnx.consume_results() + _ = self._cnx.cmd_query("SHOW WARNINGS") + warns = self._cnx.get_rows()[0] + self._cnx.consume_results() + except MySQLInterfaceError as err: + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + except Exception as err: + raise InterfaceError(f"Failed getting warnings; {err}") from None + + if warns: + return warns + + return None + + def _handle_warnings(self) -> None: + """Handle possible warnings after all results are consumed. + + Raises: + Error: Also raises exceptions if raise_on_warnings is set. + """ + if self._cnx.get_warnings and self._warning_count: + self._warnings = self._fetch_warnings() + + if not self._warnings: + return + + err = get_mysql_exception( + *self._warnings[0][1:3], warning=not self._cnx.raise_on_warnings + ) + if self._cnx.raise_on_warnings: + raise err + + warnings.warn(str(err), stacklevel=4) + + def _handle_result(self, result: Union[CextEofPacketType, CextResultType]) -> None: + """Handles the result after statement execution""" + if "columns" in result: + self._description = result["columns"] + self._rowcount = 0 + self._handle_resultset() + else: + self._last_insert_id = result["insert_id"] + self._warning_count = result["warning_count"] + self._affected_rows = result["affected_rows"] + self._rowcount = -1 + self._handle_warnings() + + def _handle_resultset(self) -> None: + """Handle a result set""" + + def _handle_eof(self) -> None: + """Handle end of reading the result + + Raises an Error on errors. + """ + self._warning_count = self._cnx.warning_count + self._handle_warnings() + if not self._cnx.more_results: + self._cnx.free_result() + + def _execute_iter(self) -> Generator[CMySQLCursor, None, None]: + """Generator returns MySQLCursor objects for multiple statements + + Deprecated: use nextset() method directly. + + This method is only used when multiple statements are executed + by the execute() method. It uses zip() to make an iterator from the + given query_iter (result of MySQLConnection.cmd_query_iter()) and + the list of statements that were executed. + """ + executed_list = RE_SQL_SPLIT_STMTS.split(self._executed) + i = 0 + self._executed = executed_list[i] + yield self + + while True: + try: + if not self.nextset(): + raise StopIteration + except InterfaceError as err: + # Result without result set + if err.errno != CR_NO_RESULT_SET: + raise + except StopIteration: + return + i += 1 + try: + self._executed = executed_list[i].strip() + except IndexError: + self._executed = executed_list[0] + yield self + return + + def execute( + self, + operation: StrOrBytes, + params: ParamsSequenceOrDictType = (), + multi: bool = False, + ) -> Optional[Generator[CMySQLCursor, None, None]]: + """Execute given statement using given parameters + + Deprecated: The multi argument is not needed and nextset() should + be used to handle multiple result sets. + """ + if not operation: + return None + + try: + if not self._cnx or self._cnx.is_closed(): + raise ProgrammingError + except (ProgrammingError, ReferenceError) as err: + raise ProgrammingError("Cursor is not connected", 2055) from err + self._cnx.handle_unread_result() + + stmt = "" + self.reset() + + try: + if isinstance(operation, str): + stmt = operation.encode(self._cnx.python_charset) + else: + stmt = operation + except (UnicodeDecodeError, UnicodeEncodeError) as err: + raise ProgrammingError(str(err)) from err + + if params: + prepared = self._cnx.prepare_for_mysql(params) + if isinstance(prepared, dict): + for key, value in prepared.items(): + stmt = stmt.replace(f"%({key})s".encode(), value) + elif isinstance(prepared, (list, tuple)): + psub = _ParamSubstitutor(prepared) + stmt = RE_PY_PARAM.sub(psub, stmt) # type: ignore[call-overload] + if psub.remaining != 0: + raise ProgrammingError( + "Not all parameters were used in the SQL statement" + ) + + try: + result = self._cnx.cmd_query( + stmt, + raw=self._raw, + buffered=self._buffered, + raw_as_string=self._raw_as_string, + ) + except MySQLInterfaceError as err: + raise get_mysql_exception( + msg=err.msg, errno=err.errno, sqlstate=err.sqlstate + ) from err + + self._executed = stmt + self._handle_result(result) + + if multi: + return self._execute_iter() + + return None + + def _batch_insert( + self, + operation: str, + seq_params: Sequence[ParamsSequenceOrDictType], + ) -> Optional[bytes]: + """Implements multi row insert""" + + def remove_comments(match: re.Match) -> str: + """Remove comments from INSERT statements. + + This function is used while removing comments from INSERT + statements. If the matched string is a comment not enclosed + by quotes, it returns an empty string, else the string itself. + """ + if match.group(1): + return "" + return match.group(2) + + tmp = re.sub( + RE_SQL_ON_DUPLICATE, + "", + re.sub(RE_SQL_COMMENT, remove_comments, operation), + ) + + matches = re.search(RE_SQL_INSERT_VALUES, tmp) + if not matches: + raise InterfaceError( + "Failed rewriting statement for multi-row INSERT. Check SQL syntax" + ) + fmt = matches.group(1).encode(self._cnx.python_charset) + values = [] + + try: + stmt = operation.encode(self._cnx.python_charset) + for params in seq_params: + tmp = fmt + prepared = self._cnx.prepare_for_mysql(params) + if isinstance(prepared, dict): + for key, value in prepared.items(): + tmp = tmp.replace(f"%({key})s".encode(), value) + elif isinstance(prepared, (list, tuple)): + psub = _ParamSubstitutor(prepared) + tmp = RE_PY_PARAM.sub(psub, tmp) # type: ignore[call-overload] + if psub.remaining != 0: + raise ProgrammingError( + "Not all parameters were used in the SQL statement" + ) + values.append(tmp) + + if fmt in stmt: + stmt = stmt.replace(fmt, b",".join(values), 1) + self._executed = stmt + return stmt + return None + except (UnicodeDecodeError, UnicodeEncodeError) as err: + raise ProgrammingError(str(err)) from err + except Exception as err: + raise InterfaceError(f"Failed executing the operation; {err}") from None + + def executemany( + self, + operation: str, + seq_params: Sequence[ParamsSequenceOrDictType], + ) -> Optional[Generator[CMySQLCursor, None, None]]: + """Execute the given operation multiple times + + The executemany() method will execute the operation iterating + over the list of parameters in seq_params. + + Example: Inserting 3 new employees and their phone number + + data = [ + ('Jane','555-001'), + ('Joe', '555-001'), + ('John', '555-003') + ] + stmt = "INSERT INTO employees (name, phone) VALUES ('%s','%s)" + cursor.executemany(stmt, data) + + INSERT statements are optimized by batching the data, that is + using the MySQL multiple rows syntax. + + Results are discarded! If they are needed, consider looping over + data using the execute() method. + """ + if not operation or not seq_params: + return None + + try: + if not self._cnx: + raise ProgrammingError + except (ProgrammingError, ReferenceError) as err: + raise ProgrammingError("Cursor is not connected") from err + self._cnx.handle_unread_result() + + if not isinstance(seq_params, (list, tuple)): + raise ProgrammingError("Parameters for query must be list or tuple.") + + # Optimize INSERTs by batching them + if re.match(RE_SQL_INSERT_STMT, operation): + if not seq_params: + self._rowcount = 0 + return None + stmt = self._batch_insert(operation, seq_params) + if stmt is not None: + self._executed = stmt + return self.execute(stmt) + + rowcnt = 0 + try: + # When processing read ops (e.g., SELECT), rowcnt is updated + # based on self._rowcount. For write ops (e.g., INSERT) is + # updated based on self._affected_rows. + # The variable self._description is None for write ops, that's + # why we use it as indicator for updating rowcnt. + for params in seq_params: + self.execute(operation, params) + if self.with_rows and self._cnx.unread_result: + self.fetchall() + rowcnt += self._rowcount if self.description else self._affected_rows + except (ValueError, TypeError) as err: + raise InterfaceError(f"Failed executing the operation; {err}") from None + + self._rowcount = rowcnt + return None + + @property + def description(self) -> Optional[List[DescriptionType]]: + """Returns description of columns in a result""" + return self._description + + @property + def rowcount(self) -> int: + """Returns the number of rows produced or affected""" + if self._rowcount == -1: + return self._affected_rows + return self._rowcount + + def close(self) -> bool: + """Close the cursor + + The result will be freed. + """ + if not self._cnx: + return False + + self._cnx.handle_unread_result() + self._warnings = None + self._cnx = None + return True + + def callproc( + self, + procname: str, + args: Sequence[Any] = (), + ) -> Optional[Union[Dict[str, ToPythonOutputTypes], RowType]]: + """Calls a stored procedure with the given arguments""" + if not procname or not isinstance(procname, str): + raise ValueError("procname must be a string") + + if not isinstance(args, (tuple, list)): + raise ValueError("args must be a sequence") + + argfmt = "@_{name}_arg{index}" + self._stored_results = [] + + try: + argnames = [] + argtypes = [] + + # MySQL itself does support calling procedures with their full + # name .. It's necessary to split + # by '.' and grab the procedure name from procname. + procname_abs = procname.split(".")[-1] + if args: + argvalues = [] + for idx, arg in enumerate(args): + argname = argfmt.format(name=procname_abs, index=idx + 1) + argnames.append(argname) + if isinstance(arg, tuple): + argtypes.append(f" CAST({argname} AS {arg[1]})") + argvalues.append(arg[0]) + else: + argtypes.append(argname) + argvalues.append(arg) + + placeholders = ",".join(f"{arg}=%s" for arg in argnames) + self.execute(f"SET {placeholders}", argvalues) + + call = f"CALL {procname}({','.join(argnames)})" + + result = self._cnx.cmd_query( + call, raw=self._raw, raw_as_string=self._raw_as_string + ) + + results = [] + while self._cnx.result_set_available: + result = self._cnx.fetch_eof_columns() + if isinstance(self, (CMySQLCursorDict, CMySQLCursorBufferedDict)): + cursor_class = CMySQLCursorBufferedDict + elif isinstance( + self, + (CMySQLCursorNamedTuple, CMySQLCursorBufferedNamedTuple), + ): + cursor_class = CMySQLCursorBufferedNamedTuple + elif self._raw: + cursor_class = CMySQLCursorBufferedRaw + else: + cursor_class = CMySQLCursorBuffered + # pylint: disable=protected-access + cur = cursor_class(self._cnx.get_self()) + cur._executed = f"(a result of {call})" + cur._handle_result(result) + # pylint: enable=protected-access + results.append(cur) + self._cnx.next_result() + self._stored_results = results + self._handle_eof() + + if argnames: + self.reset() + # Create names aliases to be compatible with namedtuples + args = [ + f"{name} AS {alias}" + for name, alias in zip( + argtypes, [arg.lstrip("@_") for arg in argnames] + ) + ] + select = f"SELECT {','.join(args)}" + self.execute(select) + + return self.fetchone() + return tuple() + + except Error: + raise + except Exception as err: + raise InterfaceError(f"Failed calling stored routine; {err}") from None + + def nextset(self) -> Optional[bool]: + """Skip to the next available result set""" + if not self._cnx.next_result(): + self.reset(free=True) + return None + self.reset(free=False) + + if not self._cnx.result_set_available: + eof = self._cnx.fetch_eof_status() + self._handle_result(eof) + raise InterfaceError(errno=CR_NO_RESULT_SET) + + self._handle_result(self._cnx.fetch_eof_columns()) + return True + + def fetchall(self) -> List[RowType]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + self._check_executed() + if not self._cnx.unread_result: + return [] + + rows: Tuple[List[RowType], Optional[CextEofPacketType]] = self._cnx.get_rows() + if self._nextrow and self._nextrow[0]: + rows[0].insert(0, self._nextrow[0]) + + if not rows[0]: + self._handle_eof() + return [] + + self._rowcount += len(rows[0]) + self._handle_eof() + # self._cnx.handle_unread_result() + return rows[0] + + def fetchmany(self, size: int = 1) -> List[RowType]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set. + """ + self._check_executed() + if self._nextrow and self._nextrow[0]: + rows = [self._nextrow[0]] + size -= 1 + else: + rows = [] + + if size and self._cnx.unread_result: + rows.extend(self._cnx.get_rows(size)[0]) + + if size: + if self._cnx.unread_result: + self._nextrow = self._cnx.get_row() + if ( + self._nextrow + and not self._nextrow[0] + and not self._cnx.more_results + ): + self._cnx.free_result() + else: + self._nextrow = (None, None) + + if not rows: + self._handle_eof() + return [] + + self._rowcount += len(rows) + return rows + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + self._check_executed() + row = self._nextrow + if not row and self._cnx.unread_result: + row = self._cnx.get_row() + + if row and row[0]: + self._nextrow = self._cnx.get_row() + if not self._nextrow[0] and not self._cnx.more_results: + self._cnx.free_result() + else: + self._handle_eof() + return None + self._rowcount += 1 + return row[0] + + def __iter__(self) -> Iterator[RowType]: + """Iteration over the result set + + Iteration over the result set which calls self.fetchone() + and returns the next row. + """ + return iter(self.fetchone, None) + + def stored_results(self) -> Generator[CMySQLCursor, None, None]: + """Returns an iterator for stored results + + This method returns an iterator over results which are stored when + callproc() is called. The iterator will provide MySQLCursorBuffered + instances. + + Returns a iterator. + """ + for result in self._stored_results: + yield result + self._stored_results = [] + + def __next__(self) -> RowType: + """Iteration over the result set + Used for iterating over the result set. Calls self.fetchone() + to get the next row. + + Raises StopIteration when no more rows are available. + """ + try: + row = self.fetchone() + except InterfaceError: + raise StopIteration from None + if not row: + raise StopIteration from None + return row + + @property + def column_names(self) -> Tuple[str, ...]: + """Returns column names + + This property returns the columns names as a tuple. + + Returns a tuple. + """ + if not self.description: + return () + return tuple(d[0] for d in self.description) + + @property + def statement(self) -> str: + """Returns the executed statement + + This property returns the executed statement. When multiple + statements were executed, the current statement in the iterator + will be returned. + """ + try: + return self._executed.strip().decode("utf8") + except AttributeError: + return self._executed.strip() # type: ignore[return-value] + + @property + def with_rows(self) -> bool: + """Returns whether the cursor could have rows returned + + This property returns True when column descriptions are available + and possibly also rows, which will need to be fetched. + + Returns True or False. + """ + if self.description: + return True + return False + + def __str__(self) -> str: + fmt = "{class_name}: {stmt}" + if self._executed: + try: + executed = self._executed.decode("utf-8") + except AttributeError: + executed = self._executed + if len(executed) > 40: + executed = executed[:40] + ".." + else: + executed = "(Nothing executed yet)" + + return fmt.format(class_name=self.__class__.__name__, stmt=executed) + + +class CMySQLCursorBuffered(CMySQLCursor): + + """Cursor using C Extension buffering results""" + + def __init__(self, connection: Type[MySQLConnectionAbstract]): + """Initialize""" + super().__init__(connection) + + self._rows: Optional[List[RowType]] = None + self._next_row: int = 0 + + def _handle_resultset(self) -> None: + """Handle a result set""" + self._rows = self._cnx.get_rows()[0] + self._next_row = 0 + self._rowcount: int = len(self._rows) + self._handle_eof() + + def reset(self, free: bool = True) -> None: + """Reset the cursor to default""" + self._rows = None + self._next_row = 0 + super().reset(free=free) + + def _fetch_row(self) -> Optional[RowType]: + """Returns the next row in the result set + + Returns a tuple or None. + """ + row = None + try: + row = self._rows[self._next_row] + except IndexError: + return None + self._next_row += 1 + return row + + def fetchall(self) -> List[RowType]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + self._check_executed() + res = self._rows[self._next_row :] + self._next_row = len(self._rows) + return res + + def fetchmany(self, size: int = 1) -> List[RowType]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set. + """ + self._check_executed() + res = [] + cnt = size or self.arraysize + while cnt > 0: + cnt -= 1 + row = self._fetch_row() + if row: + res.append(row) + else: + break + return res + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + self._check_executed() + return self._fetch_row() + + @property + def with_rows(self) -> bool: + """Returns whether the cursor could have rows returned + + This property returns True when rows are available, + which will need to be fetched. + + Returns True or False. + """ + return self._rows is not None + + +class CMySQLCursorRaw(CMySQLCursor): + """Cursor using C Extension return raw results""" + + _raw: bool = True + + +class CMySQLCursorBufferedRaw(CMySQLCursorBuffered): + """Cursor using C Extension buffering raw results""" + + _raw: bool = True + + +class CMySQLCursorDict(CMySQLCursor): + """Cursor using C Extension returning rows as dictionaries""" + + _raw: bool = False + + def fetchone(self) -> Optional[Dict[str, ToPythonOutputTypes]]: + """Return next row of a query result set. + + Returns: + dict or None: A dict from query result set. + """ + row = super().fetchone() + return dict(zip(self.column_names, row)) if row else None + + def fetchmany(self, size: int = 1) -> List[Dict[str, ToPythonOutputTypes]]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set represented + as a list of dictionaries where column names are used as keys. + """ + res = super().fetchmany(size=size) + return [dict(zip(self.column_names, row)) for row in res] + + def fetchall(self) -> List[Dict[str, ToPythonOutputTypes]]: + """Return all rows of a query result set. + + Returns: + list: A list of dictionaries with all rows of a query + result set where column names are used as keys. + """ + res = super().fetchall() + return [dict(zip(self.column_names, row)) for row in res] + + +class CMySQLCursorBufferedDict(CMySQLCursorBuffered): + """Cursor using C Extension buffering and returning rows as dictionaries""" + + _raw = False + + def _fetch_row(self) -> Optional[Dict[str, ToPythonOutputTypes]]: + row = super()._fetch_row() + if row: + return dict(zip(self.column_names, row)) + return None + + def fetchall(self) -> List[Dict[str, ToPythonOutputTypes]]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + res = super().fetchall() + return [dict(zip(self.column_names, row)) for row in res] + + +class CMySQLCursorNamedTuple(CMySQLCursor): + """Cursor using C Extension returning rows as named tuples""" + + named_tuple: Any = None + + def _handle_resultset(self) -> None: + """Handle a result set""" + super()._handle_resultset() + columns = tuple(self.column_names) + try: + self.named_tuple = NAMED_TUPLE_CACHE[columns] + except KeyError: + self.named_tuple = namedtuple("Row", columns) # type: ignore[misc] + NAMED_TUPLE_CACHE[columns] = self.named_tuple + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + row = super().fetchone() + if row: + return self.named_tuple(*row) + return None + + def fetchmany(self, size: int = 1) -> List[RowType]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set. + """ + res = super().fetchmany(size=size) + if not res: + return [] + return [self.named_tuple(*row) for row in res] + + def fetchall(self) -> List[RowType]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + res = super().fetchall() + return [self.named_tuple(*row) for row in res] + + +class CMySQLCursorBufferedNamedTuple(CMySQLCursorBuffered): + """Cursor using C Extension buffering and returning rows as named tuples""" + + named_tuple: Any = None + + def _handle_resultset(self) -> None: + super()._handle_resultset() + self.named_tuple = namedtuple("Row", self.column_names) # type: ignore[misc] + + def _fetch_row(self) -> Optional[RowType]: + row = super()._fetch_row() + if row: + return self.named_tuple(*row) + return None + + def fetchall(self) -> List[RowType]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + res = super().fetchall() + return [self.named_tuple(*row) for row in res] + + +class CMySQLCursorPrepared(CMySQLCursor): + """Cursor using MySQL Prepared Statements""" + + def __init__(self, connection: Type[MySQLConnectionAbstract]): + super().__init__(connection) + self._rows: Optional[List[RowType]] = None + self._rowcount: int = 0 + self._next_row: int = 0 + self._binary: bool = True + self._stmt: Optional[MySQLPrepStmt] = None + + def _handle_eof(self) -> None: + """Handle EOF packet""" + self._nextrow = (None, None) + self._handle_warnings() + + def _fetch_row(self, raw: bool = False) -> Optional[RowType]: + """Returns the next row in the result set + + Returns a tuple or None. + """ + if not self._stmt or not self._stmt.have_result_set: + return None + row = None + + if self._nextrow == (None, None): + (row, eof) = self._cnx.get_row( + binary=self._binary, + columns=self.description, + raw=raw, + prep_stmt=self._stmt, + ) + else: + (row, eof) = self._nextrow + + if row: + self._nextrow = self._cnx.get_row( + binary=self._binary, + columns=self.description, + raw=raw, + prep_stmt=self._stmt, + ) + eof = self._nextrow[1] + if eof is not None: + self._warning_count = eof["warning_count"] + self._handle_eof() + if self._rowcount == -1: + self._rowcount = 1 + else: + self._rowcount += 1 + if eof: + self._warning_count = eof["warning_count"] + self._handle_eof() + + return row + + def callproc(self, procname: Any, args: Any = None) -> NoReturn: + """Calls a stored procedue + + Not supported with CMySQLCursorPrepared. + """ + raise NotSupportedError() + + def close(self) -> None: + """Close the cursor + + This method will try to deallocate the prepared statement and close + the cursor. + """ + if self._stmt: + self.reset() + self._cnx.cmd_stmt_close(self._stmt) + self._stmt = None + super().close() + + def reset(self, free: bool = True) -> None: + """Resets the prepared statement.""" + if self._stmt: + self._cnx.cmd_stmt_reset(self._stmt) + super().reset(free=free) + + def execute( + self, + operation: StrOrBytes, + params: Optional[ParamsSequenceOrDictType] = None, + multi: bool = False, + ) -> None: # multi is unused + """Prepare and execute a MySQL Prepared Statement + + This method will prepare the given operation and execute it using + the given parameters. + + If the cursor instance already had a prepared statement, it is + first closed. + + Note: argument "multi" is unused. + """ + if not operation: + return + + try: + if not self._cnx or self._cnx.is_closed(): + raise ProgrammingError + except (ProgrammingError, ReferenceError) as err: + raise ProgrammingError("Cursor is not connected", 2055) from err + + self._cnx.handle_unread_result(prepared=True) + + charset = self._cnx.charset + if charset == "utf8mb4": + charset = "utf8" + + if not isinstance(operation, str): + try: + operation = operation.decode(charset) + except UnicodeDecodeError as err: + raise ProgrammingError(str(err)) from err + + if isinstance(params, dict): + replacement_keys = re.findall(RE_SQL_PYTHON_CAPTURE_PARAM_NAME, operation) + try: + # Replace params dict with params tuple in correct order. + params = tuple(params[key] for key in replacement_keys) + except KeyError as err: + raise ProgrammingError( + "Not all placeholders were found in the parameters dict" + ) from err + # Convert %(name)s to ? before sending it to MySQL + operation = re.sub(RE_SQL_PYTHON_REPLACE_PARAM, "?", operation) + + if operation is not self._executed: + if self._stmt: + self._cnx.cmd_stmt_close(self._stmt) + self._executed = operation + + try: + operation = operation.encode(charset) + except UnicodeEncodeError as err: + raise ProgrammingError(str(err)) from err + + if b"%s" in operation: + # Convert %s to ? before sending it to MySQL + operation = re.sub(RE_SQL_FIND_PARAM, b"?", operation) + + try: + self._stmt = self._cnx.cmd_stmt_prepare(operation) + except Error: + self._executed = None + self._stmt = None + raise + + self._cnx.cmd_stmt_reset(self._stmt) + + if self._stmt.param_count > 0 and not params: + return + if params: + if not isinstance(params, (tuple, list)): + raise ProgrammingError( + errno=1210, + msg=f"Incorrect type of argument: {type(params).__name__}({params})" + ", it must be of type tuple or list the argument given to " + "the prepared statement", + ) + if self._stmt.param_count != len(params): + raise ProgrammingError( + errno=1210, + msg="Incorrect number of arguments executing prepared statement", + ) + + if params is None: + params = () + res = self._cnx.cmd_stmt_execute(self._stmt, *params) + if res: + self._handle_result(res) + + def executemany( + self, operation: str, seq_params: Sequence[ParamsSequenceType] + ) -> None: + """Prepare and execute a MySQL Prepared Statement many times + + This method will prepare the given operation and execute with each + tuple found the list seq_params. + + If the cursor instance already had a prepared statement, it is + first closed. + """ + rowcnt = 0 + try: + for params in seq_params: + self.execute(operation, params) + if self.with_rows: + self.fetchall() + rowcnt += self._rowcount + except (ValueError, TypeError) as err: + raise InterfaceError(f"Failed executing the operation; {err}") from err + self._rowcount = rowcnt + + def fetchone(self) -> Optional[RowType]: + """Return next row of a query result set. + + Returns: + tuple or None: A row from query result set. + """ + self._check_executed() + return self._fetch_row() or None + + def fetchmany(self, size: Optional[int] = None) -> List[RowType]: + """Return the next set of rows of a query result set. + + When no more rows are available, it returns an empty list. + The number of rows returned can be specified using the size argument, + which defaults to one. + + Returns: + list: The next set of rows of a query result set. + """ + self._check_executed() + res = [] + cnt = size or self.arraysize + while cnt > 0 and self._stmt.have_result_set: + cnt -= 1 + row = self._fetch_row() + if row: + res.append(row) + return res + + def fetchall(self) -> List[RowType]: + """Return all rows of a query result set. + + Returns: + list: A list of tuples with all rows of a query result set. + """ + self._check_executed() + if not self._stmt.have_result_set: + return [] + + rows = self._cnx.get_rows(prep_stmt=self._stmt) + if self._nextrow and self._nextrow[0]: + rows[0].insert(0, self._nextrow[0]) + + if not rows[0]: + self._handle_eof() + return [] + + self._rowcount += len(rows[0]) + self._handle_eof() + return rows[0] + + +class CMySQLCursorPreparedDict(CMySQLCursorDict, CMySQLCursorPrepared): # type: ignore[misc] + """This class is a blend of features from CMySQLCursorDict and CMySQLCursorPrepared + + Multiple inheritance in python is allowed but care must be taken + when assuming methods resolution. In the case of multiple + inheritance, a given attribute is first searched in the current + class if it's not found then it's searched in the parent classes. + The parent classes are searched in a left-right fashion and each + class is searched once. + Based on python's attribute resolution, in this case, attributes + are searched as follows: + 1. CMySQLCursorPreparedDict (current class) + 2. CMySQLCursorDict (left parent class) + 3. CMySQLCursorPrepared (right parent class) + 4. CMySQLCursor (base class) + """ + + +class CMySQLCursorPreparedNamedTuple(CMySQLCursorNamedTuple, CMySQLCursorPrepared): + """This class is a blend of features from CMySQLCursorNamedTuple and CMySQLCursorPrepared""" + + +class CMySQLCursorPreparedRaw(CMySQLCursorPrepared): + """This class is a blend of features from CMySQLCursorRaw and CMySQLCursorPrepared""" + + _raw: bool = True diff --git a/mysql/connector/custom_types.py b/mysql/connector/custom_types.py new file mode 100644 index 0000000..7fcacbd --- /dev/null +++ b/mysql/connector/custom_types.py @@ -0,0 +1,50 @@ +# Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Custom Python types used by MySQL Connector/Python""" +from __future__ import annotations + +from typing import Type + + +class HexLiteral(str): + + """Class holding MySQL hex literals""" + + charset: str = "" + original: str = "" + + def __new__(cls: Type[HexLiteral], str_: str, charset: str = "utf8") -> HexLiteral: + hexed = [f"{i:02x}" for i in str_.encode(charset)] + obj = str.__new__(cls, "".join(hexed)) + obj.charset = charset + obj.original = str_ + return obj + + def __str__(self) -> str: + return "0x" + self diff --git a/mysql/connector/dbapi.py b/mysql/connector/dbapi.py new file mode 100644 index 0000000..f827aa1 --- /dev/null +++ b/mysql/connector/dbapi.py @@ -0,0 +1,85 @@ +# Copyright (c) 2009, 2022, Oracle and/or its affiliates. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +""" +This module implements some constructors and singletons as required by the +DB API v2.0 (PEP-249). +""" + +# Python Db API v2 +# pylint: disable=invalid-name +apilevel: str = "2.0" +threadsafety: int = 1 +paramstyle: str = "pyformat" + +import datetime +import time + +from typing import Tuple + +from . import constants + + +class _DBAPITypeObject: + def __init__(self, *values: int) -> None: + self.values: Tuple[int, ...] = values + + def __eq__(self, other: object) -> bool: + return other in self.values + + def __ne__(self, other: object) -> bool: + return other not in self.values + + +Date = datetime.date +Time = datetime.time +Timestamp = datetime.datetime + + +def DateFromTicks(ticks: int) -> datetime.date: + """Construct an object holding a date value from the given ticks value.""" + return Date(*time.localtime(ticks)[:3]) + + +def TimeFromTicks(ticks: int) -> datetime.time: + """Construct an object holding a time value from the given ticks value.""" + return Time(*time.localtime(ticks)[3:6]) + + +def TimestampFromTicks(ticks: int) -> datetime.datetime: + """Construct an object holding a time stamp from the given ticks value.""" + return Timestamp(*time.localtime(ticks)[:6]) + + +Binary = bytes + +STRING = _DBAPITypeObject(*constants.FieldType.get_string_types()) +BINARY = _DBAPITypeObject(*constants.FieldType.get_binary_types()) +NUMBER = _DBAPITypeObject(*constants.FieldType.get_number_types()) +DATETIME = _DBAPITypeObject(*constants.FieldType.get_timestamp_types()) +ROWID = _DBAPITypeObject() diff --git a/mysql/connector/django/__init__.py b/mysql/connector/django/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mysql/connector/django/base.py b/mysql/connector/django/base.py new file mode 100644 index 0000000..0d10728 --- /dev/null +++ b/mysql/connector/django/base.py @@ -0,0 +1,636 @@ +# Copyright (c) 2020, 2023, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# mypy: disable-error-code="override" + +"""Django database Backend using MySQL Connector/Python. + +This Django database backend is heavily based on the MySQL backend from Django. + +Changes include: +* Support for microseconds (MySQL 5.6.3 and later) +* Using INFORMATION_SCHEMA where possible +* Using new defaults for, for example SQL_AUTO_IS_NULL + +Requires and comes with MySQL Connector/Python v8.0.22 and later: + http://dev.mysql.com/downloads/connector/python/ +""" + +import warnings + +from datetime import datetime, time +from typing import Any, Dict, Generator, Iterator, List, Optional, Set, Tuple, Union + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.db import IntegrityError +from django.db.backends.base.base import BaseDatabaseWrapper +from django.utils import dateparse, timezone +from django.utils.functional import cached_property + +try: + import mysql.connector + + from mysql.connector.connection import MySQLConnection + from mysql.connector.connection_cext import CMySQLConnection + from mysql.connector.conversion import MySQLConverter + from mysql.connector.cursor import MySQLCursor + from mysql.connector.cursor_cext import CMySQLCursor + from mysql.connector.custom_types import HexLiteral + from mysql.connector.pooling import PooledMySQLConnection + from mysql.connector.types import ( + ParamsDictType, + ParamsSequenceOrDictType, + ParamsSequenceType, + RowType, + StrOrBytes, + ) +except ImportError as err: + raise ImproperlyConfigured(f"Error loading mysql.connector module: {err}") from err + +try: + from _mysql_connector import datetime_to_mysql +except ImportError: + HAVE_CEXT = False +else: + HAVE_CEXT = True + +from .client import DatabaseClient +from .creation import DatabaseCreation +from .features import DatabaseFeatures +from .introspection import DatabaseIntrospection +from .operations import DatabaseOperations +from .schema import DatabaseSchemaEditor +from .validation import DatabaseValidation + +Error = mysql.connector.Error +DatabaseError = mysql.connector.DatabaseError +NotSupportedError = mysql.connector.NotSupportedError +OperationalError = mysql.connector.OperationalError +ProgrammingError = mysql.connector.ProgrammingError + + +def adapt_datetime_with_timezone_support(value: datetime) -> StrOrBytes: + """Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL.""" + if settings.USE_TZ: + if timezone.is_naive(value): + warnings.warn( + f"MySQL received a naive datetime ({value})" + " while time zone support is active.", + RuntimeWarning, + ) + default_timezone = timezone.get_default_timezone() + value = timezone.make_aware(value, default_timezone) + value = value.astimezone(timezone.utc).replace(tzinfo=None) + if HAVE_CEXT: + mysql_datetime: bytes = datetime_to_mysql(value) + return mysql_datetime + return value.strftime("%Y-%m-%d %H:%M:%S.%f") + + +class CursorWrapper: + """Wrapper around MySQL Connector/Python's cursor class. + + The cursor class is defined by the options passed to MySQL + Connector/Python. If buffered option is True in those options, + MySQLCursorBuffered will be used. + """ + + codes_for_integrityerror = ( + 1048, # Column cannot be null + 1690, # BIGINT UNSIGNED value is out of range + 3819, # CHECK constraint is violated + 4025, # CHECK constraint failed + ) + + def __init__(self, cursor: Union[MySQLCursor, CMySQLCursor]) -> None: + self.cursor: Union[MySQLCursor, CMySQLCursor] = cursor + + @staticmethod + def _adapt_execute_args_dict(args: ParamsDictType) -> ParamsDictType: + if not args: + return args + new_args = dict(args) + for key, value in args.items(): + if isinstance(value, datetime): + new_args[key] = adapt_datetime_with_timezone_support(value) + + return new_args + + @staticmethod + def _adapt_execute_args( + args: Optional[ParamsSequenceType], + ) -> Optional[ParamsSequenceType]: + if not args: + return args + new_args = list(args) + for i, arg in enumerate(args): + if isinstance(arg, datetime): + new_args[i] = adapt_datetime_with_timezone_support(arg) + + return tuple(new_args) + + def execute( + self, query: str, args: Optional[ParamsSequenceOrDictType] = None + ) -> Optional[Generator[Union[MySQLCursor, CMySQLCursor], None, None]]: + """Executes the given operation + + This wrapper method around the execute()-method of the cursor is + mainly needed to re-raise using different exceptions. + """ + new_args: Optional[ParamsSequenceOrDictType] = None + if isinstance(args, dict): + new_args = self._adapt_execute_args_dict(args) + else: + new_args = self._adapt_execute_args(args) + try: + return self.cursor.execute(query, new_args) + except mysql.connector.OperationalError as exc: + if exc.args[0] in self.codes_for_integrityerror: + raise IntegrityError(*tuple(exc.args)) from None + raise + + def executemany( + self, + query: str, + args: Union[ + Tuple[ParamsSequenceOrDictType, ...], + List[ParamsSequenceOrDictType], + ], + ) -> Optional[Generator[Union[MySQLCursor, CMySQLCursor], None, None]]: + """Executes the given operation + + This wrapper method around the executemany()-method of the cursor is + mainly needed to re-raise using different exceptions. + """ + try: + return self.cursor.executemany(query, args) + except mysql.connector.OperationalError as exc: + if exc.args[0] in self.codes_for_integrityerror: + raise IntegrityError(*tuple(exc.args)) from None + raise + + def __getattr__(self, attr: Any) -> Any: + """Return an attribute of wrapped cursor""" + return getattr(self.cursor, attr) + + def __iter__(self) -> Iterator[RowType]: + """Return an iterator over wrapped cursor""" + return iter(self.cursor) + + +class DatabaseWrapper(BaseDatabaseWrapper): # pylint: disable=abstract-method + """Represent a database connection.""" + + vendor = "mysql" + # This dictionary maps Field objects to their associated MySQL column + # types, as strings. Column-type strings can contain format strings; they'll + # be interpolated against the values of Field.__dict__ before being output. + # If a column type is set to None, it won't be included in the output. + data_types = { + "AutoField": "integer AUTO_INCREMENT", + "BigAutoField": "bigint AUTO_INCREMENT", + "BinaryField": "longblob", + "BooleanField": "bool", + "CharField": "varchar(%(max_length)s)", + "DateField": "date", + "DateTimeField": "datetime(6)", + "DecimalField": "numeric(%(max_digits)s, %(decimal_places)s)", + "DurationField": "bigint", + "FileField": "varchar(%(max_length)s)", + "FilePathField": "varchar(%(max_length)s)", + "FloatField": "double precision", + "IntegerField": "integer", + "BigIntegerField": "bigint", + "IPAddressField": "char(15)", + "GenericIPAddressField": "char(39)", + "JSONField": "json", + "NullBooleanField": "bool", + "OneToOneField": "integer", + "PositiveBigIntegerField": "bigint UNSIGNED", + "PositiveIntegerField": "integer UNSIGNED", + "PositiveSmallIntegerField": "smallint UNSIGNED", + "SlugField": "varchar(%(max_length)s)", + "SmallAutoField": "smallint AUTO_INCREMENT", + "SmallIntegerField": "smallint", + "TextField": "longtext", + "TimeField": "time(6)", + "UUIDField": "char(32)", + } + + # For these data types: + # - MySQL < 8.0.13 doesn't accept default values and + # implicitly treat them as nullable + # - all versions of MySQL doesn't support full width database + # indexes + _limited_data_types = ( + "tinyblob", + "blob", + "mediumblob", + "longblob", + "tinytext", + "text", + "mediumtext", + "longtext", + "json", + ) + + operators = { + "exact": "= %s", + "iexact": "LIKE %s", + "contains": "LIKE BINARY %s", + "icontains": "LIKE %s", + "regex": "REGEXP BINARY %s", + "iregex": "REGEXP %s", + "gt": "> %s", + "gte": ">= %s", + "lt": "< %s", + "lte": "<= %s", + "startswith": "LIKE BINARY %s", + "endswith": "LIKE BINARY %s", + "istartswith": "LIKE %s", + "iendswith": "LIKE %s", + } + + # The patterns below are used to generate SQL pattern lookup clauses when + # the right-hand side of the lookup isn't a raw string (it might be an expression + # or the result of a bilateral transformation). + # In those cases, special characters for LIKE operators (e.g. \, *, _) should be + # escaped on database side. + # + # Note: we use str.format() here for readability as '%' is used as a wildcard for + # the LIKE operator. + pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\\', '\\\\'), '%%', '\%%'), '_', '\_')" + pattern_ops = { + "contains": "LIKE BINARY CONCAT('%%', {}, '%%')", + "icontains": "LIKE CONCAT('%%', {}, '%%')", + "startswith": "LIKE BINARY CONCAT({}, '%%')", + "istartswith": "LIKE CONCAT({}, '%%')", + "endswith": "LIKE BINARY CONCAT('%%', {})", + "iendswith": "LIKE CONCAT('%%', {})", + } + + isolation_level: Optional[str] = None + isolation_levels = { + "read uncommitted", + "read committed", + "repeatable read", + "serializable", + } + + Database = mysql.connector + SchemaEditorClass = DatabaseSchemaEditor + # Classes instantiated in __init__(). + client_class = DatabaseClient + creation_class = DatabaseCreation + features_class = DatabaseFeatures + introspection_class = DatabaseIntrospection + ops_class = DatabaseOperations + validation_class = DatabaseValidation + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + + options = self.settings_dict.get("OPTIONS") + if options: + self._use_pure = options.get("use_pure", not HAVE_CEXT) + converter_class = options.get( + "converter_class", + DjangoMySQLConverter, + ) + if not issubclass(converter_class, DjangoMySQLConverter): + raise ProgrammingError( + "Converter class should be a subclass of " + "mysql.connector.django.base.DjangoMySQLConverter" + ) + self.converter = converter_class() + else: + self.converter = DjangoMySQLConverter() + self._use_pure = not HAVE_CEXT + + def __getattr__(self, attr: str) -> bool: + if attr.startswith("mysql_is"): + return False + raise AttributeError + + def get_connection_params(self) -> Dict[str, Any]: + kwargs = { + "charset": "utf8", + "use_unicode": True, + "buffered": False, + "consume_results": True, + } + + settings_dict = self.settings_dict + + if settings_dict["USER"]: + kwargs["user"] = settings_dict["USER"] + if settings_dict["NAME"]: + kwargs["database"] = settings_dict["NAME"] + if settings_dict["PASSWORD"]: + kwargs["passwd"] = settings_dict["PASSWORD"] + if settings_dict["HOST"].startswith("/"): + kwargs["unix_socket"] = settings_dict["HOST"] + elif settings_dict["HOST"]: + kwargs["host"] = settings_dict["HOST"] + if settings_dict["PORT"]: + kwargs["port"] = int(settings_dict["PORT"]) + if settings_dict.get("OPTIONS", {}).get("init_command"): + kwargs["init_command"] = settings_dict["OPTIONS"]["init_command"] + + # Raise exceptions for database warnings if DEBUG is on + kwargs["raise_on_warnings"] = settings.DEBUG + + kwargs["client_flags"] = [ + # Need potentially affected rows on UPDATE + mysql.connector.constants.ClientFlag.FOUND_ROWS, + ] + + try: + options = settings_dict["OPTIONS"].copy() + isolation_level = options.pop("isolation_level") + if isolation_level: + isolation_level = isolation_level.lower() + if isolation_level not in self.isolation_levels: + valid_levels = ", ".join( + f"'{level}'" for level in sorted(self.isolation_levels) + ) + raise ImproperlyConfigured( + f"Invalid transaction isolation level '{isolation_level}' " + f"specified.\nUse one of {valid_levels}, or None." + ) + self.isolation_level = isolation_level + kwargs.update(options) + except KeyError: + # OPTIONS missing is OK + pass + return kwargs + + def get_new_connection( + self, conn_params: Dict[str, Any] + ) -> Union[PooledMySQLConnection, MySQLConnection, CMySQLConnection]: + if "converter_class" not in conn_params: + conn_params["converter_class"] = DjangoMySQLConverter + cnx = mysql.connector.connect(**conn_params) + + return cnx + + def init_connection_state(self) -> None: + assignments = [] + if self.features.is_sql_auto_is_null_enabled: # type: ignore[attr-defined] + # SQL_AUTO_IS_NULL controls whether an AUTO_INCREMENT column on + # a recently inserted row will return when the field is tested + # for NULL. Disabling this brings this aspect of MySQL in line + # with SQL standards. + assignments.append("SET SQL_AUTO_IS_NULL = 0") + + if self.isolation_level: + assignments.append( + "SET SESSION TRANSACTION ISOLATION LEVEL " + f"{self.isolation_level.upper()}" + ) + + if assignments: + with self.cursor() as cursor: + cursor.execute("; ".join(assignments)) + + if "AUTOCOMMIT" in self.settings_dict: + try: + self.set_autocommit(self.settings_dict["AUTOCOMMIT"]) + except AttributeError: + self._set_autocommit(self.settings_dict["AUTOCOMMIT"]) + + def create_cursor(self, name: Any = None) -> CursorWrapper: + cursor = self.connection.cursor() + return CursorWrapper(cursor) + + def _rollback(self) -> None: + try: + BaseDatabaseWrapper._rollback(self) # type: ignore[attr-defined] + except NotSupportedError: + pass + + def _set_autocommit(self, autocommit: bool) -> None: + with self.wrap_database_errors: + self.connection.autocommit = autocommit + + def disable_constraint_checking(self) -> bool: + """ + Disable foreign key checks, primarily for use in adding rows with + forward references. Always return True to indicate constraint checks + need to be re-enabled. + """ + with self.cursor() as cursor: + cursor.execute("SET foreign_key_checks=0") + return True + + def enable_constraint_checking(self) -> None: + """ + Re-enable foreign key checks after they have been disabled. + """ + # Override needs_rollback in case constraint_checks_disabled is + # nested inside transaction.atomic. + self.needs_rollback, needs_rollback = False, self.needs_rollback + try: + with self.cursor() as cursor: + cursor.execute("SET foreign_key_checks=1") + finally: + self.needs_rollback = needs_rollback + + def check_constraints(self, table_names: Optional[List[str]] = None) -> None: + """ + Check each table name in `table_names` for rows with invalid foreign + key references. This method is intended to be used in conjunction with + `disable_constraint_checking()` and `enable_constraint_checking()`, to + determine if rows with invalid references were entered while constraint + checks were off. + """ + with self.cursor() as cursor: + if table_names is None: + table_names = self.introspection.table_names(cursor) + for table_name in table_names: + primary_key_column_name = self.introspection.get_primary_key_column( + cursor, table_name + ) + if not primary_key_column_name: + continue + key_columns = self.introspection.get_key_columns(cursor, table_name) + for ( + column_name, + referenced_table_name, + referenced_column_name, + ) in key_columns: + cursor.execute( + f""" + SELECT REFERRING.`{primary_key_column_name}`, + REFERRING.`{column_name}` + FROM `{table_name}` as REFERRING + LEFT JOIN `{referenced_table_name}` as REFERRED + ON ( + REFERRING.`{column_name}` = + REFERRED.`{referenced_column_name}` + ) + WHERE REFERRING.`{column_name}` IS NOT NULL + AND REFERRED.`{referenced_column_name}` IS NULL + """ + ) + for bad_row in cursor.fetchall(): + raise IntegrityError( + f"The row in table '{table_name}' with primary " + f"key '{bad_row[0]}' has an invalid foreign key: " + f"{table_name}.{column_name} contains a value " + f"'{bad_row[1]}' that does not have a " + f"corresponding value in " + f"{referenced_table_name}." + f"{referenced_column_name}." + ) + + def is_usable(self) -> bool: + try: + self.connection.ping() + except Error: + return False + return True + + @cached_property + @staticmethod + def display_name() -> str: + """Display name.""" + return "MySQL" + + @cached_property + def data_type_check_constraints(self) -> Dict[str, str]: + """Mapping of Field objects to their SQL for CHECK constraints.""" + if self.features.supports_column_check_constraints: + check_constraints = { + "PositiveBigIntegerField": "`%(column)s` >= 0", + "PositiveIntegerField": "`%(column)s` >= 0", + "PositiveSmallIntegerField": "`%(column)s` >= 0", + } + return check_constraints + return {} + + @cached_property + def mysql_server_data(self) -> Dict[str, Any]: + """Return MySQL server data.""" + with self.temporary_connection() as cursor: + # Select some server variables and test if the time zone + # definitions are installed. CONVERT_TZ returns NULL if 'UTC' + # timezone isn't loaded into the mysql.time_zone table. + cursor.execute( + """ + SELECT VERSION(), + @@sql_mode, + @@default_storage_engine, + @@sql_auto_is_null, + @@lower_case_table_names, + CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL + """ + ) + row = cursor.fetchone() + return { + "version": row[0], + "sql_mode": row[1], + "default_storage_engine": row[2], + "sql_auto_is_null": bool(row[3]), + "lower_case_table_names": bool(row[4]), + "has_zoneinfo_database": bool(row[5]), + } + + @cached_property + def mysql_server_info(self) -> Any: + """Return MySQL version.""" + with self.temporary_connection() as cursor: + cursor.execute("SELECT VERSION()") + return cursor.fetchone()[0] + + @cached_property + def mysql_version(self) -> Tuple[int, ...]: + """Return MySQL version.""" + config = self.get_connection_params() + with mysql.connector.connect(**config) as conn: + server_version: Tuple[int, ...] = conn.get_server_version() + return server_version + + @cached_property + def sql_mode(self) -> Set[str]: + """Return SQL mode.""" + with self.cursor() as cursor: + cursor.execute("SELECT @@sql_mode") + sql_mode = cursor.fetchone() + return set(sql_mode[0].split(",") if sql_mode else ()) + + @property + def use_pure(self) -> bool: + """Return True if pure Python version is being used.""" + ans: bool = self._use_pure + return ans + + +class DjangoMySQLConverter(MySQLConverter): + """Custom converter for Django.""" + + # pylint: disable=unused-argument + + @staticmethod + def _time_to_python(value: bytes, dsc: Any = None) -> Optional[time]: + """Return MySQL TIME data type as datetime.time() + + Returns datetime.time() + """ + return dateparse.parse_time(value.decode("utf-8")) + + @staticmethod + def _datetime_to_python(value: bytes, dsc: Any = None) -> Optional[datetime]: + """Connector/Python always returns naive datetime.datetime + + Connector/Python always returns naive timestamps since MySQL has + no time zone support. + + - A naive datetime is a datetime that doesn't know its own timezone. + + Django needs a non-naive datetime, but in this method we don't need + to make a datetime value time zone aware since Django itself at some + point will make it aware (at least in versions 3.2.16 and 4.1.2) when + USE_TZ=True. This may change in a future release, we need to keep an + eye on this behaviour. + + Returns datetime.datetime() + """ + return MySQLConverter._datetime_to_python(value) if value else None + + # pylint: enable=unused-argument + + def _safestring_to_mysql(self, value: str) -> Union[bytes, HexLiteral]: + return self._str_to_mysql(value) + + def _safetext_to_mysql(self, value: str) -> Union[bytes, HexLiteral]: + return self._str_to_mysql(value) + + def _safebytes_to_mysql(self, value: bytes) -> bytes: + return self._bytes_to_mysql(value) diff --git a/mysql/connector/django/client.py b/mysql/connector/django/client.py new file mode 100644 index 0000000..090ccd5 --- /dev/null +++ b/mysql/connector/django/client.py @@ -0,0 +1,106 @@ +# Copyright (c) 2020, 2022, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Database Client.""" + +import os +import subprocess + +from typing import Any, Dict, Iterable, List, Optional, Tuple + +from django.db.backends.base.client import BaseDatabaseClient + + +class DatabaseClient(BaseDatabaseClient): + """Encapsulate backend-specific methods for opening a client shell.""" + + executable_name = "mysql" + + @classmethod + def settings_to_cmd_args_env( + cls, settings_dict: Dict[str, Any], parameters: Optional[Iterable[str]] = None + ) -> Tuple[List[str], Optional[Dict[str, Any]]]: + args = [cls.executable_name] + + db = settings_dict["OPTIONS"].get("database", settings_dict["NAME"]) + user = settings_dict["OPTIONS"].get("user", settings_dict["USER"]) + passwd = settings_dict["OPTIONS"].get("password", settings_dict["PASSWORD"]) + host = settings_dict["OPTIONS"].get("host", settings_dict["HOST"]) + port = settings_dict["OPTIONS"].get("port", settings_dict["PORT"]) + ssl_ca = settings_dict["OPTIONS"].get("ssl_ca") + ssl_cert = settings_dict["OPTIONS"].get("ssl_cert") + ssl_key = settings_dict["OPTIONS"].get("ssl_key") + defaults_file = settings_dict["OPTIONS"].get("read_default_file") + charset = settings_dict["OPTIONS"].get("charset") + + # --defaults-file should always be the first option + if defaults_file: + args.append(f"--defaults-file={defaults_file}") + + # Load any custom init_commands. We always force SQL_MODE to TRADITIONAL + init_command = settings_dict["OPTIONS"].get("init_command", "") + args.append(f"--init-command=SET @@session.SQL_MODE=TRADITIONAL;{init_command}") + + if user: + args.append(f"--user={user}") + if passwd: + args.append(f"--password={passwd}") + + if host: + if "/" in host: + args.append(f"--socket={host}") + else: + args.append(f"--host={host}") + + if port: + args.append(f"--port={port}") + + if db: + args.append(f"--database={db}") + + if ssl_ca: + args.append(f"--ssl-ca={ssl_ca}") + if ssl_cert: + args.append(f"--ssl-cert={ssl_cert}") + if ssl_key: + args.append(f"--ssl-key={ssl_key}") + + if charset: + args.append(f"--default-character-set={charset}") + + if parameters: + args.extend(parameters) + + return args, None + + def runshell(self, parameters: Optional[Iterable[str]] = None) -> None: + args, env = self.settings_to_cmd_args_env( + self.connection.settings_dict, parameters + ) + env = {**os.environ, **env} if env else None + subprocess.run(args, env=env, check=True) diff --git a/mysql/connector/django/compiler.py b/mysql/connector/django/compiler.py new file mode 100644 index 0000000..1ee7871 --- /dev/null +++ b/mysql/connector/django/compiler.py @@ -0,0 +1,45 @@ +# Copyright (c) 2020, 2022, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""SQL Compiler classes.""" + +from django.db.backends.mysql.compiler import ( + SQLAggregateCompiler, + SQLCompiler, + SQLDeleteCompiler, + SQLInsertCompiler, + SQLUpdateCompiler, +) + +__all__ = [ + "SQLAggregateCompiler", + "SQLCompiler", + "SQLDeleteCompiler", + "SQLInsertCompiler", + "SQLUpdateCompiler", +] diff --git a/mysql/connector/django/creation.py b/mysql/connector/django/creation.py new file mode 100644 index 0000000..82f0853 --- /dev/null +++ b/mysql/connector/django/creation.py @@ -0,0 +1,33 @@ +# Copyright (c) 2020, 2022, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Backend specific database creation.""" + +from django.db.backends.mysql.creation import DatabaseCreation + +__all__ = ["DatabaseCreation"] diff --git a/mysql/connector/django/features.py b/mysql/connector/django/features.py new file mode 100644 index 0000000..e8debb8 --- /dev/null +++ b/mysql/connector/django/features.py @@ -0,0 +1,50 @@ +# Copyright (c) 2020, 2022, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Database Features.""" + +from typing import Any, List + +from django.db.backends.mysql.features import DatabaseFeatures as MySQLDatabaseFeatures +from django.utils.functional import cached_property + + +class DatabaseFeatures(MySQLDatabaseFeatures): + """Database Features Specification class.""" + + empty_fetchmany_value: List[Any] = [] + + @cached_property + def can_introspect_check_constraints(self) -> bool: # type: ignore[override] + """Check if backend support introspection CHECK of constraints.""" + return self.connection.mysql_version >= (8, 0, 16) + + @cached_property + def supports_microsecond_precision(self) -> bool: + """Check if backend support microsecond precision.""" + return self.connection.mysql_version >= (5, 6, 3) diff --git a/mysql/connector/django/introspection.py b/mysql/connector/django/introspection.py new file mode 100644 index 0000000..304a0ec --- /dev/null +++ b/mysql/connector/django/introspection.py @@ -0,0 +1,461 @@ +# Copyright (c) 2020, 2022, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# mypy: disable-error-code="override,attr-defined,call-arg" + +"""Database Introspection.""" + +from collections import namedtuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple + +import sqlparse + +from django import VERSION as DJANGO_VERSION +from django.db.backends.base.introspection import ( + BaseDatabaseIntrospection, + FieldInfo as BaseFieldInfo, + TableInfo, +) +from django.db.models import Index +from django.utils.datastructures import OrderedSet + +from mysql.connector.constants import FieldType + +# from .base import CursorWrapper produces a circular import error, +# avoiding importing CursorWrapper explicitly, using a documented +# trick; write the imports inside if TYPE_CHECKING: so that they +# are not executed at runtime. +# Ref: https://buildmedia.readthedocs.org/media/pdf/mypy/stable/mypy.pdf [page 42] +if TYPE_CHECKING: + # CursorWraper is used exclusively for type hinting + from mysql.connector.django.base import CursorWrapper + +# Based on my investigation, named tuples to +# comply with mypy need to define a static list or tuple +# for field_names (second argument). In this case, the field +# names are created dynamically for FieldInfo which triggers +# a mypy error. The solution is not straightforward since +# FieldInfo attributes are Django version dependent. Code +# refactory is needed to fix this issue. +FieldInfo = namedtuple( # type: ignore[misc] + "FieldInfo", + BaseFieldInfo._fields + ("extra", "is_unsigned", "has_json_constraint"), +) +if DJANGO_VERSION < (3, 2, 0): + InfoLine = namedtuple( + "InfoLine", + "col_name data_type max_len num_prec num_scale extra column_default " + "is_unsigned", + ) +else: + InfoLine = namedtuple( # type: ignore[no-redef] + "InfoLine", + "col_name data_type max_len num_prec num_scale extra column_default " + "collation is_unsigned", + ) + + +class DatabaseIntrospection(BaseDatabaseIntrospection): + """Encapsulate backend-specific introspection utilities.""" + + data_types_reverse = { + FieldType.BLOB: "TextField", + FieldType.DECIMAL: "DecimalField", + FieldType.NEWDECIMAL: "DecimalField", + FieldType.DATE: "DateField", + FieldType.DATETIME: "DateTimeField", + FieldType.DOUBLE: "FloatField", + FieldType.FLOAT: "FloatField", + FieldType.INT24: "IntegerField", + FieldType.LONG: "IntegerField", + FieldType.LONGLONG: "BigIntegerField", + FieldType.SHORT: "SmallIntegerField", + FieldType.STRING: "CharField", + FieldType.TIME: "TimeField", + FieldType.TIMESTAMP: "DateTimeField", + FieldType.TINY: "IntegerField", + FieldType.TINY_BLOB: "TextField", + FieldType.MEDIUM_BLOB: "TextField", + FieldType.LONG_BLOB: "TextField", + FieldType.VAR_STRING: "CharField", + } + + def get_field_type(self, data_type: str, description: FieldInfo) -> str: + field_type = super().get_field_type(data_type, description) # type: ignore[arg-type] + if "auto_increment" in description.extra: + if field_type == "IntegerField": + return "AutoField" + if field_type == "BigIntegerField": + return "BigAutoField" + if field_type == "SmallIntegerField": + return "SmallAutoField" + if description.is_unsigned: + if field_type == "BigIntegerField": + return "PositiveBigIntegerField" + if field_type == "IntegerField": + return "PositiveIntegerField" + if field_type == "SmallIntegerField": + return "PositiveSmallIntegerField" + # JSON data type is an alias for LONGTEXT in MariaDB, use check + # constraints clauses to introspect JSONField. + if description.has_json_constraint: + return "JSONField" + return field_type + + def get_table_list(self, cursor: "CursorWrapper") -> List[TableInfo]: + """Return a list of table and view names in the current database.""" + cursor.execute("SHOW FULL TABLES") + return [ + TableInfo(row[0], {"BASE TABLE": "t", "VIEW": "v"}.get(row[1])) + for row in cursor.fetchall() + ] + + def get_table_description( + self, cursor: "CursorWrapper", table_name: str + ) -> List[FieldInfo]: + """ + Return a description of the table with the DB-API cursor.description + interface." + """ + json_constraints: Dict[Any, Any] = {} + # A default collation for the given table. + cursor.execute( + """ + SELECT table_collation + FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = %s + """, + [table_name], + ) + row = cursor.fetchone() + default_column_collation = row[0] if row else "" + # information_schema database gives more accurate results for some figures: + # - varchar length returned by cursor.description is an internal length, + # not visible length (#5725) + # - precision and scale (for decimal fields) (#5014) + # - auto_increment is not available in cursor.description + if DJANGO_VERSION < (3, 2, 0): + cursor.execute( + """ + SELECT + column_name, data_type, character_maximum_length, + numeric_precision, numeric_scale, extra, column_default, + CASE + WHEN column_type LIKE '%% unsigned' THEN 1 + ELSE 0 + END AS is_unsigned + FROM information_schema.columns + WHERE table_name = %s AND table_schema = DATABASE() + """, + [table_name], + ) + else: + cursor.execute( + """ + SELECT + column_name, data_type, character_maximum_length, + numeric_precision, numeric_scale, extra, column_default, + CASE + WHEN collation_name = %s THEN NULL + ELSE collation_name + END AS collation_name, + CASE + WHEN column_type LIKE '%% unsigned' THEN 1 + ELSE 0 + END AS is_unsigned + FROM information_schema.columns + WHERE table_name = %s AND table_schema = DATABASE() + """, + [default_column_collation, table_name], + ) + field_info = {line[0]: InfoLine(*line) for line in cursor.fetchall()} + + cursor.execute( + f"SELECT * FROM {self.connection.ops.quote_name(table_name)} LIMIT 1" + ) + + def to_int(i: Any) -> Optional[int]: + return int(i) if i is not None else i + + fields = [] + for line in cursor.description: + info = field_info[line[0]] + if DJANGO_VERSION < (3, 2, 0): + fields.append( + FieldInfo( + *line[:3], + to_int(info.max_len) or line[3], + to_int(info.num_prec) or line[4], + to_int(info.num_scale) or line[5], + line[6], + info.column_default, + info.extra, + info.is_unsigned, + line[0] in json_constraints, + ) + ) + else: + fields.append( + FieldInfo( + *line[:3], + to_int(info.max_len) or line[3], + to_int(info.num_prec) or line[4], + to_int(info.num_scale) or line[5], + line[6], + info.column_default, + info.collation, + info.extra, + info.is_unsigned, + line[0] in json_constraints, + ) + ) + return fields + + def get_indexes( + self, cursor: "CursorWrapper", table_name: str + ) -> Dict[int, Dict[str, bool]]: + """Return indexes from table.""" + cursor.execute(f"SHOW INDEX FROM {self.connection.ops.quote_name(table_name)}") + # Do a two-pass search for indexes: on first pass check which indexes + # are multicolumn, on second pass check which single-column indexes + # are present. + rows = list(cursor.fetchall()) + multicol_indexes = set() + for row in rows: + if row[3] > 1: + multicol_indexes.add(row[2]) + indexes: Dict[int, Dict[str, bool]] = {} + for row in rows: + if row[2] in multicol_indexes: + continue + if row[4] not in indexes: + indexes[row[4]] = {"primary_key": False, "unique": False} + # It's possible to have the unique and PK constraints in + # separate indexes. + if row[2] == "PRIMARY": + indexes[row[4]]["primary_key"] = True + if not row[1]: + indexes[row[4]]["unique"] = True + return indexes + + def get_primary_key_column( + self, cursor: "CursorWrapper", table_name: str + ) -> Optional[int]: + """ + Returns the name of the primary key column for the given table + """ + for column in self.get_indexes(cursor, table_name).items(): + if column[1]["primary_key"]: + return column[0] + return None + + def get_sequences( + self, cursor: "CursorWrapper", table_name: str, table_fields: Any = () + ) -> List[Dict[str, str]]: + for field_info in self.get_table_description(cursor, table_name): + if "auto_increment" in field_info.extra: + # MySQL allows only one auto-increment column per table. + return [{"table": table_name, "column": field_info.name}] + return [] + + def get_relations( + self, cursor: "CursorWrapper", table_name: str + ) -> Dict[str, Tuple[str, str]]: + """ + Return a dictionary of {field_name: (field_name_other_table, other_table)} + representing all relationships to the given table. + """ + constraints = self.get_key_columns(cursor, table_name) + relations = {} + for my_fieldname, other_table, other_field in constraints: + relations[my_fieldname] = (other_field, other_table) + return relations + + def get_key_columns( + self, cursor: "CursorWrapper", table_name: str + ) -> List[Tuple[str, str, str]]: + """ + Return a list of (column_name, referenced_table_name, referenced_column_name) + for all key columns in the given table. + """ + key_columns: List[Any] = [] + cursor.execute( + """ + SELECT column_name, referenced_table_name, referenced_column_name + FROM information_schema.key_column_usage + WHERE table_name = %s + AND table_schema = DATABASE() + AND referenced_table_name IS NOT NULL + AND referenced_column_name IS NOT NULL""", + [table_name], + ) + key_columns.extend(cursor.fetchall()) + return key_columns + + def get_storage_engine(self, cursor: "CursorWrapper", table_name: str) -> str: + """ + Retrieve the storage engine for a given table. Return the default + storage engine if the table doesn't exist. + """ + cursor.execute( + "SELECT engine FROM information_schema.tables WHERE table_name = %s", + [table_name], + ) + result = cursor.fetchone() + # pylint: disable=protected-access + if not result: + return self.connection.features._mysql_storage_engine + # pylint: enable=protected-access + return result[0] + + def _parse_constraint_columns( + self, check_clause: Any, columns: Set[str] + ) -> OrderedSet: + check_columns: OrderedSet = OrderedSet() + statement = sqlparse.parse(check_clause)[0] + tokens = (token for token in statement.flatten() if not token.is_whitespace) + for token in tokens: + if ( + token.ttype == sqlparse.tokens.Name + and self.connection.ops.quote_name(token.value) == token.value + and token.value[1:-1] in columns + ): + check_columns.add(token.value[1:-1]) + return check_columns + + def get_constraints( + self, cursor: "CursorWrapper", table_name: str + ) -> Dict[str, Any]: + """ + Retrieve any constraints or keys (unique, pk, fk, check, index) across + one or more columns. + """ + constraints: Dict[str, Any] = {} + # Get the actual constraint names and columns + name_query = """ + SELECT kc.`constraint_name`, kc.`column_name`, + kc.`referenced_table_name`, kc.`referenced_column_name` + FROM information_schema.key_column_usage AS kc + WHERE + kc.table_schema = DATABASE() AND + kc.table_name = %s + ORDER BY kc.`ordinal_position` + """ + cursor.execute(name_query, [table_name]) + for constraint, column, ref_table, ref_column in cursor.fetchall(): + if constraint not in constraints: + constraints[constraint] = { + "columns": OrderedSet(), + "primary_key": False, + "unique": False, + "index": False, + "check": False, + "foreign_key": (ref_table, ref_column) if ref_column else None, + } + if self.connection.features.supports_index_column_ordering: + constraints[constraint]["orders"] = [] + constraints[constraint]["columns"].add(column) + # Now get the constraint types + type_query = """ + SELECT c.constraint_name, c.constraint_type + FROM information_schema.table_constraints AS c + WHERE + c.table_schema = DATABASE() AND + c.table_name = %s + """ + cursor.execute(type_query, [table_name]) + for constraint, kind in cursor.fetchall(): + if kind.lower() == "primary key": + constraints[constraint]["primary_key"] = True + constraints[constraint]["unique"] = True + elif kind.lower() == "unique": + constraints[constraint]["unique"] = True + # Add check constraints. + if self.connection.features.can_introspect_check_constraints: + unnamed_constraints_index = 0 + columns = { + info.name for info in self.get_table_description(cursor, table_name) + } + type_query = """ + SELECT cc.constraint_name, cc.check_clause + FROM + information_schema.check_constraints AS cc, + information_schema.table_constraints AS tc + WHERE + cc.constraint_schema = DATABASE() AND + tc.table_schema = cc.constraint_schema AND + cc.constraint_name = tc.constraint_name AND + tc.constraint_type = 'CHECK' AND + tc.table_name = %s + """ + cursor.execute(type_query, [table_name]) + for constraint, check_clause in cursor.fetchall(): + constraint_columns = self._parse_constraint_columns( + check_clause, columns + ) + # Ensure uniqueness of unnamed constraints. Unnamed unique + # and check columns constraints have the same name as + # a column. + if set(constraint_columns) == {constraint}: + unnamed_constraints_index += 1 + constraint = f"__unnamed_constraint_{unnamed_constraints_index}__" + constraints[constraint] = { + "columns": constraint_columns, + "primary_key": False, + "unique": False, + "index": False, + "check": True, + "foreign_key": None, + } + # Now add in the indexes + cursor.execute(f"SHOW INDEX FROM {self.connection.ops.quote_name(table_name)}") + for _, _, index, _, column, order, type_ in [ + x[:6] + (x[10],) for x in cursor.fetchall() + ]: + if index not in constraints: + constraints[index] = { + "columns": OrderedSet(), + "primary_key": False, + "unique": False, + "check": False, + "foreign_key": None, + } + if self.connection.features.supports_index_column_ordering: + constraints[index]["orders"] = [] + constraints[index]["index"] = True + constraints[index]["type"] = ( + Index.suffix if type_ == "BTREE" else type_.lower() + ) + constraints[index]["columns"].add(column) + if self.connection.features.supports_index_column_ordering: + constraints[index]["orders"].append("DESC" if order == "D" else "ASC") + # Convert the sorted sets to lists + for constraint in constraints.values(): + constraint["columns"] = list(constraint["columns"]) + return constraints diff --git a/mysql/connector/django/operations.py b/mysql/connector/django/operations.py new file mode 100644 index 0000000..d010b0b --- /dev/null +++ b/mysql/connector/django/operations.py @@ -0,0 +1,104 @@ +# Copyright (c) 2020, 2022, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# mypy: disable-error-code="override,attr-defined" + +"""Database Operations.""" + +from datetime import datetime, time +from typing import Optional + +from django.conf import settings +from django.db.backends.mysql.operations import ( + DatabaseOperations as MySQLDatabaseOperations, +) +from django.utils import timezone + +try: + from _mysql_connector import datetime_to_mysql, time_to_mysql +except ImportError: + HAVE_CEXT = False +else: + HAVE_CEXT = True + + +class DatabaseOperations(MySQLDatabaseOperations): + """Database Operations class.""" + + compiler_module = "mysql.connector.django.compiler" + + def regex_lookup(self, lookup_type: str) -> str: + """Return the string to use in a query when performing regular + expression lookup.""" + if self.connection.mysql_version < (8, 0, 0): + if lookup_type == "regex": + return "%s REGEXP BINARY %s" + return "%s REGEXP %s" + + match_option = "c" if lookup_type == "regex" else "i" + return f"REGEXP_LIKE(%s, %s, '{match_option}')" + + def adapt_datetimefield_value(self, value: Optional[datetime]) -> Optional[bytes]: + """Transform a datetime value to an object compatible with what is + expected by the backend driver for datetime columns.""" + return self.value_to_db_datetime(value) + + def value_to_db_datetime(self, value: Optional[datetime]) -> Optional[bytes]: + """Convert value to MySQL DATETIME.""" + ans: Optional[bytes] = None + if value is None: + return ans + # MySQL doesn't support tz-aware times + if timezone.is_aware(value): + if settings.USE_TZ: + value = value.astimezone(timezone.utc).replace(tzinfo=None) + else: + raise ValueError("MySQL backend does not support timezone-aware times") + if not self.connection.features.supports_microsecond_precision: + value = value.replace(microsecond=0) + if not self.connection.use_pure: + return datetime_to_mysql(value) + return self.connection.converter.to_mysql(value) + + def adapt_timefield_value(self, value: Optional[time]) -> Optional[bytes]: + """Transform a time value to an object compatible with what is expected + by the backend driver for time columns.""" + return self.value_to_db_time(value) + + def value_to_db_time(self, value: Optional[time]) -> Optional[bytes]: + """Convert value to MySQL TIME.""" + if value is None: + return None + + # MySQL doesn't support tz-aware times + if timezone.is_aware(value): + raise ValueError("MySQL backend does not support timezone-aware times") + + if not self.connection.use_pure: + return time_to_mysql(value) + return self.connection.converter.to_mysql(value) diff --git a/mysql/connector/django/schema.py b/mysql/connector/django/schema.py new file mode 100644 index 0000000..4d4f454 --- /dev/null +++ b/mysql/connector/django/schema.py @@ -0,0 +1,59 @@ +# Copyright (c) 2020, 2022, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# mypy: disable-error-code="override" + +"""Database schema editor.""" +from typing import Any + +from django.db.backends.mysql.schema import ( + DatabaseSchemaEditor as MySQLDatabaseSchemaEditor, +) + + +class DatabaseSchemaEditor(MySQLDatabaseSchemaEditor): + """This class is responsible for emitting schema-changing statements to the + databases. + """ + + def quote_value(self, value: Any) -> Any: + """Quote value.""" + self.connection.ensure_connection() + if isinstance(value, str): + value = value.replace("%", "%%") + quoted = self.connection.connection.converter.escape(value) + if isinstance(value, str) and isinstance(quoted, bytes): + quoted = quoted.decode() + return quoted + + def prepare_default(self, value: Any) -> Any: + """Implement the required abstract method. + + MySQL has requires_literal_defaults=False, therefore return the value. + """ + return value diff --git a/mysql/connector/django/validation.py b/mysql/connector/django/validation.py new file mode 100644 index 0000000..9096e1c --- /dev/null +++ b/mysql/connector/django/validation.py @@ -0,0 +1,33 @@ +# Copyright (c) 2020, 2022, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Backend specific database validation.""" + +from django.db.backends.mysql.validation import DatabaseValidation + +__all__ = ["DatabaseValidation"] diff --git a/mysql/connector/errorcode.py b/mysql/connector/errorcode.py new file mode 100644 index 0000000..39fdb1b --- /dev/null +++ b/mysql/connector/errorcode.py @@ -0,0 +1,1877 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2013, 2022, Oracle and/or its affiliates. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""This module contains the MySQL Server and Client error codes.""" + +# This file was auto-generated. +_GENERATED_ON = "2021-08-11" +_MYSQL_VERSION = (8, 0, 27) + +# Start MySQL Errors +OBSOLETE_ER_HASHCHK = 1000 +OBSOLETE_ER_NISAMCHK = 1001 +ER_NO = 1002 +ER_YES = 1003 +ER_CANT_CREATE_FILE = 1004 +ER_CANT_CREATE_TABLE = 1005 +ER_CANT_CREATE_DB = 1006 +ER_DB_CREATE_EXISTS = 1007 +ER_DB_DROP_EXISTS = 1008 +OBSOLETE_ER_DB_DROP_DELETE = 1009 +ER_DB_DROP_RMDIR = 1010 +OBSOLETE_ER_CANT_DELETE_FILE = 1011 +ER_CANT_FIND_SYSTEM_REC = 1012 +ER_CANT_GET_STAT = 1013 +OBSOLETE_ER_CANT_GET_WD = 1014 +ER_CANT_LOCK = 1015 +ER_CANT_OPEN_FILE = 1016 +ER_FILE_NOT_FOUND = 1017 +ER_CANT_READ_DIR = 1018 +OBSOLETE_ER_CANT_SET_WD = 1019 +ER_CHECKREAD = 1020 +OBSOLETE_ER_DISK_FULL = 1021 +ER_DUP_KEY = 1022 +OBSOLETE_ER_ERROR_ON_CLOSE = 1023 +ER_ERROR_ON_READ = 1024 +ER_ERROR_ON_RENAME = 1025 +ER_ERROR_ON_WRITE = 1026 +ER_FILE_USED = 1027 +OBSOLETE_ER_FILSORT_ABORT = 1028 +OBSOLETE_ER_FORM_NOT_FOUND = 1029 +ER_GET_ERRNO = 1030 +ER_ILLEGAL_HA = 1031 +ER_KEY_NOT_FOUND = 1032 +ER_NOT_FORM_FILE = 1033 +ER_NOT_KEYFILE = 1034 +ER_OLD_KEYFILE = 1035 +ER_OPEN_AS_READONLY = 1036 +ER_OUTOFMEMORY = 1037 +ER_OUT_OF_SORTMEMORY = 1038 +OBSOLETE_ER_UNEXPECTED_EOF = 1039 +ER_CON_COUNT_ERROR = 1040 +ER_OUT_OF_RESOURCES = 1041 +ER_BAD_HOST_ERROR = 1042 +ER_HANDSHAKE_ERROR = 1043 +ER_DBACCESS_DENIED_ERROR = 1044 +ER_ACCESS_DENIED_ERROR = 1045 +ER_NO_DB_ERROR = 1046 +ER_UNKNOWN_COM_ERROR = 1047 +ER_BAD_NULL_ERROR = 1048 +ER_BAD_DB_ERROR = 1049 +ER_TABLE_EXISTS_ERROR = 1050 +ER_BAD_TABLE_ERROR = 1051 +ER_NON_UNIQ_ERROR = 1052 +ER_SERVER_SHUTDOWN = 1053 +ER_BAD_FIELD_ERROR = 1054 +ER_WRONG_FIELD_WITH_GROUP = 1055 +ER_WRONG_GROUP_FIELD = 1056 +ER_WRONG_SUM_SELECT = 1057 +ER_WRONG_VALUE_COUNT = 1058 +ER_TOO_LONG_IDENT = 1059 +ER_DUP_FIELDNAME = 1060 +ER_DUP_KEYNAME = 1061 +ER_DUP_ENTRY = 1062 +ER_WRONG_FIELD_SPEC = 1063 +ER_PARSE_ERROR = 1064 +ER_EMPTY_QUERY = 1065 +ER_NONUNIQ_TABLE = 1066 +ER_INVALID_DEFAULT = 1067 +ER_MULTIPLE_PRI_KEY = 1068 +ER_TOO_MANY_KEYS = 1069 +ER_TOO_MANY_KEY_PARTS = 1070 +ER_TOO_LONG_KEY = 1071 +ER_KEY_COLUMN_DOES_NOT_EXITS = 1072 +ER_BLOB_USED_AS_KEY = 1073 +ER_TOO_BIG_FIELDLENGTH = 1074 +ER_WRONG_AUTO_KEY = 1075 +ER_READY = 1076 +OBSOLETE_ER_NORMAL_SHUTDOWN = 1077 +OBSOLETE_ER_GOT_SIGNAL = 1078 +ER_SHUTDOWN_COMPLETE = 1079 +ER_FORCING_CLOSE = 1080 +ER_IPSOCK_ERROR = 1081 +ER_NO_SUCH_INDEX = 1082 +ER_WRONG_FIELD_TERMINATORS = 1083 +ER_BLOBS_AND_NO_TERMINATED = 1084 +ER_TEXTFILE_NOT_READABLE = 1085 +ER_FILE_EXISTS_ERROR = 1086 +ER_LOAD_INFO = 1087 +ER_ALTER_INFO = 1088 +ER_WRONG_SUB_KEY = 1089 +ER_CANT_REMOVE_ALL_FIELDS = 1090 +ER_CANT_DROP_FIELD_OR_KEY = 1091 +ER_INSERT_INFO = 1092 +ER_UPDATE_TABLE_USED = 1093 +ER_NO_SUCH_THREAD = 1094 +ER_KILL_DENIED_ERROR = 1095 +ER_NO_TABLES_USED = 1096 +ER_TOO_BIG_SET = 1097 +ER_NO_UNIQUE_LOGFILE = 1098 +ER_TABLE_NOT_LOCKED_FOR_WRITE = 1099 +ER_TABLE_NOT_LOCKED = 1100 +ER_BLOB_CANT_HAVE_DEFAULT = 1101 +ER_WRONG_DB_NAME = 1102 +ER_WRONG_TABLE_NAME = 1103 +ER_TOO_BIG_SELECT = 1104 +ER_UNKNOWN_ERROR = 1105 +ER_UNKNOWN_PROCEDURE = 1106 +ER_WRONG_PARAMCOUNT_TO_PROCEDURE = 1107 +ER_WRONG_PARAMETERS_TO_PROCEDURE = 1108 +ER_UNKNOWN_TABLE = 1109 +ER_FIELD_SPECIFIED_TWICE = 1110 +ER_INVALID_GROUP_FUNC_USE = 1111 +ER_UNSUPPORTED_EXTENSION = 1112 +ER_TABLE_MUST_HAVE_COLUMNS = 1113 +ER_RECORD_FILE_FULL = 1114 +ER_UNKNOWN_CHARACTER_SET = 1115 +ER_TOO_MANY_TABLES = 1116 +ER_TOO_MANY_FIELDS = 1117 +ER_TOO_BIG_ROWSIZE = 1118 +ER_STACK_OVERRUN = 1119 +ER_WRONG_OUTER_JOIN_UNUSED = 1120 +ER_NULL_COLUMN_IN_INDEX = 1121 +ER_CANT_FIND_UDF = 1122 +ER_CANT_INITIALIZE_UDF = 1123 +ER_UDF_NO_PATHS = 1124 +ER_UDF_EXISTS = 1125 +ER_CANT_OPEN_LIBRARY = 1126 +ER_CANT_FIND_DL_ENTRY = 1127 +ER_FUNCTION_NOT_DEFINED = 1128 +ER_HOST_IS_BLOCKED = 1129 +ER_HOST_NOT_PRIVILEGED = 1130 +ER_PASSWORD_ANONYMOUS_USER = 1131 +ER_PASSWORD_NOT_ALLOWED = 1132 +ER_PASSWORD_NO_MATCH = 1133 +ER_UPDATE_INFO = 1134 +ER_CANT_CREATE_THREAD = 1135 +ER_WRONG_VALUE_COUNT_ON_ROW = 1136 +ER_CANT_REOPEN_TABLE = 1137 +ER_INVALID_USE_OF_NULL = 1138 +ER_REGEXP_ERROR = 1139 +ER_MIX_OF_GROUP_FUNC_AND_FIELDS = 1140 +ER_NONEXISTING_GRANT = 1141 +ER_TABLEACCESS_DENIED_ERROR = 1142 +ER_COLUMNACCESS_DENIED_ERROR = 1143 +ER_ILLEGAL_GRANT_FOR_TABLE = 1144 +ER_GRANT_WRONG_HOST_OR_USER = 1145 +ER_NO_SUCH_TABLE = 1146 +ER_NONEXISTING_TABLE_GRANT = 1147 +ER_NOT_ALLOWED_COMMAND = 1148 +ER_SYNTAX_ERROR = 1149 +OBSOLETE_ER_UNUSED1 = 1150 +OBSOLETE_ER_UNUSED2 = 1151 +ER_ABORTING_CONNECTION = 1152 +ER_NET_PACKET_TOO_LARGE = 1153 +ER_NET_READ_ERROR_FROM_PIPE = 1154 +ER_NET_FCNTL_ERROR = 1155 +ER_NET_PACKETS_OUT_OF_ORDER = 1156 +ER_NET_UNCOMPRESS_ERROR = 1157 +ER_NET_READ_ERROR = 1158 +ER_NET_READ_INTERRUPTED = 1159 +ER_NET_ERROR_ON_WRITE = 1160 +ER_NET_WRITE_INTERRUPTED = 1161 +ER_TOO_LONG_STRING = 1162 +ER_TABLE_CANT_HANDLE_BLOB = 1163 +ER_TABLE_CANT_HANDLE_AUTO_INCREMENT = 1164 +OBSOLETE_ER_UNUSED3 = 1165 +ER_WRONG_COLUMN_NAME = 1166 +ER_WRONG_KEY_COLUMN = 1167 +ER_WRONG_MRG_TABLE = 1168 +ER_DUP_UNIQUE = 1169 +ER_BLOB_KEY_WITHOUT_LENGTH = 1170 +ER_PRIMARY_CANT_HAVE_NULL = 1171 +ER_TOO_MANY_ROWS = 1172 +ER_REQUIRES_PRIMARY_KEY = 1173 +OBSOLETE_ER_NO_RAID_COMPILED = 1174 +ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE = 1175 +ER_KEY_DOES_NOT_EXITS = 1176 +ER_CHECK_NO_SUCH_TABLE = 1177 +ER_CHECK_NOT_IMPLEMENTED = 1178 +ER_CANT_DO_THIS_DURING_AN_TRANSACTION = 1179 +ER_ERROR_DURING_COMMIT = 1180 +ER_ERROR_DURING_ROLLBACK = 1181 +ER_ERROR_DURING_FLUSH_LOGS = 1182 +OBSOLETE_ER_ERROR_DURING_CHECKPOINT = 1183 +ER_NEW_ABORTING_CONNECTION = 1184 +OBSOLETE_ER_DUMP_NOT_IMPLEMENTED = 1185 +OBSOLETE_ER_FLUSH_MASTER_BINLOG_CLOSED = 1186 +OBSOLETE_ER_INDEX_REBUILD = 1187 +ER_MASTER = 1188 +ER_MASTER_NET_READ = 1189 +ER_MASTER_NET_WRITE = 1190 +ER_FT_MATCHING_KEY_NOT_FOUND = 1191 +ER_LOCK_OR_ACTIVE_TRANSACTION = 1192 +ER_UNKNOWN_SYSTEM_VARIABLE = 1193 +ER_CRASHED_ON_USAGE = 1194 +ER_CRASHED_ON_REPAIR = 1195 +ER_WARNING_NOT_COMPLETE_ROLLBACK = 1196 +ER_TRANS_CACHE_FULL = 1197 +OBSOLETE_ER_SLAVE_MUST_STOP = 1198 +ER_SLAVE_NOT_RUNNING = 1199 +ER_BAD_SLAVE = 1200 +ER_MASTER_INFO = 1201 +ER_SLAVE_THREAD = 1202 +ER_TOO_MANY_USER_CONNECTIONS = 1203 +ER_SET_CONSTANTS_ONLY = 1204 +ER_LOCK_WAIT_TIMEOUT = 1205 +ER_LOCK_TABLE_FULL = 1206 +ER_READ_ONLY_TRANSACTION = 1207 +OBSOLETE_ER_DROP_DB_WITH_READ_LOCK = 1208 +OBSOLETE_ER_CREATE_DB_WITH_READ_LOCK = 1209 +ER_WRONG_ARGUMENTS = 1210 +ER_NO_PERMISSION_TO_CREATE_USER = 1211 +OBSOLETE_ER_UNION_TABLES_IN_DIFFERENT_DIR = 1212 +ER_LOCK_DEADLOCK = 1213 +ER_TABLE_CANT_HANDLE_FT = 1214 +ER_CANNOT_ADD_FOREIGN = 1215 +ER_NO_REFERENCED_ROW = 1216 +ER_ROW_IS_REFERENCED = 1217 +ER_CONNECT_TO_MASTER = 1218 +OBSOLETE_ER_QUERY_ON_MASTER = 1219 +ER_ERROR_WHEN_EXECUTING_COMMAND = 1220 +ER_WRONG_USAGE = 1221 +ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT = 1222 +ER_CANT_UPDATE_WITH_READLOCK = 1223 +ER_MIXING_NOT_ALLOWED = 1224 +ER_DUP_ARGUMENT = 1225 +ER_USER_LIMIT_REACHED = 1226 +ER_SPECIFIC_ACCESS_DENIED_ERROR = 1227 +ER_LOCAL_VARIABLE = 1228 +ER_GLOBAL_VARIABLE = 1229 +ER_NO_DEFAULT = 1230 +ER_WRONG_VALUE_FOR_VAR = 1231 +ER_WRONG_TYPE_FOR_VAR = 1232 +ER_VAR_CANT_BE_READ = 1233 +ER_CANT_USE_OPTION_HERE = 1234 +ER_NOT_SUPPORTED_YET = 1235 +ER_MASTER_FATAL_ERROR_READING_BINLOG = 1236 +ER_SLAVE_IGNORED_TABLE = 1237 +ER_INCORRECT_GLOBAL_LOCAL_VAR = 1238 +ER_WRONG_FK_DEF = 1239 +ER_KEY_REF_DO_NOT_MATCH_TABLE_REF = 1240 +ER_OPERAND_COLUMNS = 1241 +ER_SUBQUERY_NO_1_ROW = 1242 +ER_UNKNOWN_STMT_HANDLER = 1243 +ER_CORRUPT_HELP_DB = 1244 +OBSOLETE_ER_CYCLIC_REFERENCE = 1245 +ER_AUTO_CONVERT = 1246 +ER_ILLEGAL_REFERENCE = 1247 +ER_DERIVED_MUST_HAVE_ALIAS = 1248 +ER_SELECT_REDUCED = 1249 +ER_TABLENAME_NOT_ALLOWED_HERE = 1250 +ER_NOT_SUPPORTED_AUTH_MODE = 1251 +ER_SPATIAL_CANT_HAVE_NULL = 1252 +ER_COLLATION_CHARSET_MISMATCH = 1253 +OBSOLETE_ER_SLAVE_WAS_RUNNING = 1254 +OBSOLETE_ER_SLAVE_WAS_NOT_RUNNING = 1255 +ER_TOO_BIG_FOR_UNCOMPRESS = 1256 +ER_ZLIB_Z_MEM_ERROR = 1257 +ER_ZLIB_Z_BUF_ERROR = 1258 +ER_ZLIB_Z_DATA_ERROR = 1259 +ER_CUT_VALUE_GROUP_CONCAT = 1260 +ER_WARN_TOO_FEW_RECORDS = 1261 +ER_WARN_TOO_MANY_RECORDS = 1262 +ER_WARN_NULL_TO_NOTNULL = 1263 +ER_WARN_DATA_OUT_OF_RANGE = 1264 +WARN_DATA_TRUNCATED = 1265 +ER_WARN_USING_OTHER_HANDLER = 1266 +ER_CANT_AGGREGATE_2COLLATIONS = 1267 +OBSOLETE_ER_DROP_USER = 1268 +ER_REVOKE_GRANTS = 1269 +ER_CANT_AGGREGATE_3COLLATIONS = 1270 +ER_CANT_AGGREGATE_NCOLLATIONS = 1271 +ER_VARIABLE_IS_NOT_STRUCT = 1272 +ER_UNKNOWN_COLLATION = 1273 +ER_SLAVE_IGNORED_SSL_PARAMS = 1274 +OBSOLETE_ER_SERVER_IS_IN_SECURE_AUTH_MODE = 1275 +ER_WARN_FIELD_RESOLVED = 1276 +ER_BAD_SLAVE_UNTIL_COND = 1277 +ER_MISSING_SKIP_SLAVE = 1278 +ER_UNTIL_COND_IGNORED = 1279 +ER_WRONG_NAME_FOR_INDEX = 1280 +ER_WRONG_NAME_FOR_CATALOG = 1281 +OBSOLETE_ER_WARN_QC_RESIZE = 1282 +ER_BAD_FT_COLUMN = 1283 +ER_UNKNOWN_KEY_CACHE = 1284 +ER_WARN_HOSTNAME_WONT_WORK = 1285 +ER_UNKNOWN_STORAGE_ENGINE = 1286 +ER_WARN_DEPRECATED_SYNTAX = 1287 +ER_NON_UPDATABLE_TABLE = 1288 +ER_FEATURE_DISABLED = 1289 +ER_OPTION_PREVENTS_STATEMENT = 1290 +ER_DUPLICATED_VALUE_IN_TYPE = 1291 +ER_TRUNCATED_WRONG_VALUE = 1292 +OBSOLETE_ER_TOO_MUCH_AUTO_TIMESTAMP_COLS = 1293 +ER_INVALID_ON_UPDATE = 1294 +ER_UNSUPPORTED_PS = 1295 +ER_GET_ERRMSG = 1296 +ER_GET_TEMPORARY_ERRMSG = 1297 +ER_UNKNOWN_TIME_ZONE = 1298 +ER_WARN_INVALID_TIMESTAMP = 1299 +ER_INVALID_CHARACTER_STRING = 1300 +ER_WARN_ALLOWED_PACKET_OVERFLOWED = 1301 +ER_CONFLICTING_DECLARATIONS = 1302 +ER_SP_NO_RECURSIVE_CREATE = 1303 +ER_SP_ALREADY_EXISTS = 1304 +ER_SP_DOES_NOT_EXIST = 1305 +ER_SP_DROP_FAILED = 1306 +ER_SP_STORE_FAILED = 1307 +ER_SP_LILABEL_MISMATCH = 1308 +ER_SP_LABEL_REDEFINE = 1309 +ER_SP_LABEL_MISMATCH = 1310 +ER_SP_UNINIT_VAR = 1311 +ER_SP_BADSELECT = 1312 +ER_SP_BADRETURN = 1313 +ER_SP_BADSTATEMENT = 1314 +ER_UPDATE_LOG_DEPRECATED_IGNORED = 1315 +ER_UPDATE_LOG_DEPRECATED_TRANSLATED = 1316 +ER_QUERY_INTERRUPTED = 1317 +ER_SP_WRONG_NO_OF_ARGS = 1318 +ER_SP_COND_MISMATCH = 1319 +ER_SP_NORETURN = 1320 +ER_SP_NORETURNEND = 1321 +ER_SP_BAD_CURSOR_QUERY = 1322 +ER_SP_BAD_CURSOR_SELECT = 1323 +ER_SP_CURSOR_MISMATCH = 1324 +ER_SP_CURSOR_ALREADY_OPEN = 1325 +ER_SP_CURSOR_NOT_OPEN = 1326 +ER_SP_UNDECLARED_VAR = 1327 +ER_SP_WRONG_NO_OF_FETCH_ARGS = 1328 +ER_SP_FETCH_NO_DATA = 1329 +ER_SP_DUP_PARAM = 1330 +ER_SP_DUP_VAR = 1331 +ER_SP_DUP_COND = 1332 +ER_SP_DUP_CURS = 1333 +ER_SP_CANT_ALTER = 1334 +ER_SP_SUBSELECT_NYI = 1335 +ER_STMT_NOT_ALLOWED_IN_SF_OR_TRG = 1336 +ER_SP_VARCOND_AFTER_CURSHNDLR = 1337 +ER_SP_CURSOR_AFTER_HANDLER = 1338 +ER_SP_CASE_NOT_FOUND = 1339 +ER_FPARSER_TOO_BIG_FILE = 1340 +ER_FPARSER_BAD_HEADER = 1341 +ER_FPARSER_EOF_IN_COMMENT = 1342 +ER_FPARSER_ERROR_IN_PARAMETER = 1343 +ER_FPARSER_EOF_IN_UNKNOWN_PARAMETER = 1344 +ER_VIEW_NO_EXPLAIN = 1345 +OBSOLETE_ER_FRM_UNKNOWN_TYPE = 1346 +ER_WRONG_OBJECT = 1347 +ER_NONUPDATEABLE_COLUMN = 1348 +OBSOLETE_ER_VIEW_SELECT_DERIVED_UNUSED = 1349 +ER_VIEW_SELECT_CLAUSE = 1350 +ER_VIEW_SELECT_VARIABLE = 1351 +ER_VIEW_SELECT_TMPTABLE = 1352 +ER_VIEW_WRONG_LIST = 1353 +ER_WARN_VIEW_MERGE = 1354 +ER_WARN_VIEW_WITHOUT_KEY = 1355 +ER_VIEW_INVALID = 1356 +ER_SP_NO_DROP_SP = 1357 +OBSOLETE_ER_SP_GOTO_IN_HNDLR = 1358 +ER_TRG_ALREADY_EXISTS = 1359 +ER_TRG_DOES_NOT_EXIST = 1360 +ER_TRG_ON_VIEW_OR_TEMP_TABLE = 1361 +ER_TRG_CANT_CHANGE_ROW = 1362 +ER_TRG_NO_SUCH_ROW_IN_TRG = 1363 +ER_NO_DEFAULT_FOR_FIELD = 1364 +ER_DIVISION_BY_ZERO = 1365 +ER_TRUNCATED_WRONG_VALUE_FOR_FIELD = 1366 +ER_ILLEGAL_VALUE_FOR_TYPE = 1367 +ER_VIEW_NONUPD_CHECK = 1368 +ER_VIEW_CHECK_FAILED = 1369 +ER_PROCACCESS_DENIED_ERROR = 1370 +ER_RELAY_LOG_FAIL = 1371 +OBSOLETE_ER_PASSWD_LENGTH = 1372 +ER_UNKNOWN_TARGET_BINLOG = 1373 +ER_IO_ERR_LOG_INDEX_READ = 1374 +ER_BINLOG_PURGE_PROHIBITED = 1375 +ER_FSEEK_FAIL = 1376 +ER_BINLOG_PURGE_FATAL_ERR = 1377 +ER_LOG_IN_USE = 1378 +ER_LOG_PURGE_UNKNOWN_ERR = 1379 +ER_RELAY_LOG_INIT = 1380 +ER_NO_BINARY_LOGGING = 1381 +ER_RESERVED_SYNTAX = 1382 +OBSOLETE_ER_WSAS_FAILED = 1383 +OBSOLETE_ER_DIFF_GROUPS_PROC = 1384 +OBSOLETE_ER_NO_GROUP_FOR_PROC = 1385 +OBSOLETE_ER_ORDER_WITH_PROC = 1386 +OBSOLETE_ER_LOGGING_PROHIBIT_CHANGING_OF = 1387 +OBSOLETE_ER_NO_FILE_MAPPING = 1388 +OBSOLETE_ER_WRONG_MAGIC = 1389 +ER_PS_MANY_PARAM = 1390 +ER_KEY_PART_0 = 1391 +ER_VIEW_CHECKSUM = 1392 +ER_VIEW_MULTIUPDATE = 1393 +ER_VIEW_NO_INSERT_FIELD_LIST = 1394 +ER_VIEW_DELETE_MERGE_VIEW = 1395 +ER_CANNOT_USER = 1396 +ER_XAER_NOTA = 1397 +ER_XAER_INVAL = 1398 +ER_XAER_RMFAIL = 1399 +ER_XAER_OUTSIDE = 1400 +ER_XAER_RMERR = 1401 +ER_XA_RBROLLBACK = 1402 +ER_NONEXISTING_PROC_GRANT = 1403 +ER_PROC_AUTO_GRANT_FAIL = 1404 +ER_PROC_AUTO_REVOKE_FAIL = 1405 +ER_DATA_TOO_LONG = 1406 +ER_SP_BAD_SQLSTATE = 1407 +ER_STARTUP = 1408 +ER_LOAD_FROM_FIXED_SIZE_ROWS_TO_VAR = 1409 +ER_CANT_CREATE_USER_WITH_GRANT = 1410 +ER_WRONG_VALUE_FOR_TYPE = 1411 +ER_TABLE_DEF_CHANGED = 1412 +ER_SP_DUP_HANDLER = 1413 +ER_SP_NOT_VAR_ARG = 1414 +ER_SP_NO_RETSET = 1415 +ER_CANT_CREATE_GEOMETRY_OBJECT = 1416 +OBSOLETE_ER_FAILED_ROUTINE_BREAK_BINLOG = 1417 +ER_BINLOG_UNSAFE_ROUTINE = 1418 +ER_BINLOG_CREATE_ROUTINE_NEED_SUPER = 1419 +OBSOLETE_ER_EXEC_STMT_WITH_OPEN_CURSOR = 1420 +ER_STMT_HAS_NO_OPEN_CURSOR = 1421 +ER_COMMIT_NOT_ALLOWED_IN_SF_OR_TRG = 1422 +ER_NO_DEFAULT_FOR_VIEW_FIELD = 1423 +ER_SP_NO_RECURSION = 1424 +ER_TOO_BIG_SCALE = 1425 +ER_TOO_BIG_PRECISION = 1426 +ER_M_BIGGER_THAN_D = 1427 +ER_WRONG_LOCK_OF_SYSTEM_TABLE = 1428 +ER_CONNECT_TO_FOREIGN_DATA_SOURCE = 1429 +ER_QUERY_ON_FOREIGN_DATA_SOURCE = 1430 +ER_FOREIGN_DATA_SOURCE_DOESNT_EXIST = 1431 +ER_FOREIGN_DATA_STRING_INVALID_CANT_CREATE = 1432 +ER_FOREIGN_DATA_STRING_INVALID = 1433 +OBSOLETE_ER_CANT_CREATE_FEDERATED_TABLE = 1434 +ER_TRG_IN_WRONG_SCHEMA = 1435 +ER_STACK_OVERRUN_NEED_MORE = 1436 +ER_TOO_LONG_BODY = 1437 +ER_WARN_CANT_DROP_DEFAULT_KEYCACHE = 1438 +ER_TOO_BIG_DISPLAYWIDTH = 1439 +ER_XAER_DUPID = 1440 +ER_DATETIME_FUNCTION_OVERFLOW = 1441 +ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG = 1442 +ER_VIEW_PREVENT_UPDATE = 1443 +ER_PS_NO_RECURSION = 1444 +ER_SP_CANT_SET_AUTOCOMMIT = 1445 +OBSOLETE_ER_MALFORMED_DEFINER = 1446 +ER_VIEW_FRM_NO_USER = 1447 +ER_VIEW_OTHER_USER = 1448 +ER_NO_SUCH_USER = 1449 +ER_FORBID_SCHEMA_CHANGE = 1450 +ER_ROW_IS_REFERENCED_2 = 1451 +ER_NO_REFERENCED_ROW_2 = 1452 +ER_SP_BAD_VAR_SHADOW = 1453 +ER_TRG_NO_DEFINER = 1454 +ER_OLD_FILE_FORMAT = 1455 +ER_SP_RECURSION_LIMIT = 1456 +OBSOLETE_ER_SP_PROC_TABLE_CORRUPT = 1457 +ER_SP_WRONG_NAME = 1458 +ER_TABLE_NEEDS_UPGRADE = 1459 +ER_SP_NO_AGGREGATE = 1460 +ER_MAX_PREPARED_STMT_COUNT_REACHED = 1461 +ER_VIEW_RECURSIVE = 1462 +ER_NON_GROUPING_FIELD_USED = 1463 +ER_TABLE_CANT_HANDLE_SPKEYS = 1464 +ER_NO_TRIGGERS_ON_SYSTEM_SCHEMA = 1465 +ER_REMOVED_SPACES = 1466 +ER_AUTOINC_READ_FAILED = 1467 +ER_USERNAME = 1468 +ER_HOSTNAME = 1469 +ER_WRONG_STRING_LENGTH = 1470 +ER_NON_INSERTABLE_TABLE = 1471 +ER_ADMIN_WRONG_MRG_TABLE = 1472 +ER_TOO_HIGH_LEVEL_OF_NESTING_FOR_SELECT = 1473 +ER_NAME_BECOMES_EMPTY = 1474 +ER_AMBIGUOUS_FIELD_TERM = 1475 +ER_FOREIGN_SERVER_EXISTS = 1476 +ER_FOREIGN_SERVER_DOESNT_EXIST = 1477 +ER_ILLEGAL_HA_CREATE_OPTION = 1478 +ER_PARTITION_REQUIRES_VALUES_ERROR = 1479 +ER_PARTITION_WRONG_VALUES_ERROR = 1480 +ER_PARTITION_MAXVALUE_ERROR = 1481 +OBSOLETE_ER_PARTITION_SUBPARTITION_ERROR = 1482 +OBSOLETE_ER_PARTITION_SUBPART_MIX_ERROR = 1483 +ER_PARTITION_WRONG_NO_PART_ERROR = 1484 +ER_PARTITION_WRONG_NO_SUBPART_ERROR = 1485 +ER_WRONG_EXPR_IN_PARTITION_FUNC_ERROR = 1486 +OBSOLETE_ER_NO_CONST_EXPR_IN_RANGE_OR_LIST_ERROR = 1487 +ER_FIELD_NOT_FOUND_PART_ERROR = 1488 +OBSOLETE_ER_LIST_OF_FIELDS_ONLY_IN_HASH_ERROR = 1489 +ER_INCONSISTENT_PARTITION_INFO_ERROR = 1490 +ER_PARTITION_FUNC_NOT_ALLOWED_ERROR = 1491 +ER_PARTITIONS_MUST_BE_DEFINED_ERROR = 1492 +ER_RANGE_NOT_INCREASING_ERROR = 1493 +ER_INCONSISTENT_TYPE_OF_FUNCTIONS_ERROR = 1494 +ER_MULTIPLE_DEF_CONST_IN_LIST_PART_ERROR = 1495 +ER_PARTITION_ENTRY_ERROR = 1496 +ER_MIX_HANDLER_ERROR = 1497 +ER_PARTITION_NOT_DEFINED_ERROR = 1498 +ER_TOO_MANY_PARTITIONS_ERROR = 1499 +ER_SUBPARTITION_ERROR = 1500 +ER_CANT_CREATE_HANDLER_FILE = 1501 +ER_BLOB_FIELD_IN_PART_FUNC_ERROR = 1502 +ER_UNIQUE_KEY_NEED_ALL_FIELDS_IN_PF = 1503 +ER_NO_PARTS_ERROR = 1504 +ER_PARTITION_MGMT_ON_NONPARTITIONED = 1505 +ER_FOREIGN_KEY_ON_PARTITIONED = 1506 +ER_DROP_PARTITION_NON_EXISTENT = 1507 +ER_DROP_LAST_PARTITION = 1508 +ER_COALESCE_ONLY_ON_HASH_PARTITION = 1509 +ER_REORG_HASH_ONLY_ON_SAME_NO = 1510 +ER_REORG_NO_PARAM_ERROR = 1511 +ER_ONLY_ON_RANGE_LIST_PARTITION = 1512 +ER_ADD_PARTITION_SUBPART_ERROR = 1513 +ER_ADD_PARTITION_NO_NEW_PARTITION = 1514 +ER_COALESCE_PARTITION_NO_PARTITION = 1515 +ER_REORG_PARTITION_NOT_EXIST = 1516 +ER_SAME_NAME_PARTITION = 1517 +ER_NO_BINLOG_ERROR = 1518 +ER_CONSECUTIVE_REORG_PARTITIONS = 1519 +ER_REORG_OUTSIDE_RANGE = 1520 +ER_PARTITION_FUNCTION_FAILURE = 1521 +OBSOLETE_ER_PART_STATE_ERROR = 1522 +ER_LIMITED_PART_RANGE = 1523 +ER_PLUGIN_IS_NOT_LOADED = 1524 +ER_WRONG_VALUE = 1525 +ER_NO_PARTITION_FOR_GIVEN_VALUE = 1526 +ER_FILEGROUP_OPTION_ONLY_ONCE = 1527 +ER_CREATE_FILEGROUP_FAILED = 1528 +ER_DROP_FILEGROUP_FAILED = 1529 +ER_TABLESPACE_AUTO_EXTEND_ERROR = 1530 +ER_WRONG_SIZE_NUMBER = 1531 +ER_SIZE_OVERFLOW_ERROR = 1532 +ER_ALTER_FILEGROUP_FAILED = 1533 +ER_BINLOG_ROW_LOGGING_FAILED = 1534 +OBSOLETE_ER_BINLOG_ROW_WRONG_TABLE_DEF = 1535 +OBSOLETE_ER_BINLOG_ROW_RBR_TO_SBR = 1536 +ER_EVENT_ALREADY_EXISTS = 1537 +OBSOLETE_ER_EVENT_STORE_FAILED = 1538 +ER_EVENT_DOES_NOT_EXIST = 1539 +OBSOLETE_ER_EVENT_CANT_ALTER = 1540 +OBSOLETE_ER_EVENT_DROP_FAILED = 1541 +ER_EVENT_INTERVAL_NOT_POSITIVE_OR_TOO_BIG = 1542 +ER_EVENT_ENDS_BEFORE_STARTS = 1543 +ER_EVENT_EXEC_TIME_IN_THE_PAST = 1544 +OBSOLETE_ER_EVENT_OPEN_TABLE_FAILED = 1545 +OBSOLETE_ER_EVENT_NEITHER_M_EXPR_NOR_M_AT = 1546 +OBSOLETE_ER_COL_COUNT_DOESNT_MATCH_CORRUPTED = 1547 +OBSOLETE_ER_CANNOT_LOAD_FROM_TABLE = 1548 +OBSOLETE_ER_EVENT_CANNOT_DELETE = 1549 +OBSOLETE_ER_EVENT_COMPILE_ERROR = 1550 +ER_EVENT_SAME_NAME = 1551 +OBSOLETE_ER_EVENT_DATA_TOO_LONG = 1552 +ER_DROP_INDEX_FK = 1553 +ER_WARN_DEPRECATED_SYNTAX_WITH_VER = 1554 +OBSOLETE_ER_CANT_WRITE_LOCK_LOG_TABLE = 1555 +ER_CANT_LOCK_LOG_TABLE = 1556 +ER_FOREIGN_DUPLICATE_KEY_OLD_UNUSED = 1557 +ER_COL_COUNT_DOESNT_MATCH_PLEASE_UPDATE = 1558 +OBSOLETE_ER_TEMP_TABLE_PREVENTS_SWITCH_OUT_OF_RBR = 1559 +ER_STORED_FUNCTION_PREVENTS_SWITCH_BINLOG_FORMAT = 1560 +OBSOLETE_ER_NDB_CANT_SWITCH_BINLOG_FORMAT = 1561 +ER_PARTITION_NO_TEMPORARY = 1562 +ER_PARTITION_CONST_DOMAIN_ERROR = 1563 +ER_PARTITION_FUNCTION_IS_NOT_ALLOWED = 1564 +OBSOLETE_ER_DDL_LOG_ERROR_UNUSED = 1565 +ER_NULL_IN_VALUES_LESS_THAN = 1566 +ER_WRONG_PARTITION_NAME = 1567 +ER_CANT_CHANGE_TX_CHARACTERISTICS = 1568 +ER_DUP_ENTRY_AUTOINCREMENT_CASE = 1569 +OBSOLETE_ER_EVENT_MODIFY_QUEUE_ERROR = 1570 +ER_EVENT_SET_VAR_ERROR = 1571 +ER_PARTITION_MERGE_ERROR = 1572 +OBSOLETE_ER_CANT_ACTIVATE_LOG = 1573 +OBSOLETE_ER_RBR_NOT_AVAILABLE = 1574 +ER_BASE64_DECODE_ERROR = 1575 +ER_EVENT_RECURSION_FORBIDDEN = 1576 +OBSOLETE_ER_EVENTS_DB_ERROR = 1577 +ER_ONLY_INTEGERS_ALLOWED = 1578 +ER_UNSUPORTED_LOG_ENGINE = 1579 +ER_BAD_LOG_STATEMENT = 1580 +ER_CANT_RENAME_LOG_TABLE = 1581 +ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT = 1582 +ER_WRONG_PARAMETERS_TO_NATIVE_FCT = 1583 +ER_WRONG_PARAMETERS_TO_STORED_FCT = 1584 +ER_NATIVE_FCT_NAME_COLLISION = 1585 +ER_DUP_ENTRY_WITH_KEY_NAME = 1586 +ER_BINLOG_PURGE_EMFILE = 1587 +ER_EVENT_CANNOT_CREATE_IN_THE_PAST = 1588 +ER_EVENT_CANNOT_ALTER_IN_THE_PAST = 1589 +OBSOLETE_ER_SLAVE_INCIDENT = 1590 +ER_NO_PARTITION_FOR_GIVEN_VALUE_SILENT = 1591 +ER_BINLOG_UNSAFE_STATEMENT = 1592 +ER_BINLOG_FATAL_ERROR = 1593 +OBSOLETE_ER_SLAVE_RELAY_LOG_READ_FAILURE = 1594 +OBSOLETE_ER_SLAVE_RELAY_LOG_WRITE_FAILURE = 1595 +OBSOLETE_ER_SLAVE_CREATE_EVENT_FAILURE = 1596 +OBSOLETE_ER_SLAVE_MASTER_COM_FAILURE = 1597 +ER_BINLOG_LOGGING_IMPOSSIBLE = 1598 +ER_VIEW_NO_CREATION_CTX = 1599 +ER_VIEW_INVALID_CREATION_CTX = 1600 +OBSOLETE_ER_SR_INVALID_CREATION_CTX = 1601 +ER_TRG_CORRUPTED_FILE = 1602 +ER_TRG_NO_CREATION_CTX = 1603 +ER_TRG_INVALID_CREATION_CTX = 1604 +ER_EVENT_INVALID_CREATION_CTX = 1605 +ER_TRG_CANT_OPEN_TABLE = 1606 +OBSOLETE_ER_CANT_CREATE_SROUTINE = 1607 +OBSOLETE_ER_NEVER_USED = 1608 +ER_NO_FORMAT_DESCRIPTION_EVENT_BEFORE_BINLOG_STATEMENT = 1609 +ER_SLAVE_CORRUPT_EVENT = 1610 +OBSOLETE_ER_LOAD_DATA_INVALID_COLUMN_UNUSED = 1611 +ER_LOG_PURGE_NO_FILE = 1612 +ER_XA_RBTIMEOUT = 1613 +ER_XA_RBDEADLOCK = 1614 +ER_NEED_REPREPARE = 1615 +OBSOLETE_ER_DELAYED_NOT_SUPPORTED = 1616 +WARN_NO_MASTER_INFO = 1617 +WARN_OPTION_IGNORED = 1618 +ER_PLUGIN_DELETE_BUILTIN = 1619 +WARN_PLUGIN_BUSY = 1620 +ER_VARIABLE_IS_READONLY = 1621 +ER_WARN_ENGINE_TRANSACTION_ROLLBACK = 1622 +OBSOLETE_ER_SLAVE_HEARTBEAT_FAILURE = 1623 +ER_SLAVE_HEARTBEAT_VALUE_OUT_OF_RANGE = 1624 +ER_NDB_REPLICATION_SCHEMA_ERROR = 1625 +ER_CONFLICT_FN_PARSE_ERROR = 1626 +ER_EXCEPTIONS_WRITE_ERROR = 1627 +ER_TOO_LONG_TABLE_COMMENT = 1628 +ER_TOO_LONG_FIELD_COMMENT = 1629 +ER_FUNC_INEXISTENT_NAME_COLLISION = 1630 +ER_DATABASE_NAME = 1631 +ER_TABLE_NAME = 1632 +ER_PARTITION_NAME = 1633 +ER_SUBPARTITION_NAME = 1634 +ER_TEMPORARY_NAME = 1635 +ER_RENAMED_NAME = 1636 +ER_TOO_MANY_CONCURRENT_TRXS = 1637 +WARN_NON_ASCII_SEPARATOR_NOT_IMPLEMENTED = 1638 +ER_DEBUG_SYNC_TIMEOUT = 1639 +ER_DEBUG_SYNC_HIT_LIMIT = 1640 +ER_DUP_SIGNAL_SET = 1641 +ER_SIGNAL_WARN = 1642 +ER_SIGNAL_NOT_FOUND = 1643 +ER_SIGNAL_EXCEPTION = 1644 +ER_RESIGNAL_WITHOUT_ACTIVE_HANDLER = 1645 +ER_SIGNAL_BAD_CONDITION_TYPE = 1646 +WARN_COND_ITEM_TRUNCATED = 1647 +ER_COND_ITEM_TOO_LONG = 1648 +ER_UNKNOWN_LOCALE = 1649 +ER_SLAVE_IGNORE_SERVER_IDS = 1650 +OBSOLETE_ER_QUERY_CACHE_DISABLED = 1651 +ER_SAME_NAME_PARTITION_FIELD = 1652 +ER_PARTITION_COLUMN_LIST_ERROR = 1653 +ER_WRONG_TYPE_COLUMN_VALUE_ERROR = 1654 +ER_TOO_MANY_PARTITION_FUNC_FIELDS_ERROR = 1655 +ER_MAXVALUE_IN_VALUES_IN = 1656 +ER_TOO_MANY_VALUES_ERROR = 1657 +ER_ROW_SINGLE_PARTITION_FIELD_ERROR = 1658 +ER_FIELD_TYPE_NOT_ALLOWED_AS_PARTITION_FIELD = 1659 +ER_PARTITION_FIELDS_TOO_LONG = 1660 +ER_BINLOG_ROW_ENGINE_AND_STMT_ENGINE = 1661 +ER_BINLOG_ROW_MODE_AND_STMT_ENGINE = 1662 +ER_BINLOG_UNSAFE_AND_STMT_ENGINE = 1663 +ER_BINLOG_ROW_INJECTION_AND_STMT_ENGINE = 1664 +ER_BINLOG_STMT_MODE_AND_ROW_ENGINE = 1665 +ER_BINLOG_ROW_INJECTION_AND_STMT_MODE = 1666 +ER_BINLOG_MULTIPLE_ENGINES_AND_SELF_LOGGING_ENGINE = 1667 +ER_BINLOG_UNSAFE_LIMIT = 1668 +OBSOLETE_ER_UNUSED4 = 1669 +ER_BINLOG_UNSAFE_SYSTEM_TABLE = 1670 +ER_BINLOG_UNSAFE_AUTOINC_COLUMNS = 1671 +ER_BINLOG_UNSAFE_UDF = 1672 +ER_BINLOG_UNSAFE_SYSTEM_VARIABLE = 1673 +ER_BINLOG_UNSAFE_SYSTEM_FUNCTION = 1674 +ER_BINLOG_UNSAFE_NONTRANS_AFTER_TRANS = 1675 +ER_MESSAGE_AND_STATEMENT = 1676 +OBSOLETE_ER_SLAVE_CONVERSION_FAILED = 1677 +ER_SLAVE_CANT_CREATE_CONVERSION = 1678 +ER_INSIDE_TRANSACTION_PREVENTS_SWITCH_BINLOG_FORMAT = 1679 +ER_PATH_LENGTH = 1680 +ER_WARN_DEPRECATED_SYNTAX_NO_REPLACEMENT = 1681 +ER_WRONG_NATIVE_TABLE_STRUCTURE = 1682 +ER_WRONG_PERFSCHEMA_USAGE = 1683 +ER_WARN_I_S_SKIPPED_TABLE = 1684 +ER_INSIDE_TRANSACTION_PREVENTS_SWITCH_BINLOG_DIRECT = 1685 +ER_STORED_FUNCTION_PREVENTS_SWITCH_BINLOG_DIRECT = 1686 +ER_SPATIAL_MUST_HAVE_GEOM_COL = 1687 +ER_TOO_LONG_INDEX_COMMENT = 1688 +ER_LOCK_ABORTED = 1689 +ER_DATA_OUT_OF_RANGE = 1690 +OBSOLETE_ER_WRONG_SPVAR_TYPE_IN_LIMIT = 1691 +ER_BINLOG_UNSAFE_MULTIPLE_ENGINES_AND_SELF_LOGGING_ENGINE = 1692 +ER_BINLOG_UNSAFE_MIXED_STATEMENT = 1693 +ER_INSIDE_TRANSACTION_PREVENTS_SWITCH_SQL_LOG_BIN = 1694 +ER_STORED_FUNCTION_PREVENTS_SWITCH_SQL_LOG_BIN = 1695 +ER_FAILED_READ_FROM_PAR_FILE = 1696 +ER_VALUES_IS_NOT_INT_TYPE_ERROR = 1697 +ER_ACCESS_DENIED_NO_PASSWORD_ERROR = 1698 +ER_SET_PASSWORD_AUTH_PLUGIN = 1699 +OBSOLETE_ER_GRANT_PLUGIN_USER_EXISTS = 1700 +ER_TRUNCATE_ILLEGAL_FK = 1701 +ER_PLUGIN_IS_PERMANENT = 1702 +ER_SLAVE_HEARTBEAT_VALUE_OUT_OF_RANGE_MIN = 1703 +ER_SLAVE_HEARTBEAT_VALUE_OUT_OF_RANGE_MAX = 1704 +ER_STMT_CACHE_FULL = 1705 +ER_MULTI_UPDATE_KEY_CONFLICT = 1706 +ER_TABLE_NEEDS_REBUILD = 1707 +WARN_OPTION_BELOW_LIMIT = 1708 +ER_INDEX_COLUMN_TOO_LONG = 1709 +ER_ERROR_IN_TRIGGER_BODY = 1710 +ER_ERROR_IN_UNKNOWN_TRIGGER_BODY = 1711 +ER_INDEX_CORRUPT = 1712 +ER_UNDO_RECORD_TOO_BIG = 1713 +ER_BINLOG_UNSAFE_INSERT_IGNORE_SELECT = 1714 +ER_BINLOG_UNSAFE_INSERT_SELECT_UPDATE = 1715 +ER_BINLOG_UNSAFE_REPLACE_SELECT = 1716 +ER_BINLOG_UNSAFE_CREATE_IGNORE_SELECT = 1717 +ER_BINLOG_UNSAFE_CREATE_REPLACE_SELECT = 1718 +ER_BINLOG_UNSAFE_UPDATE_IGNORE = 1719 +ER_PLUGIN_NO_UNINSTALL = 1720 +ER_PLUGIN_NO_INSTALL = 1721 +ER_BINLOG_UNSAFE_WRITE_AUTOINC_SELECT = 1722 +ER_BINLOG_UNSAFE_CREATE_SELECT_AUTOINC = 1723 +ER_BINLOG_UNSAFE_INSERT_TWO_KEYS = 1724 +ER_TABLE_IN_FK_CHECK = 1725 +ER_UNSUPPORTED_ENGINE = 1726 +ER_BINLOG_UNSAFE_AUTOINC_NOT_FIRST = 1727 +ER_CANNOT_LOAD_FROM_TABLE_V2 = 1728 +ER_MASTER_DELAY_VALUE_OUT_OF_RANGE = 1729 +ER_ONLY_FD_AND_RBR_EVENTS_ALLOWED_IN_BINLOG_STATEMENT = 1730 +ER_PARTITION_EXCHANGE_DIFFERENT_OPTION = 1731 +ER_PARTITION_EXCHANGE_PART_TABLE = 1732 +ER_PARTITION_EXCHANGE_TEMP_TABLE = 1733 +ER_PARTITION_INSTEAD_OF_SUBPARTITION = 1734 +ER_UNKNOWN_PARTITION = 1735 +ER_TABLES_DIFFERENT_METADATA = 1736 +ER_ROW_DOES_NOT_MATCH_PARTITION = 1737 +ER_BINLOG_CACHE_SIZE_GREATER_THAN_MAX = 1738 +ER_WARN_INDEX_NOT_APPLICABLE = 1739 +ER_PARTITION_EXCHANGE_FOREIGN_KEY = 1740 +OBSOLETE_ER_NO_SUCH_KEY_VALUE = 1741 +ER_RPL_INFO_DATA_TOO_LONG = 1742 +OBSOLETE_ER_NETWORK_READ_EVENT_CHECKSUM_FAILURE = 1743 +OBSOLETE_ER_BINLOG_READ_EVENT_CHECKSUM_FAILURE = 1744 +ER_BINLOG_STMT_CACHE_SIZE_GREATER_THAN_MAX = 1745 +ER_CANT_UPDATE_TABLE_IN_CREATE_TABLE_SELECT = 1746 +ER_PARTITION_CLAUSE_ON_NONPARTITIONED = 1747 +ER_ROW_DOES_NOT_MATCH_GIVEN_PARTITION_SET = 1748 +OBSOLETE_ER_NO_SUCH_PARTITION__UNUSED = 1749 +ER_CHANGE_RPL_INFO_REPOSITORY_FAILURE = 1750 +ER_WARNING_NOT_COMPLETE_ROLLBACK_WITH_CREATED_TEMP_TABLE = 1751 +ER_WARNING_NOT_COMPLETE_ROLLBACK_WITH_DROPPED_TEMP_TABLE = 1752 +ER_MTS_FEATURE_IS_NOT_SUPPORTED = 1753 +ER_MTS_UPDATED_DBS_GREATER_MAX = 1754 +ER_MTS_CANT_PARALLEL = 1755 +ER_MTS_INCONSISTENT_DATA = 1756 +ER_FULLTEXT_NOT_SUPPORTED_WITH_PARTITIONING = 1757 +ER_DA_INVALID_CONDITION_NUMBER = 1758 +ER_INSECURE_PLAIN_TEXT = 1759 +ER_INSECURE_CHANGE_MASTER = 1760 +ER_FOREIGN_DUPLICATE_KEY_WITH_CHILD_INFO = 1761 +ER_FOREIGN_DUPLICATE_KEY_WITHOUT_CHILD_INFO = 1762 +ER_SQLTHREAD_WITH_SECURE_SLAVE = 1763 +ER_TABLE_HAS_NO_FT = 1764 +ER_VARIABLE_NOT_SETTABLE_IN_SF_OR_TRIGGER = 1765 +ER_VARIABLE_NOT_SETTABLE_IN_TRANSACTION = 1766 +OBSOLETE_ER_GTID_NEXT_IS_NOT_IN_GTID_NEXT_LIST = 1767 +OBSOLETE_ER_CANT_CHANGE_GTID_NEXT_IN_TRANSACTION = 1768 +ER_SET_STATEMENT_CANNOT_INVOKE_FUNCTION = 1769 +ER_GTID_NEXT_CANT_BE_AUTOMATIC_IF_GTID_NEXT_LIST_IS_NON_NULL = 1770 +OBSOLETE_ER_SKIPPING_LOGGED_TRANSACTION = 1771 +ER_MALFORMED_GTID_SET_SPECIFICATION = 1772 +ER_MALFORMED_GTID_SET_ENCODING = 1773 +ER_MALFORMED_GTID_SPECIFICATION = 1774 +ER_GNO_EXHAUSTED = 1775 +ER_BAD_SLAVE_AUTO_POSITION = 1776 +ER_AUTO_POSITION_REQUIRES_GTID_MODE_NOT_OFF = 1777 +ER_CANT_DO_IMPLICIT_COMMIT_IN_TRX_WHEN_GTID_NEXT_IS_SET = 1778 +ER_GTID_MODE_ON_REQUIRES_ENFORCE_GTID_CONSISTENCY_ON = 1779 +OBSOLETE_ER_GTID_MODE_REQUIRES_BINLOG = 1780 +ER_CANT_SET_GTID_NEXT_TO_GTID_WHEN_GTID_MODE_IS_OFF = 1781 +ER_CANT_SET_GTID_NEXT_TO_ANONYMOUS_WHEN_GTID_MODE_IS_ON = 1782 +ER_CANT_SET_GTID_NEXT_LIST_TO_NON_NULL_WHEN_GTID_MODE_IS_OFF = 1783 +OBSOLETE_ER_FOUND_GTID_EVENT_WHEN_GTID_MODE_IS_OFF__UNUSED = 1784 +ER_GTID_UNSAFE_NON_TRANSACTIONAL_TABLE = 1785 +ER_GTID_UNSAFE_CREATE_SELECT = 1786 +OBSOLETE_ER_GTID_UNSAFE_CREATE_DROP_TEMP_TABLE_IN_TRANSACTION = 1787 +ER_GTID_MODE_CAN_ONLY_CHANGE_ONE_STEP_AT_A_TIME = 1788 +ER_MASTER_HAS_PURGED_REQUIRED_GTIDS = 1789 +ER_CANT_SET_GTID_NEXT_WHEN_OWNING_GTID = 1790 +ER_UNKNOWN_EXPLAIN_FORMAT = 1791 +ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION = 1792 +ER_TOO_LONG_TABLE_PARTITION_COMMENT = 1793 +ER_SLAVE_CONFIGURATION = 1794 +ER_INNODB_FT_LIMIT = 1795 +ER_INNODB_NO_FT_TEMP_TABLE = 1796 +ER_INNODB_FT_WRONG_DOCID_COLUMN = 1797 +ER_INNODB_FT_WRONG_DOCID_INDEX = 1798 +ER_INNODB_ONLINE_LOG_TOO_BIG = 1799 +ER_UNKNOWN_ALTER_ALGORITHM = 1800 +ER_UNKNOWN_ALTER_LOCK = 1801 +ER_MTS_CHANGE_MASTER_CANT_RUN_WITH_GAPS = 1802 +ER_MTS_RECOVERY_FAILURE = 1803 +ER_MTS_RESET_WORKERS = 1804 +ER_COL_COUNT_DOESNT_MATCH_CORRUPTED_V2 = 1805 +ER_SLAVE_SILENT_RETRY_TRANSACTION = 1806 +ER_DISCARD_FK_CHECKS_RUNNING = 1807 +ER_TABLE_SCHEMA_MISMATCH = 1808 +ER_TABLE_IN_SYSTEM_TABLESPACE = 1809 +ER_IO_READ_ERROR = 1810 +ER_IO_WRITE_ERROR = 1811 +ER_TABLESPACE_MISSING = 1812 +ER_TABLESPACE_EXISTS = 1813 +ER_TABLESPACE_DISCARDED = 1814 +ER_INTERNAL_ERROR = 1815 +ER_INNODB_IMPORT_ERROR = 1816 +ER_INNODB_INDEX_CORRUPT = 1817 +ER_INVALID_YEAR_COLUMN_LENGTH = 1818 +ER_NOT_VALID_PASSWORD = 1819 +ER_MUST_CHANGE_PASSWORD = 1820 +ER_FK_NO_INDEX_CHILD = 1821 +ER_FK_NO_INDEX_PARENT = 1822 +ER_FK_FAIL_ADD_SYSTEM = 1823 +ER_FK_CANNOT_OPEN_PARENT = 1824 +ER_FK_INCORRECT_OPTION = 1825 +ER_FK_DUP_NAME = 1826 +ER_PASSWORD_FORMAT = 1827 +ER_FK_COLUMN_CANNOT_DROP = 1828 +ER_FK_COLUMN_CANNOT_DROP_CHILD = 1829 +ER_FK_COLUMN_NOT_NULL = 1830 +ER_DUP_INDEX = 1831 +ER_FK_COLUMN_CANNOT_CHANGE = 1832 +ER_FK_COLUMN_CANNOT_CHANGE_CHILD = 1833 +OBSOLETE_ER_UNUSED5 = 1834 +ER_MALFORMED_PACKET = 1835 +ER_READ_ONLY_MODE = 1836 +ER_GTID_NEXT_TYPE_UNDEFINED_GTID = 1837 +ER_VARIABLE_NOT_SETTABLE_IN_SP = 1838 +OBSOLETE_ER_CANT_SET_GTID_PURGED_WHEN_GTID_MODE_IS_OFF = 1839 +ER_CANT_SET_GTID_PURGED_WHEN_GTID_EXECUTED_IS_NOT_EMPTY = 1840 +ER_CANT_SET_GTID_PURGED_WHEN_OWNED_GTIDS_IS_NOT_EMPTY = 1841 +ER_GTID_PURGED_WAS_CHANGED = 1842 +ER_GTID_EXECUTED_WAS_CHANGED = 1843 +ER_BINLOG_STMT_MODE_AND_NO_REPL_TABLES = 1844 +ER_ALTER_OPERATION_NOT_SUPPORTED = 1845 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON = 1846 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_COPY = 1847 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_PARTITION = 1848 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_FK_RENAME = 1849 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_COLUMN_TYPE = 1850 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_FK_CHECK = 1851 +OBSOLETE_ER_UNUSED6 = 1852 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_NOPK = 1853 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_AUTOINC = 1854 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_HIDDEN_FTS = 1855 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_CHANGE_FTS = 1856 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_FTS = 1857 +OBSOLETE_ER_SQL_REPLICA_SKIP_COUNTER_NOT_SETTABLE_IN_GTID_MODE = 1858 +ER_DUP_UNKNOWN_IN_INDEX = 1859 +ER_IDENT_CAUSES_TOO_LONG_PATH = 1860 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_NOT_NULL = 1861 +ER_MUST_CHANGE_PASSWORD_LOGIN = 1862 +ER_ROW_IN_WRONG_PARTITION = 1863 +ER_MTS_EVENT_BIGGER_PENDING_JOBS_SIZE_MAX = 1864 +OBSOLETE_ER_INNODB_NO_FT_USES_PARSER = 1865 +ER_BINLOG_LOGICAL_CORRUPTION = 1866 +ER_WARN_PURGE_LOG_IN_USE = 1867 +ER_WARN_PURGE_LOG_IS_ACTIVE = 1868 +ER_AUTO_INCREMENT_CONFLICT = 1869 +WARN_ON_BLOCKHOLE_IN_RBR = 1870 +ER_SLAVE_MI_INIT_REPOSITORY = 1871 +ER_SLAVE_RLI_INIT_REPOSITORY = 1872 +ER_ACCESS_DENIED_CHANGE_USER_ERROR = 1873 +ER_INNODB_READ_ONLY = 1874 +ER_STOP_SLAVE_SQL_THREAD_TIMEOUT = 1875 +ER_STOP_SLAVE_IO_THREAD_TIMEOUT = 1876 +ER_TABLE_CORRUPT = 1877 +ER_TEMP_FILE_WRITE_FAILURE = 1878 +ER_INNODB_FT_AUX_NOT_HEX_ID = 1879 +ER_OLD_TEMPORALS_UPGRADED = 1880 +ER_INNODB_FORCED_RECOVERY = 1881 +ER_AES_INVALID_IV = 1882 +ER_PLUGIN_CANNOT_BE_UNINSTALLED = 1883 +ER_GTID_UNSAFE_BINLOG_SPLITTABLE_STATEMENT_AND_ASSIGNED_GTID = 1884 +ER_SLAVE_HAS_MORE_GTIDS_THAN_MASTER = 1885 +ER_MISSING_KEY = 1886 +WARN_NAMED_PIPE_ACCESS_EVERYONE = 1887 +ER_FILE_CORRUPT = 3000 +ER_ERROR_ON_MASTER = 3001 +OBSOLETE_ER_INCONSISTENT_ERROR = 3002 +ER_STORAGE_ENGINE_NOT_LOADED = 3003 +ER_GET_STACKED_DA_WITHOUT_ACTIVE_HANDLER = 3004 +ER_WARN_LEGACY_SYNTAX_CONVERTED = 3005 +ER_BINLOG_UNSAFE_FULLTEXT_PLUGIN = 3006 +ER_CANNOT_DISCARD_TEMPORARY_TABLE = 3007 +ER_FK_DEPTH_EXCEEDED = 3008 +ER_COL_COUNT_DOESNT_MATCH_PLEASE_UPDATE_V2 = 3009 +ER_WARN_TRIGGER_DOESNT_HAVE_CREATED = 3010 +ER_REFERENCED_TRG_DOES_NOT_EXIST = 3011 +ER_EXPLAIN_NOT_SUPPORTED = 3012 +ER_INVALID_FIELD_SIZE = 3013 +ER_MISSING_HA_CREATE_OPTION = 3014 +ER_ENGINE_OUT_OF_MEMORY = 3015 +ER_PASSWORD_EXPIRE_ANONYMOUS_USER = 3016 +ER_SLAVE_SQL_THREAD_MUST_STOP = 3017 +ER_NO_FT_MATERIALIZED_SUBQUERY = 3018 +ER_INNODB_UNDO_LOG_FULL = 3019 +ER_INVALID_ARGUMENT_FOR_LOGARITHM = 3020 +ER_SLAVE_CHANNEL_IO_THREAD_MUST_STOP = 3021 +ER_WARN_OPEN_TEMP_TABLES_MUST_BE_ZERO = 3022 +ER_WARN_ONLY_MASTER_LOG_FILE_NO_POS = 3023 +ER_QUERY_TIMEOUT = 3024 +ER_NON_RO_SELECT_DISABLE_TIMER = 3025 +ER_DUP_LIST_ENTRY = 3026 +OBSOLETE_ER_SQL_MODE_NO_EFFECT = 3027 +ER_AGGREGATE_ORDER_FOR_UNION = 3028 +ER_AGGREGATE_ORDER_NON_AGG_QUERY = 3029 +ER_SLAVE_WORKER_STOPPED_PREVIOUS_THD_ERROR = 3030 +ER_DONT_SUPPORT_REPLICA_PRESERVE_COMMIT_ORDER = 3031 +ER_SERVER_OFFLINE_MODE = 3032 +ER_GIS_DIFFERENT_SRIDS = 3033 +ER_GIS_UNSUPPORTED_ARGUMENT = 3034 +ER_GIS_UNKNOWN_ERROR = 3035 +ER_GIS_UNKNOWN_EXCEPTION = 3036 +ER_GIS_INVALID_DATA = 3037 +ER_BOOST_GEOMETRY_EMPTY_INPUT_EXCEPTION = 3038 +ER_BOOST_GEOMETRY_CENTROID_EXCEPTION = 3039 +ER_BOOST_GEOMETRY_OVERLAY_INVALID_INPUT_EXCEPTION = 3040 +ER_BOOST_GEOMETRY_TURN_INFO_EXCEPTION = 3041 +ER_BOOST_GEOMETRY_SELF_INTERSECTION_POINT_EXCEPTION = 3042 +ER_BOOST_GEOMETRY_UNKNOWN_EXCEPTION = 3043 +ER_STD_BAD_ALLOC_ERROR = 3044 +ER_STD_DOMAIN_ERROR = 3045 +ER_STD_LENGTH_ERROR = 3046 +ER_STD_INVALID_ARGUMENT = 3047 +ER_STD_OUT_OF_RANGE_ERROR = 3048 +ER_STD_OVERFLOW_ERROR = 3049 +ER_STD_RANGE_ERROR = 3050 +ER_STD_UNDERFLOW_ERROR = 3051 +ER_STD_LOGIC_ERROR = 3052 +ER_STD_RUNTIME_ERROR = 3053 +ER_STD_UNKNOWN_EXCEPTION = 3054 +ER_GIS_DATA_WRONG_ENDIANESS = 3055 +ER_CHANGE_MASTER_PASSWORD_LENGTH = 3056 +ER_USER_LOCK_WRONG_NAME = 3057 +ER_USER_LOCK_DEADLOCK = 3058 +ER_REPLACE_INACCESSIBLE_ROWS = 3059 +ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_GIS = 3060 +ER_ILLEGAL_USER_VAR = 3061 +ER_GTID_MODE_OFF = 3062 +OBSOLETE_ER_UNSUPPORTED_BY_REPLICATION_THREAD = 3063 +ER_INCORRECT_TYPE = 3064 +ER_FIELD_IN_ORDER_NOT_SELECT = 3065 +ER_AGGREGATE_IN_ORDER_NOT_SELECT = 3066 +ER_INVALID_RPL_WILD_TABLE_FILTER_PATTERN = 3067 +ER_NET_OK_PACKET_TOO_LARGE = 3068 +ER_INVALID_JSON_DATA = 3069 +ER_INVALID_GEOJSON_MISSING_MEMBER = 3070 +ER_INVALID_GEOJSON_WRONG_TYPE = 3071 +ER_INVALID_GEOJSON_UNSPECIFIED = 3072 +ER_DIMENSION_UNSUPPORTED = 3073 +ER_SLAVE_CHANNEL_DOES_NOT_EXIST = 3074 +OBSOLETE_ER_SLAVE_MULTIPLE_CHANNELS_HOST_PORT = 3075 +ER_SLAVE_CHANNEL_NAME_INVALID_OR_TOO_LONG = 3076 +ER_SLAVE_NEW_CHANNEL_WRONG_REPOSITORY = 3077 +OBSOLETE_ER_SLAVE_CHANNEL_DELETE = 3078 +ER_SLAVE_MULTIPLE_CHANNELS_CMD = 3079 +ER_SLAVE_MAX_CHANNELS_EXCEEDED = 3080 +ER_SLAVE_CHANNEL_MUST_STOP = 3081 +ER_SLAVE_CHANNEL_NOT_RUNNING = 3082 +ER_SLAVE_CHANNEL_WAS_RUNNING = 3083 +ER_SLAVE_CHANNEL_WAS_NOT_RUNNING = 3084 +ER_SLAVE_CHANNEL_SQL_THREAD_MUST_STOP = 3085 +ER_SLAVE_CHANNEL_SQL_SKIP_COUNTER = 3086 +ER_WRONG_FIELD_WITH_GROUP_V2 = 3087 +ER_MIX_OF_GROUP_FUNC_AND_FIELDS_V2 = 3088 +ER_WARN_DEPRECATED_SYSVAR_UPDATE = 3089 +ER_WARN_DEPRECATED_SQLMODE = 3090 +ER_CANNOT_LOG_PARTIAL_DROP_DATABASE_WITH_GTID = 3091 +ER_GROUP_REPLICATION_CONFIGURATION = 3092 +ER_GROUP_REPLICATION_RUNNING = 3093 +ER_GROUP_REPLICATION_APPLIER_INIT_ERROR = 3094 +ER_GROUP_REPLICATION_STOP_APPLIER_THREAD_TIMEOUT = 3095 +ER_GROUP_REPLICATION_COMMUNICATION_LAYER_SESSION_ERROR = 3096 +ER_GROUP_REPLICATION_COMMUNICATION_LAYER_JOIN_ERROR = 3097 +ER_BEFORE_DML_VALIDATION_ERROR = 3098 +ER_PREVENTS_VARIABLE_WITHOUT_RBR = 3099 +ER_RUN_HOOK_ERROR = 3100 +ER_TRANSACTION_ROLLBACK_DURING_COMMIT = 3101 +ER_GENERATED_COLUMN_FUNCTION_IS_NOT_ALLOWED = 3102 +ER_UNSUPPORTED_ALTER_INPLACE_ON_VIRTUAL_COLUMN = 3103 +ER_WRONG_FK_OPTION_FOR_GENERATED_COLUMN = 3104 +ER_NON_DEFAULT_VALUE_FOR_GENERATED_COLUMN = 3105 +ER_UNSUPPORTED_ACTION_ON_GENERATED_COLUMN = 3106 +ER_GENERATED_COLUMN_NON_PRIOR = 3107 +ER_DEPENDENT_BY_GENERATED_COLUMN = 3108 +ER_GENERATED_COLUMN_REF_AUTO_INC = 3109 +ER_FEATURE_NOT_AVAILABLE = 3110 +ER_CANT_SET_GTID_MODE = 3111 +ER_CANT_USE_AUTO_POSITION_WITH_GTID_MODE_OFF = 3112 +OBSOLETE_ER_CANT_REPLICATE_ANONYMOUS_WITH_AUTO_POSITION = 3113 +OBSOLETE_ER_CANT_REPLICATE_ANONYMOUS_WITH_GTID_MODE_ON = 3114 +OBSOLETE_ER_CANT_REPLICATE_GTID_WITH_GTID_MODE_OFF = 3115 +ER_CANT_ENFORCE_GTID_CONSISTENCY_WITH_ONGOING_GTID_VIOLATING_TX = 3116 +ER_ENFORCE_GTID_CONSISTENCY_WARN_WITH_ONGOING_GTID_VIOLATING_TX = 3117 +ER_ACCOUNT_HAS_BEEN_LOCKED = 3118 +ER_WRONG_TABLESPACE_NAME = 3119 +ER_TABLESPACE_IS_NOT_EMPTY = 3120 +ER_WRONG_FILE_NAME = 3121 +ER_BOOST_GEOMETRY_INCONSISTENT_TURNS_EXCEPTION = 3122 +ER_WARN_OPTIMIZER_HINT_SYNTAX_ERROR = 3123 +ER_WARN_BAD_MAX_EXECUTION_TIME = 3124 +ER_WARN_UNSUPPORTED_MAX_EXECUTION_TIME = 3125 +ER_WARN_CONFLICTING_HINT = 3126 +ER_WARN_UNKNOWN_QB_NAME = 3127 +ER_UNRESOLVED_HINT_NAME = 3128 +ER_WARN_ON_MODIFYING_GTID_EXECUTED_TABLE = 3129 +ER_PLUGGABLE_PROTOCOL_COMMAND_NOT_SUPPORTED = 3130 +ER_LOCKING_SERVICE_WRONG_NAME = 3131 +ER_LOCKING_SERVICE_DEADLOCK = 3132 +ER_LOCKING_SERVICE_TIMEOUT = 3133 +ER_GIS_MAX_POINTS_IN_GEOMETRY_OVERFLOWED = 3134 +ER_SQL_MODE_MERGED = 3135 +ER_VTOKEN_PLUGIN_TOKEN_MISMATCH = 3136 +ER_VTOKEN_PLUGIN_TOKEN_NOT_FOUND = 3137 +ER_CANT_SET_VARIABLE_WHEN_OWNING_GTID = 3138 +ER_SLAVE_CHANNEL_OPERATION_NOT_ALLOWED = 3139 +ER_INVALID_JSON_TEXT = 3140 +ER_INVALID_JSON_TEXT_IN_PARAM = 3141 +ER_INVALID_JSON_BINARY_DATA = 3142 +ER_INVALID_JSON_PATH = 3143 +ER_INVALID_JSON_CHARSET = 3144 +ER_INVALID_JSON_CHARSET_IN_FUNCTION = 3145 +ER_INVALID_TYPE_FOR_JSON = 3146 +ER_INVALID_CAST_TO_JSON = 3147 +ER_INVALID_JSON_PATH_CHARSET = 3148 +ER_INVALID_JSON_PATH_WILDCARD = 3149 +ER_JSON_VALUE_TOO_BIG = 3150 +ER_JSON_KEY_TOO_BIG = 3151 +ER_JSON_USED_AS_KEY = 3152 +ER_JSON_VACUOUS_PATH = 3153 +ER_JSON_BAD_ONE_OR_ALL_ARG = 3154 +ER_NUMERIC_JSON_VALUE_OUT_OF_RANGE = 3155 +ER_INVALID_JSON_VALUE_FOR_CAST = 3156 +ER_JSON_DOCUMENT_TOO_DEEP = 3157 +ER_JSON_DOCUMENT_NULL_KEY = 3158 +ER_SECURE_TRANSPORT_REQUIRED = 3159 +ER_NO_SECURE_TRANSPORTS_CONFIGURED = 3160 +ER_DISABLED_STORAGE_ENGINE = 3161 +ER_USER_DOES_NOT_EXIST = 3162 +ER_USER_ALREADY_EXISTS = 3163 +ER_AUDIT_API_ABORT = 3164 +ER_INVALID_JSON_PATH_ARRAY_CELL = 3165 +ER_BUFPOOL_RESIZE_INPROGRESS = 3166 +ER_FEATURE_DISABLED_SEE_DOC = 3167 +ER_SERVER_ISNT_AVAILABLE = 3168 +ER_SESSION_WAS_KILLED = 3169 +ER_CAPACITY_EXCEEDED = 3170 +ER_CAPACITY_EXCEEDED_IN_RANGE_OPTIMIZER = 3171 +OBSOLETE_ER_TABLE_NEEDS_UPG_PART = 3172 +ER_CANT_WAIT_FOR_EXECUTED_GTID_SET_WHILE_OWNING_A_GTID = 3173 +ER_CANNOT_ADD_FOREIGN_BASE_COL_VIRTUAL = 3174 +ER_CANNOT_CREATE_VIRTUAL_INDEX_CONSTRAINT = 3175 +ER_ERROR_ON_MODIFYING_GTID_EXECUTED_TABLE = 3176 +ER_LOCK_REFUSED_BY_ENGINE = 3177 +ER_UNSUPPORTED_ALTER_ONLINE_ON_VIRTUAL_COLUMN = 3178 +ER_MASTER_KEY_ROTATION_NOT_SUPPORTED_BY_SE = 3179 +OBSOLETE_ER_MASTER_KEY_ROTATION_ERROR_BY_SE = 3180 +ER_MASTER_KEY_ROTATION_BINLOG_FAILED = 3181 +ER_MASTER_KEY_ROTATION_SE_UNAVAILABLE = 3182 +ER_TABLESPACE_CANNOT_ENCRYPT = 3183 +ER_INVALID_ENCRYPTION_OPTION = 3184 +ER_CANNOT_FIND_KEY_IN_KEYRING = 3185 +ER_CAPACITY_EXCEEDED_IN_PARSER = 3186 +ER_UNSUPPORTED_ALTER_ENCRYPTION_INPLACE = 3187 +ER_KEYRING_UDF_KEYRING_SERVICE_ERROR = 3188 +ER_USER_COLUMN_OLD_LENGTH = 3189 +ER_CANT_RESET_MASTER = 3190 +ER_GROUP_REPLICATION_MAX_GROUP_SIZE = 3191 +ER_CANNOT_ADD_FOREIGN_BASE_COL_STORED = 3192 +ER_TABLE_REFERENCED = 3193 +OBSOLETE_ER_PARTITION_ENGINE_DEPRECATED_FOR_TABLE = 3194 +OBSOLETE_ER_WARN_USING_GEOMFROMWKB_TO_SET_SRID_ZERO = 3195 +OBSOLETE_ER_WARN_USING_GEOMFROMWKB_TO_SET_SRID = 3196 +ER_XA_RETRY = 3197 +ER_KEYRING_AWS_UDF_AWS_KMS_ERROR = 3198 +ER_BINLOG_UNSAFE_XA = 3199 +ER_UDF_ERROR = 3200 +ER_KEYRING_MIGRATION_FAILURE = 3201 +ER_KEYRING_ACCESS_DENIED_ERROR = 3202 +ER_KEYRING_MIGRATION_STATUS = 3203 +OBSOLETE_ER_PLUGIN_FAILED_TO_OPEN_TABLES = 3204 +OBSOLETE_ER_PLUGIN_FAILED_TO_OPEN_TABLE = 3205 +OBSOLETE_ER_AUDIT_LOG_NO_KEYRING_PLUGIN_INSTALLED = 3206 +OBSOLETE_ER_AUDIT_LOG_ENCRYPTION_PASSWORD_HAS_NOT_BEEN_SET = 3207 +OBSOLETE_ER_AUDIT_LOG_COULD_NOT_CREATE_AES_KEY = 3208 +OBSOLETE_ER_AUDIT_LOG_ENCRYPTION_PASSWORD_CANNOT_BE_FETCHED = 3209 +OBSOLETE_ER_AUDIT_LOG_JSON_FILTERING_NOT_ENABLED = 3210 +OBSOLETE_ER_AUDIT_LOG_UDF_INSUFFICIENT_PRIVILEGE = 3211 +OBSOLETE_ER_AUDIT_LOG_SUPER_PRIVILEGE_REQUIRED = 3212 +OBSOLETE_ER_COULD_NOT_REINITIALIZE_AUDIT_LOG_FILTERS = 3213 +OBSOLETE_ER_AUDIT_LOG_UDF_INVALID_ARGUMENT_TYPE = 3214 +OBSOLETE_ER_AUDIT_LOG_UDF_INVALID_ARGUMENT_COUNT = 3215 +OBSOLETE_ER_AUDIT_LOG_HAS_NOT_BEEN_INSTALLED = 3216 +OBSOLETE_ER_AUDIT_LOG_UDF_READ_INVALID_MAX_ARRAY_LENGTH_ARG_TYPE = 3217 +ER_AUDIT_LOG_UDF_READ_INVALID_MAX_ARRAY_LENGTH_ARG_VALUE = 3218 +OBSOLETE_ER_AUDIT_LOG_JSON_FILTER_PARSING_ERROR = 3219 +OBSOLETE_ER_AUDIT_LOG_JSON_FILTER_NAME_CANNOT_BE_EMPTY = 3220 +OBSOLETE_ER_AUDIT_LOG_JSON_USER_NAME_CANNOT_BE_EMPTY = 3221 +OBSOLETE_ER_AUDIT_LOG_JSON_FILTER_DOES_NOT_EXISTS = 3222 +OBSOLETE_ER_AUDIT_LOG_USER_FIRST_CHARACTER_MUST_BE_ALPHANUMERIC = 3223 +OBSOLETE_ER_AUDIT_LOG_USER_NAME_INVALID_CHARACTER = 3224 +OBSOLETE_ER_AUDIT_LOG_HOST_NAME_INVALID_CHARACTER = 3225 +OBSOLETE_ER_XA_REPLICATION_FILTERS = 3226 +OBSOLETE_ER_CANT_OPEN_ERROR_LOG = 3227 +OBSOLETE_ER_GROUPING_ON_TIMESTAMP_IN_DST = 3228 +OBSOLETE_ER_CANT_START_SERVER_NAMED_PIPE = 3229 +ER_WRITE_SET_EXCEEDS_LIMIT = 3230 +ER_UNSUPPORT_COMPRESSED_TEMPORARY_TABLE = 3500 +ER_ACL_OPERATION_FAILED = 3501 +ER_UNSUPPORTED_INDEX_ALGORITHM = 3502 +ER_NO_SUCH_DB = 3503 +ER_TOO_BIG_ENUM = 3504 +ER_TOO_LONG_SET_ENUM_VALUE = 3505 +ER_INVALID_DD_OBJECT = 3506 +ER_UPDATING_DD_TABLE = 3507 +ER_INVALID_DD_OBJECT_ID = 3508 +ER_INVALID_DD_OBJECT_NAME = 3509 +ER_TABLESPACE_MISSING_WITH_NAME = 3510 +ER_TOO_LONG_ROUTINE_COMMENT = 3511 +ER_SP_LOAD_FAILED = 3512 +ER_INVALID_BITWISE_OPERANDS_SIZE = 3513 +ER_INVALID_BITWISE_AGGREGATE_OPERANDS_SIZE = 3514 +ER_WARN_UNSUPPORTED_HINT = 3515 +ER_UNEXPECTED_GEOMETRY_TYPE = 3516 +ER_SRS_PARSE_ERROR = 3517 +ER_SRS_PROJ_PARAMETER_MISSING = 3518 +ER_WARN_SRS_NOT_FOUND = 3519 +ER_SRS_NOT_CARTESIAN = 3520 +ER_SRS_NOT_CARTESIAN_UNDEFINED = 3521 +ER_PK_INDEX_CANT_BE_INVISIBLE = 3522 +ER_UNKNOWN_AUTHID = 3523 +ER_FAILED_ROLE_GRANT = 3524 +ER_OPEN_ROLE_TABLES = 3525 +ER_FAILED_DEFAULT_ROLES = 3526 +ER_COMPONENTS_NO_SCHEME = 3527 +ER_COMPONENTS_NO_SCHEME_SERVICE = 3528 +ER_COMPONENTS_CANT_LOAD = 3529 +ER_ROLE_NOT_GRANTED = 3530 +ER_FAILED_REVOKE_ROLE = 3531 +ER_RENAME_ROLE = 3532 +ER_COMPONENTS_CANT_ACQUIRE_SERVICE_IMPLEMENTATION = 3533 +ER_COMPONENTS_CANT_SATISFY_DEPENDENCY = 3534 +ER_COMPONENTS_LOAD_CANT_REGISTER_SERVICE_IMPLEMENTATION = 3535 +ER_COMPONENTS_LOAD_CANT_INITIALIZE = 3536 +ER_COMPONENTS_UNLOAD_NOT_LOADED = 3537 +ER_COMPONENTS_UNLOAD_CANT_DEINITIALIZE = 3538 +ER_COMPONENTS_CANT_RELEASE_SERVICE = 3539 +ER_COMPONENTS_UNLOAD_CANT_UNREGISTER_SERVICE = 3540 +ER_COMPONENTS_CANT_UNLOAD = 3541 +ER_WARN_UNLOAD_THE_NOT_PERSISTED = 3542 +ER_COMPONENT_TABLE_INCORRECT = 3543 +ER_COMPONENT_MANIPULATE_ROW_FAILED = 3544 +ER_COMPONENTS_UNLOAD_DUPLICATE_IN_GROUP = 3545 +ER_CANT_SET_GTID_PURGED_DUE_SETS_CONSTRAINTS = 3546 +ER_CANNOT_LOCK_USER_MANAGEMENT_CACHES = 3547 +ER_SRS_NOT_FOUND = 3548 +ER_VARIABLE_NOT_PERSISTED = 3549 +ER_IS_QUERY_INVALID_CLAUSE = 3550 +ER_UNABLE_TO_STORE_STATISTICS = 3551 +ER_NO_SYSTEM_SCHEMA_ACCESS = 3552 +ER_NO_SYSTEM_TABLESPACE_ACCESS = 3553 +ER_NO_SYSTEM_TABLE_ACCESS = 3554 +ER_NO_SYSTEM_TABLE_ACCESS_FOR_DICTIONARY_TABLE = 3555 +ER_NO_SYSTEM_TABLE_ACCESS_FOR_SYSTEM_TABLE = 3556 +ER_NO_SYSTEM_TABLE_ACCESS_FOR_TABLE = 3557 +ER_INVALID_OPTION_KEY = 3558 +ER_INVALID_OPTION_VALUE = 3559 +ER_INVALID_OPTION_KEY_VALUE_PAIR = 3560 +ER_INVALID_OPTION_START_CHARACTER = 3561 +ER_INVALID_OPTION_END_CHARACTER = 3562 +ER_INVALID_OPTION_CHARACTERS = 3563 +ER_DUPLICATE_OPTION_KEY = 3564 +ER_WARN_SRS_NOT_FOUND_AXIS_ORDER = 3565 +ER_NO_ACCESS_TO_NATIVE_FCT = 3566 +ER_RESET_MASTER_TO_VALUE_OUT_OF_RANGE = 3567 +ER_UNRESOLVED_TABLE_LOCK = 3568 +ER_DUPLICATE_TABLE_LOCK = 3569 +ER_BINLOG_UNSAFE_SKIP_LOCKED = 3570 +ER_BINLOG_UNSAFE_NOWAIT = 3571 +ER_LOCK_NOWAIT = 3572 +ER_CTE_RECURSIVE_REQUIRES_UNION = 3573 +ER_CTE_RECURSIVE_REQUIRES_NONRECURSIVE_FIRST = 3574 +ER_CTE_RECURSIVE_FORBIDS_AGGREGATION = 3575 +ER_CTE_RECURSIVE_FORBIDDEN_JOIN_ORDER = 3576 +ER_CTE_RECURSIVE_REQUIRES_SINGLE_REFERENCE = 3577 +ER_SWITCH_TMP_ENGINE = 3578 +ER_WINDOW_NO_SUCH_WINDOW = 3579 +ER_WINDOW_CIRCULARITY_IN_WINDOW_GRAPH = 3580 +ER_WINDOW_NO_CHILD_PARTITIONING = 3581 +ER_WINDOW_NO_INHERIT_FRAME = 3582 +ER_WINDOW_NO_REDEFINE_ORDER_BY = 3583 +ER_WINDOW_FRAME_START_ILLEGAL = 3584 +ER_WINDOW_FRAME_END_ILLEGAL = 3585 +ER_WINDOW_FRAME_ILLEGAL = 3586 +ER_WINDOW_RANGE_FRAME_ORDER_TYPE = 3587 +ER_WINDOW_RANGE_FRAME_TEMPORAL_TYPE = 3588 +ER_WINDOW_RANGE_FRAME_NUMERIC_TYPE = 3589 +ER_WINDOW_RANGE_BOUND_NOT_CONSTANT = 3590 +ER_WINDOW_DUPLICATE_NAME = 3591 +ER_WINDOW_ILLEGAL_ORDER_BY = 3592 +ER_WINDOW_INVALID_WINDOW_FUNC_USE = 3593 +ER_WINDOW_INVALID_WINDOW_FUNC_ALIAS_USE = 3594 +ER_WINDOW_NESTED_WINDOW_FUNC_USE_IN_WINDOW_SPEC = 3595 +ER_WINDOW_ROWS_INTERVAL_USE = 3596 +ER_WINDOW_NO_GROUP_ORDER_UNUSED = 3597 +ER_WINDOW_EXPLAIN_JSON = 3598 +ER_WINDOW_FUNCTION_IGNORES_FRAME = 3599 +ER_WL9236_NOW_UNUSED = 3600 +ER_INVALID_NO_OF_ARGS = 3601 +ER_FIELD_IN_GROUPING_NOT_GROUP_BY = 3602 +ER_TOO_LONG_TABLESPACE_COMMENT = 3603 +ER_ENGINE_CANT_DROP_TABLE = 3604 +ER_ENGINE_CANT_DROP_MISSING_TABLE = 3605 +ER_TABLESPACE_DUP_FILENAME = 3606 +ER_DB_DROP_RMDIR2 = 3607 +ER_IMP_NO_FILES_MATCHED = 3608 +ER_IMP_SCHEMA_DOES_NOT_EXIST = 3609 +ER_IMP_TABLE_ALREADY_EXISTS = 3610 +ER_IMP_INCOMPATIBLE_MYSQLD_VERSION = 3611 +ER_IMP_INCOMPATIBLE_DD_VERSION = 3612 +ER_IMP_INCOMPATIBLE_SDI_VERSION = 3613 +ER_WARN_INVALID_HINT = 3614 +ER_VAR_DOES_NOT_EXIST = 3615 +ER_LONGITUDE_OUT_OF_RANGE = 3616 +ER_LATITUDE_OUT_OF_RANGE = 3617 +ER_NOT_IMPLEMENTED_FOR_GEOGRAPHIC_SRS = 3618 +ER_ILLEGAL_PRIVILEGE_LEVEL = 3619 +ER_NO_SYSTEM_VIEW_ACCESS = 3620 +ER_COMPONENT_FILTER_FLABBERGASTED = 3621 +ER_PART_EXPR_TOO_LONG = 3622 +ER_UDF_DROP_DYNAMICALLY_REGISTERED = 3623 +ER_UNABLE_TO_STORE_COLUMN_STATISTICS = 3624 +ER_UNABLE_TO_UPDATE_COLUMN_STATISTICS = 3625 +ER_UNABLE_TO_DROP_COLUMN_STATISTICS = 3626 +ER_UNABLE_TO_BUILD_HISTOGRAM = 3627 +ER_MANDATORY_ROLE = 3628 +ER_MISSING_TABLESPACE_FILE = 3629 +ER_PERSIST_ONLY_ACCESS_DENIED_ERROR = 3630 +ER_CMD_NEED_SUPER = 3631 +ER_PATH_IN_DATADIR = 3632 +ER_CLONE_DDL_IN_PROGRESS = 3633 +ER_CLONE_TOO_MANY_CONCURRENT_CLONES = 3634 +ER_APPLIER_LOG_EVENT_VALIDATION_ERROR = 3635 +ER_CTE_MAX_RECURSION_DEPTH = 3636 +ER_NOT_HINT_UPDATABLE_VARIABLE = 3637 +ER_CREDENTIALS_CONTRADICT_TO_HISTORY = 3638 +ER_WARNING_PASSWORD_HISTORY_CLAUSES_VOID = 3639 +ER_CLIENT_DOES_NOT_SUPPORT = 3640 +ER_I_S_SKIPPED_TABLESPACE = 3641 +ER_TABLESPACE_ENGINE_MISMATCH = 3642 +ER_WRONG_SRID_FOR_COLUMN = 3643 +ER_CANNOT_ALTER_SRID_DUE_TO_INDEX = 3644 +ER_WARN_BINLOG_PARTIAL_UPDATES_DISABLED = 3645 +ER_WARN_BINLOG_V1_ROW_EVENTS_DISABLED = 3646 +ER_WARN_BINLOG_PARTIAL_UPDATES_SUGGESTS_PARTIAL_IMAGES = 3647 +ER_COULD_NOT_APPLY_JSON_DIFF = 3648 +ER_CORRUPTED_JSON_DIFF = 3649 +ER_RESOURCE_GROUP_EXISTS = 3650 +ER_RESOURCE_GROUP_NOT_EXISTS = 3651 +ER_INVALID_VCPU_ID = 3652 +ER_INVALID_VCPU_RANGE = 3653 +ER_INVALID_THREAD_PRIORITY = 3654 +ER_DISALLOWED_OPERATION = 3655 +ER_RESOURCE_GROUP_BUSY = 3656 +ER_RESOURCE_GROUP_DISABLED = 3657 +ER_FEATURE_UNSUPPORTED = 3658 +ER_ATTRIBUTE_IGNORED = 3659 +ER_INVALID_THREAD_ID = 3660 +ER_RESOURCE_GROUP_BIND_FAILED = 3661 +ER_INVALID_USE_OF_FORCE_OPTION = 3662 +ER_GROUP_REPLICATION_COMMAND_FAILURE = 3663 +ER_SDI_OPERATION_FAILED = 3664 +ER_MISSING_JSON_TABLE_VALUE = 3665 +ER_WRONG_JSON_TABLE_VALUE = 3666 +ER_TF_MUST_HAVE_ALIAS = 3667 +ER_TF_FORBIDDEN_JOIN_TYPE = 3668 +ER_JT_VALUE_OUT_OF_RANGE = 3669 +ER_JT_MAX_NESTED_PATH = 3670 +ER_PASSWORD_EXPIRATION_NOT_SUPPORTED_BY_AUTH_METHOD = 3671 +ER_INVALID_GEOJSON_CRS_NOT_TOP_LEVEL = 3672 +ER_BAD_NULL_ERROR_NOT_IGNORED = 3673 +WARN_USELESS_SPATIAL_INDEX = 3674 +ER_DISK_FULL_NOWAIT = 3675 +ER_PARSE_ERROR_IN_DIGEST_FN = 3676 +ER_UNDISCLOSED_PARSE_ERROR_IN_DIGEST_FN = 3677 +ER_SCHEMA_DIR_EXISTS = 3678 +ER_SCHEMA_DIR_MISSING = 3679 +ER_SCHEMA_DIR_CREATE_FAILED = 3680 +ER_SCHEMA_DIR_UNKNOWN = 3681 +ER_ONLY_IMPLEMENTED_FOR_SRID_0_AND_4326 = 3682 +ER_BINLOG_EXPIRE_LOG_DAYS_AND_SECS_USED_TOGETHER = 3683 +ER_REGEXP_BUFFER_OVERFLOW = 3684 +ER_REGEXP_ILLEGAL_ARGUMENT = 3685 +ER_REGEXP_INDEX_OUTOFBOUNDS_ERROR = 3686 +ER_REGEXP_INTERNAL_ERROR = 3687 +ER_REGEXP_RULE_SYNTAX = 3688 +ER_REGEXP_BAD_ESCAPE_SEQUENCE = 3689 +ER_REGEXP_UNIMPLEMENTED = 3690 +ER_REGEXP_MISMATCHED_PAREN = 3691 +ER_REGEXP_BAD_INTERVAL = 3692 +ER_REGEXP_MAX_LT_MIN = 3693 +ER_REGEXP_INVALID_BACK_REF = 3694 +ER_REGEXP_LOOK_BEHIND_LIMIT = 3695 +ER_REGEXP_MISSING_CLOSE_BRACKET = 3696 +ER_REGEXP_INVALID_RANGE = 3697 +ER_REGEXP_STACK_OVERFLOW = 3698 +ER_REGEXP_TIME_OUT = 3699 +ER_REGEXP_PATTERN_TOO_BIG = 3700 +ER_CANT_SET_ERROR_LOG_SERVICE = 3701 +ER_EMPTY_PIPELINE_FOR_ERROR_LOG_SERVICE = 3702 +ER_COMPONENT_FILTER_DIAGNOSTICS = 3703 +ER_NOT_IMPLEMENTED_FOR_CARTESIAN_SRS = 3704 +ER_NOT_IMPLEMENTED_FOR_PROJECTED_SRS = 3705 +ER_NONPOSITIVE_RADIUS = 3706 +ER_RESTART_SERVER_FAILED = 3707 +ER_SRS_MISSING_MANDATORY_ATTRIBUTE = 3708 +ER_SRS_MULTIPLE_ATTRIBUTE_DEFINITIONS = 3709 +ER_SRS_NAME_CANT_BE_EMPTY_OR_WHITESPACE = 3710 +ER_SRS_ORGANIZATION_CANT_BE_EMPTY_OR_WHITESPACE = 3711 +ER_SRS_ID_ALREADY_EXISTS = 3712 +ER_WARN_SRS_ID_ALREADY_EXISTS = 3713 +ER_CANT_MODIFY_SRID_0 = 3714 +ER_WARN_RESERVED_SRID_RANGE = 3715 +ER_CANT_MODIFY_SRS_USED_BY_COLUMN = 3716 +ER_SRS_INVALID_CHARACTER_IN_ATTRIBUTE = 3717 +ER_SRS_ATTRIBUTE_STRING_TOO_LONG = 3718 +ER_DEPRECATED_UTF8_ALIAS = 3719 +ER_DEPRECATED_NATIONAL = 3720 +ER_INVALID_DEFAULT_UTF8MB4_COLLATION = 3721 +ER_UNABLE_TO_COLLECT_LOG_STATUS = 3722 +ER_RESERVED_TABLESPACE_NAME = 3723 +ER_UNABLE_TO_SET_OPTION = 3724 +ER_SLAVE_POSSIBLY_DIVERGED_AFTER_DDL = 3725 +ER_SRS_NOT_GEOGRAPHIC = 3726 +ER_POLYGON_TOO_LARGE = 3727 +ER_SPATIAL_UNIQUE_INDEX = 3728 +ER_INDEX_TYPE_NOT_SUPPORTED_FOR_SPATIAL_INDEX = 3729 +ER_FK_CANNOT_DROP_PARENT = 3730 +ER_GEOMETRY_PARAM_LONGITUDE_OUT_OF_RANGE = 3731 +ER_GEOMETRY_PARAM_LATITUDE_OUT_OF_RANGE = 3732 +ER_FK_CANNOT_USE_VIRTUAL_COLUMN = 3733 +ER_FK_NO_COLUMN_PARENT = 3734 +ER_CANT_SET_ERROR_SUPPRESSION_LIST = 3735 +ER_SRS_GEOGCS_INVALID_AXES = 3736 +ER_SRS_INVALID_SEMI_MAJOR_AXIS = 3737 +ER_SRS_INVALID_INVERSE_FLATTENING = 3738 +ER_SRS_INVALID_ANGULAR_UNIT = 3739 +ER_SRS_INVALID_PRIME_MERIDIAN = 3740 +ER_TRANSFORM_SOURCE_SRS_NOT_SUPPORTED = 3741 +ER_TRANSFORM_TARGET_SRS_NOT_SUPPORTED = 3742 +ER_TRANSFORM_SOURCE_SRS_MISSING_TOWGS84 = 3743 +ER_TRANSFORM_TARGET_SRS_MISSING_TOWGS84 = 3744 +ER_TEMP_TABLE_PREVENTS_SWITCH_SESSION_BINLOG_FORMAT = 3745 +ER_TEMP_TABLE_PREVENTS_SWITCH_GLOBAL_BINLOG_FORMAT = 3746 +ER_RUNNING_APPLIER_PREVENTS_SWITCH_GLOBAL_BINLOG_FORMAT = 3747 +ER_CLIENT_GTID_UNSAFE_CREATE_DROP_TEMP_TABLE_IN_TRX_IN_SBR = 3748 +OBSOLETE_ER_XA_CANT_CREATE_MDL_BACKUP = 3749 +ER_TABLE_WITHOUT_PK = 3750 +ER_WARN_DATA_TRUNCATED_FUNCTIONAL_INDEX = 3751 +ER_WARN_DATA_OUT_OF_RANGE_FUNCTIONAL_INDEX = 3752 +ER_FUNCTIONAL_INDEX_ON_JSON_OR_GEOMETRY_FUNCTION = 3753 +ER_FUNCTIONAL_INDEX_REF_AUTO_INCREMENT = 3754 +ER_CANNOT_DROP_COLUMN_FUNCTIONAL_INDEX = 3755 +ER_FUNCTIONAL_INDEX_PRIMARY_KEY = 3756 +ER_FUNCTIONAL_INDEX_ON_LOB = 3757 +ER_FUNCTIONAL_INDEX_FUNCTION_IS_NOT_ALLOWED = 3758 +ER_FULLTEXT_FUNCTIONAL_INDEX = 3759 +ER_SPATIAL_FUNCTIONAL_INDEX = 3760 +ER_WRONG_KEY_COLUMN_FUNCTIONAL_INDEX = 3761 +ER_FUNCTIONAL_INDEX_ON_FIELD = 3762 +ER_GENERATED_COLUMN_NAMED_FUNCTION_IS_NOT_ALLOWED = 3763 +ER_GENERATED_COLUMN_ROW_VALUE = 3764 +ER_GENERATED_COLUMN_VARIABLES = 3765 +ER_DEPENDENT_BY_DEFAULT_GENERATED_VALUE = 3766 +ER_DEFAULT_VAL_GENERATED_NON_PRIOR = 3767 +ER_DEFAULT_VAL_GENERATED_REF_AUTO_INC = 3768 +ER_DEFAULT_VAL_GENERATED_FUNCTION_IS_NOT_ALLOWED = 3769 +ER_DEFAULT_VAL_GENERATED_NAMED_FUNCTION_IS_NOT_ALLOWED = 3770 +ER_DEFAULT_VAL_GENERATED_ROW_VALUE = 3771 +ER_DEFAULT_VAL_GENERATED_VARIABLES = 3772 +ER_DEFAULT_AS_VAL_GENERATED = 3773 +ER_UNSUPPORTED_ACTION_ON_DEFAULT_VAL_GENERATED = 3774 +ER_GTID_UNSAFE_ALTER_ADD_COL_WITH_DEFAULT_EXPRESSION = 3775 +ER_FK_CANNOT_CHANGE_ENGINE = 3776 +ER_WARN_DEPRECATED_USER_SET_EXPR = 3777 +ER_WARN_DEPRECATED_UTF8MB3_COLLATION = 3778 +ER_WARN_DEPRECATED_NESTED_COMMENT_SYNTAX = 3779 +ER_FK_INCOMPATIBLE_COLUMNS = 3780 +ER_GR_HOLD_WAIT_TIMEOUT = 3781 +ER_GR_HOLD_KILLED = 3782 +ER_GR_HOLD_MEMBER_STATUS_ERROR = 3783 +ER_RPL_ENCRYPTION_FAILED_TO_FETCH_KEY = 3784 +ER_RPL_ENCRYPTION_KEY_NOT_FOUND = 3785 +ER_RPL_ENCRYPTION_KEYRING_INVALID_KEY = 3786 +ER_RPL_ENCRYPTION_HEADER_ERROR = 3787 +ER_RPL_ENCRYPTION_FAILED_TO_ROTATE_LOGS = 3788 +ER_RPL_ENCRYPTION_KEY_EXISTS_UNEXPECTED = 3789 +ER_RPL_ENCRYPTION_FAILED_TO_GENERATE_KEY = 3790 +ER_RPL_ENCRYPTION_FAILED_TO_STORE_KEY = 3791 +ER_RPL_ENCRYPTION_FAILED_TO_REMOVE_KEY = 3792 +ER_RPL_ENCRYPTION_UNABLE_TO_CHANGE_OPTION = 3793 +ER_RPL_ENCRYPTION_MASTER_KEY_RECOVERY_FAILED = 3794 +ER_SLOW_LOG_MODE_IGNORED_WHEN_NOT_LOGGING_TO_FILE = 3795 +ER_GRP_TRX_CONSISTENCY_NOT_ALLOWED = 3796 +ER_GRP_TRX_CONSISTENCY_BEFORE = 3797 +ER_GRP_TRX_CONSISTENCY_AFTER_ON_TRX_BEGIN = 3798 +ER_GRP_TRX_CONSISTENCY_BEGIN_NOT_ALLOWED = 3799 +ER_FUNCTIONAL_INDEX_ROW_VALUE_IS_NOT_ALLOWED = 3800 +ER_RPL_ENCRYPTION_FAILED_TO_ENCRYPT = 3801 +ER_PAGE_TRACKING_NOT_STARTED = 3802 +ER_PAGE_TRACKING_RANGE_NOT_TRACKED = 3803 +ER_PAGE_TRACKING_CANNOT_PURGE = 3804 +ER_RPL_ENCRYPTION_CANNOT_ROTATE_BINLOG_MASTER_KEY = 3805 +ER_BINLOG_MASTER_KEY_RECOVERY_OUT_OF_COMBINATION = 3806 +ER_BINLOG_MASTER_KEY_ROTATION_FAIL_TO_OPERATE_KEY = 3807 +ER_BINLOG_MASTER_KEY_ROTATION_FAIL_TO_ROTATE_LOGS = 3808 +ER_BINLOG_MASTER_KEY_ROTATION_FAIL_TO_REENCRYPT_LOG = 3809 +ER_BINLOG_MASTER_KEY_ROTATION_FAIL_TO_CLEANUP_UNUSED_KEYS = 3810 +ER_BINLOG_MASTER_KEY_ROTATION_FAIL_TO_CLEANUP_AUX_KEY = 3811 +ER_NON_BOOLEAN_EXPR_FOR_CHECK_CONSTRAINT = 3812 +ER_COLUMN_CHECK_CONSTRAINT_REFERENCES_OTHER_COLUMN = 3813 +ER_CHECK_CONSTRAINT_NAMED_FUNCTION_IS_NOT_ALLOWED = 3814 +ER_CHECK_CONSTRAINT_FUNCTION_IS_NOT_ALLOWED = 3815 +ER_CHECK_CONSTRAINT_VARIABLES = 3816 +ER_CHECK_CONSTRAINT_ROW_VALUE = 3817 +ER_CHECK_CONSTRAINT_REFERS_AUTO_INCREMENT_COLUMN = 3818 +ER_CHECK_CONSTRAINT_VIOLATED = 3819 +ER_CHECK_CONSTRAINT_REFERS_UNKNOWN_COLUMN = 3820 +ER_CHECK_CONSTRAINT_NOT_FOUND = 3821 +ER_CHECK_CONSTRAINT_DUP_NAME = 3822 +ER_CHECK_CONSTRAINT_CLAUSE_USING_FK_REFER_ACTION_COLUMN = 3823 +WARN_UNENCRYPTED_TABLE_IN_ENCRYPTED_DB = 3824 +ER_INVALID_ENCRYPTION_REQUEST = 3825 +ER_CANNOT_SET_TABLE_ENCRYPTION = 3826 +ER_CANNOT_SET_DATABASE_ENCRYPTION = 3827 +ER_CANNOT_SET_TABLESPACE_ENCRYPTION = 3828 +ER_TABLESPACE_CANNOT_BE_ENCRYPTED = 3829 +ER_TABLESPACE_CANNOT_BE_DECRYPTED = 3830 +ER_TABLESPACE_TYPE_UNKNOWN = 3831 +ER_TARGET_TABLESPACE_UNENCRYPTED = 3832 +ER_CANNOT_USE_ENCRYPTION_CLAUSE = 3833 +ER_INVALID_MULTIPLE_CLAUSES = 3834 +ER_UNSUPPORTED_USE_OF_GRANT_AS = 3835 +ER_UKNOWN_AUTH_ID_OR_ACCESS_DENIED_FOR_GRANT_AS = 3836 +ER_DEPENDENT_BY_FUNCTIONAL_INDEX = 3837 +ER_PLUGIN_NOT_EARLY = 3838 +ER_INNODB_REDO_LOG_ARCHIVE_START_SUBDIR_PATH = 3839 +ER_INNODB_REDO_LOG_ARCHIVE_START_TIMEOUT = 3840 +ER_INNODB_REDO_LOG_ARCHIVE_DIRS_INVALID = 3841 +ER_INNODB_REDO_LOG_ARCHIVE_LABEL_NOT_FOUND = 3842 +ER_INNODB_REDO_LOG_ARCHIVE_DIR_EMPTY = 3843 +ER_INNODB_REDO_LOG_ARCHIVE_NO_SUCH_DIR = 3844 +ER_INNODB_REDO_LOG_ARCHIVE_DIR_CLASH = 3845 +ER_INNODB_REDO_LOG_ARCHIVE_DIR_PERMISSIONS = 3846 +ER_INNODB_REDO_LOG_ARCHIVE_FILE_CREATE = 3847 +ER_INNODB_REDO_LOG_ARCHIVE_ACTIVE = 3848 +ER_INNODB_REDO_LOG_ARCHIVE_INACTIVE = 3849 +ER_INNODB_REDO_LOG_ARCHIVE_FAILED = 3850 +ER_INNODB_REDO_LOG_ARCHIVE_SESSION = 3851 +ER_STD_REGEX_ERROR = 3852 +ER_INVALID_JSON_TYPE = 3853 +ER_CANNOT_CONVERT_STRING = 3854 +ER_DEPENDENT_BY_PARTITION_FUNC = 3855 +ER_WARN_DEPRECATED_FLOAT_AUTO_INCREMENT = 3856 +ER_RPL_CANT_STOP_SLAVE_WHILE_LOCKED_BACKUP = 3857 +ER_WARN_DEPRECATED_FLOAT_DIGITS = 3858 +ER_WARN_DEPRECATED_FLOAT_UNSIGNED = 3859 +ER_WARN_DEPRECATED_INTEGER_DISPLAY_WIDTH = 3860 +ER_WARN_DEPRECATED_ZEROFILL = 3861 +ER_CLONE_DONOR = 3862 +ER_CLONE_PROTOCOL = 3863 +ER_CLONE_DONOR_VERSION = 3864 +ER_CLONE_OS = 3865 +ER_CLONE_PLATFORM = 3866 +ER_CLONE_CHARSET = 3867 +ER_CLONE_CONFIG = 3868 +ER_CLONE_SYS_CONFIG = 3869 +ER_CLONE_PLUGIN_MATCH = 3870 +ER_CLONE_LOOPBACK = 3871 +ER_CLONE_ENCRYPTION = 3872 +ER_CLONE_DISK_SPACE = 3873 +ER_CLONE_IN_PROGRESS = 3874 +ER_CLONE_DISALLOWED = 3875 +ER_CANNOT_GRANT_ROLES_TO_ANONYMOUS_USER = 3876 +ER_SECONDARY_ENGINE_PLUGIN = 3877 +ER_SECOND_PASSWORD_CANNOT_BE_EMPTY = 3878 +ER_DB_ACCESS_DENIED = 3879 +ER_DA_AUTH_ID_WITH_SYSTEM_USER_PRIV_IN_MANDATORY_ROLES = 3880 +ER_DA_RPL_GTID_TABLE_CANNOT_OPEN = 3881 +ER_GEOMETRY_IN_UNKNOWN_LENGTH_UNIT = 3882 +ER_DA_PLUGIN_INSTALL_ERROR = 3883 +ER_NO_SESSION_TEMP = 3884 +ER_DA_UNKNOWN_ERROR_NUMBER = 3885 +ER_COLUMN_CHANGE_SIZE = 3886 +ER_REGEXP_INVALID_CAPTURE_GROUP_NAME = 3887 +ER_DA_SSL_LIBRARY_ERROR = 3888 +ER_SECONDARY_ENGINE = 3889 +ER_SECONDARY_ENGINE_DDL = 3890 +ER_INCORRECT_CURRENT_PASSWORD = 3891 +ER_MISSING_CURRENT_PASSWORD = 3892 +ER_CURRENT_PASSWORD_NOT_REQUIRED = 3893 +ER_PASSWORD_CANNOT_BE_RETAINED_ON_PLUGIN_CHANGE = 3894 +ER_CURRENT_PASSWORD_CANNOT_BE_RETAINED = 3895 +ER_PARTIAL_REVOKES_EXIST = 3896 +ER_CANNOT_GRANT_SYSTEM_PRIV_TO_MANDATORY_ROLE = 3897 +ER_XA_REPLICATION_FILTERS = 3898 +ER_UNSUPPORTED_SQL_MODE = 3899 +ER_REGEXP_INVALID_FLAG = 3900 +ER_PARTIAL_REVOKE_AND_DB_GRANT_BOTH_EXISTS = 3901 +ER_UNIT_NOT_FOUND = 3902 +ER_INVALID_JSON_VALUE_FOR_FUNC_INDEX = 3903 +ER_JSON_VALUE_OUT_OF_RANGE_FOR_FUNC_INDEX = 3904 +ER_EXCEEDED_MV_KEYS_NUM = 3905 +ER_EXCEEDED_MV_KEYS_SPACE = 3906 +ER_FUNCTIONAL_INDEX_DATA_IS_TOO_LONG = 3907 +ER_WRONG_MVI_VALUE = 3908 +ER_WARN_FUNC_INDEX_NOT_APPLICABLE = 3909 +ER_GRP_RPL_UDF_ERROR = 3910 +ER_UPDATE_GTID_PURGED_WITH_GR = 3911 +ER_GROUPING_ON_TIMESTAMP_IN_DST = 3912 +ER_TABLE_NAME_CAUSES_TOO_LONG_PATH = 3913 +ER_AUDIT_LOG_INSUFFICIENT_PRIVILEGE = 3914 +OBSOLETE_ER_AUDIT_LOG_PASSWORD_HAS_BEEN_COPIED = 3915 +ER_DA_GRP_RPL_STARTED_AUTO_REJOIN = 3916 +ER_SYSVAR_CHANGE_DURING_QUERY = 3917 +ER_GLOBSTAT_CHANGE_DURING_QUERY = 3918 +ER_GRP_RPL_MESSAGE_SERVICE_INIT_FAILURE = 3919 +ER_CHANGE_MASTER_WRONG_COMPRESSION_ALGORITHM_CLIENT = 3920 +ER_CHANGE_MASTER_WRONG_COMPRESSION_LEVEL_CLIENT = 3921 +ER_WRONG_COMPRESSION_ALGORITHM_CLIENT = 3922 +ER_WRONG_COMPRESSION_LEVEL_CLIENT = 3923 +ER_CHANGE_MASTER_WRONG_COMPRESSION_ALGORITHM_LIST_CLIENT = 3924 +ER_CLIENT_PRIVILEGE_CHECKS_USER_CANNOT_BE_ANONYMOUS = 3925 +ER_CLIENT_PRIVILEGE_CHECKS_USER_DOES_NOT_EXIST = 3926 +ER_CLIENT_PRIVILEGE_CHECKS_USER_CORRUPT = 3927 +ER_CLIENT_PRIVILEGE_CHECKS_USER_NEEDS_RPL_APPLIER_PRIV = 3928 +ER_WARN_DA_PRIVILEGE_NOT_REGISTERED = 3929 +ER_CLIENT_KEYRING_UDF_KEY_INVALID = 3930 +ER_CLIENT_KEYRING_UDF_KEY_TYPE_INVALID = 3931 +ER_CLIENT_KEYRING_UDF_KEY_TOO_LONG = 3932 +ER_CLIENT_KEYRING_UDF_KEY_TYPE_TOO_LONG = 3933 +ER_JSON_SCHEMA_VALIDATION_ERROR_WITH_DETAILED_REPORT = 3934 +ER_DA_UDF_INVALID_CHARSET_SPECIFIED = 3935 +ER_DA_UDF_INVALID_CHARSET = 3936 +ER_DA_UDF_INVALID_COLLATION = 3937 +ER_DA_UDF_INVALID_EXTENSION_ARGUMENT_TYPE = 3938 +ER_MULTIPLE_CONSTRAINTS_WITH_SAME_NAME = 3939 +ER_CONSTRAINT_NOT_FOUND = 3940 +ER_ALTER_CONSTRAINT_ENFORCEMENT_NOT_SUPPORTED = 3941 +ER_TABLE_VALUE_CONSTRUCTOR_MUST_HAVE_COLUMNS = 3942 +ER_TABLE_VALUE_CONSTRUCTOR_CANNOT_HAVE_DEFAULT = 3943 +ER_CLIENT_QUERY_FAILURE_INVALID_NON_ROW_FORMAT = 3944 +ER_REQUIRE_ROW_FORMAT_INVALID_VALUE = 3945 +ER_FAILED_TO_DETERMINE_IF_ROLE_IS_MANDATORY = 3946 +ER_FAILED_TO_FETCH_MANDATORY_ROLE_LIST = 3947 +ER_CLIENT_LOCAL_FILES_DISABLED = 3948 +ER_IMP_INCOMPATIBLE_CFG_VERSION = 3949 +ER_DA_OOM = 3950 +ER_DA_UDF_INVALID_ARGUMENT_TO_SET_CHARSET = 3951 +ER_DA_UDF_INVALID_RETURN_TYPE_TO_SET_CHARSET = 3952 +ER_MULTIPLE_INTO_CLAUSES = 3953 +ER_MISPLACED_INTO = 3954 +ER_USER_ACCESS_DENIED_FOR_USER_ACCOUNT_BLOCKED_BY_PASSWORD_LOCK = 3955 +ER_WARN_DEPRECATED_YEAR_UNSIGNED = 3956 +ER_CLONE_NETWORK_PACKET = 3957 +ER_SDI_OPERATION_FAILED_MISSING_RECORD = 3958 +ER_DEPENDENT_BY_CHECK_CONSTRAINT = 3959 +ER_GRP_OPERATION_NOT_ALLOWED_GR_MUST_STOP = 3960 +ER_WARN_DEPRECATED_JSON_TABLE_ON_ERROR_ON_EMPTY = 3961 +ER_WARN_DEPRECATED_INNER_INTO = 3962 +ER_WARN_DEPRECATED_VALUES_FUNCTION_ALWAYS_NULL = 3963 +ER_WARN_DEPRECATED_SQL_CALC_FOUND_ROWS = 3964 +ER_WARN_DEPRECATED_FOUND_ROWS = 3965 +ER_MISSING_JSON_VALUE = 3966 +ER_MULTIPLE_JSON_VALUES = 3967 +ER_HOSTNAME_TOO_LONG = 3968 +ER_WARN_CLIENT_DEPRECATED_PARTITION_PREFIX_KEY = 3969 +ER_GROUP_REPLICATION_USER_EMPTY_MSG = 3970 +ER_GROUP_REPLICATION_USER_MANDATORY_MSG = 3971 +ER_GROUP_REPLICATION_PASSWORD_LENGTH = 3972 +ER_SUBQUERY_TRANSFORM_REJECTED = 3973 +ER_DA_GRP_RPL_RECOVERY_ENDPOINT_FORMAT = 3974 +ER_DA_GRP_RPL_RECOVERY_ENDPOINT_INVALID = 3975 +ER_WRONG_VALUE_FOR_VAR_PLUS_ACTIONABLE_PART = 3976 +ER_STATEMENT_NOT_ALLOWED_AFTER_START_TRANSACTION = 3977 +ER_FOREIGN_KEY_WITH_ATOMIC_CREATE_SELECT = 3978 +ER_NOT_ALLOWED_WITH_START_TRANSACTION = 3979 +ER_INVALID_JSON_ATTRIBUTE = 3980 +ER_ENGINE_ATTRIBUTE_NOT_SUPPORTED = 3981 +ER_INVALID_USER_ATTRIBUTE_JSON = 3982 +ER_INNODB_REDO_DISABLED = 3983 +ER_INNODB_REDO_ARCHIVING_ENABLED = 3984 +ER_MDL_OUT_OF_RESOURCES = 3985 +ER_IMPLICIT_COMPARISON_FOR_JSON = 3986 +ER_FUNCTION_DOES_NOT_SUPPORT_CHARACTER_SET = 3987 +ER_IMPOSSIBLE_STRING_CONVERSION = 3988 +ER_SCHEMA_READ_ONLY = 3989 +ER_RPL_ASYNC_RECONNECT_GTID_MODE_OFF = 3990 +ER_RPL_ASYNC_RECONNECT_AUTO_POSITION_OFF = 3991 +ER_DISABLE_GTID_MODE_REQUIRES_ASYNC_RECONNECT_OFF = 3992 +ER_DISABLE_AUTO_POSITION_REQUIRES_ASYNC_RECONNECT_OFF = 3993 +ER_INVALID_PARAMETER_USE = 3994 +ER_CHARACTER_SET_MISMATCH = 3995 +ER_WARN_VAR_VALUE_CHANGE_NOT_SUPPORTED = 3996 +ER_INVALID_TIME_ZONE_INTERVAL = 3997 +ER_INVALID_CAST = 3998 +ER_HYPERGRAPH_NOT_SUPPORTED_YET = 3999 +ER_WARN_HYPERGRAPH_EXPERIMENTAL = 4000 +ER_DA_NO_ERROR_LOG_PARSER_CONFIGURED = 4001 +ER_DA_ERROR_LOG_TABLE_DISABLED = 4002 +ER_DA_ERROR_LOG_MULTIPLE_FILTERS = 4003 +ER_DA_CANT_OPEN_ERROR_LOG = 4004 +ER_USER_REFERENCED_AS_DEFINER = 4005 +ER_CANNOT_USER_REFERENCED_AS_DEFINER = 4006 +ER_REGEX_NUMBER_TOO_BIG = 4007 +ER_SPVAR_NONINTEGER_TYPE = 4008 +WARN_UNSUPPORTED_ACL_TABLES_READ = 4009 +ER_BINLOG_UNSAFE_ACL_TABLE_READ_IN_DML_DDL = 4010 +ER_STOP_REPLICA_MONITOR_IO_THREAD_TIMEOUT = 4011 +ER_STARTING_REPLICA_MONITOR_IO_THREAD = 4012 +ER_CANT_USE_ANONYMOUS_TO_GTID_WITH_GTID_MODE_NOT_ON = 4013 +ER_CANT_COMBINE_ANONYMOUS_TO_GTID_AND_AUTOPOSITION = 4014 +ER_ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS_REQUIRES_GTID_MODE_ON = 4015 +ER_SQL_REPLICA_SKIP_COUNTER_USED_WITH_GTID_MODE_ON = 4016 +ER_USING_ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS_AS_LOCAL_OR_UUID = 4017 +ER_CANT_SET_ANONYMOUS_TO_GTID_AND_WAIT_UNTIL_SQL_THD_AFTER_GTIDS = 4018 +ER_CANT_SET_SQL_AFTER_OR_BEFORE_GTIDS_WITH_ANONYMOUS_TO_GTID = 4019 +ER_ANONYMOUS_TO_GTID_UUID_SAME_AS_GROUP_NAME = 4020 +ER_CANT_USE_SAME_UUID_AS_GROUP_NAME = 4021 +ER_GRP_RPL_RECOVERY_CHANNEL_STILL_RUNNING = 4022 +ER_INNODB_INVALID_AUTOEXTEND_SIZE_VALUE = 4023 +ER_INNODB_INCOMPATIBLE_WITH_TABLESPACE = 4024 +ER_INNODB_AUTOEXTEND_SIZE_OUT_OF_RANGE = 4025 +ER_CANNOT_USE_AUTOEXTEND_SIZE_CLAUSE = 4026 +ER_ROLE_GRANTED_TO_ITSELF = 4027 +ER_TABLE_MUST_HAVE_A_VISIBLE_COLUMN = 4028 +ER_INNODB_COMPRESSION_FAILURE = 4029 +ER_WARN_ASYNC_CONN_FAILOVER_NETWORK_NAMESPACE = 4030 +ER_CLIENT_INTERACTION_TIMEOUT = 4031 +ER_INVALID_CAST_TO_GEOMETRY = 4032 +ER_INVALID_CAST_POLYGON_RING_DIRECTION = 4033 +ER_GIS_DIFFERENT_SRIDS_AGGREGATION = 4034 +ER_RELOAD_KEYRING_FAILURE = 4035 +ER_SDI_GET_KEYS_INVALID_TABLESPACE = 4036 +ER_CHANGE_RPL_SRC_WRONG_COMPRESSION_ALGORITHM_SIZE = 4037 +ER_WARN_DEPRECATED_TLS_VERSION_FOR_CHANNEL_CLI = 4038 +ER_CANT_USE_SAME_UUID_AS_VIEW_CHANGE_UUID = 4039 +ER_ANONYMOUS_TO_GTID_UUID_SAME_AS_VIEW_CHANGE_UUID = 4040 +ER_GRP_RPL_VIEW_CHANGE_UUID_FAIL_GET_VARIABLE = 4041 +ER_WARN_ADUIT_LOG_MAX_SIZE_AND_PRUNE_SECONDS = 4042 +ER_WARN_ADUIT_LOG_MAX_SIZE_CLOSE_TO_ROTATE_ON_SIZE = 4043 +ER_KERBEROS_CREATE_USER = 4044 +ER_INSTALL_PLUGIN_CONFLICT_CLIENT = 4045 +ER_DA_ERROR_LOG_COMPONENT_FLUSH_FAILED = 4046 +ER_WARN_SQL_AFTER_MTS_GAPS_GAP_NOT_CALCULATED = 4047 +ER_INVALID_ASSIGNMENT_TARGET = 4048 +ER_OPERATION_NOT_ALLOWED_ON_GR_SECONDARY = 4049 +ER_GRP_RPL_FAILOVER_CHANNEL_STATUS_PROPAGATION = 4050 +ER_WARN_AUDIT_LOG_FORMAT_UNIX_TIMESTAMP_ONLY_WHEN_JSON = 4051 +ER_INVALID_MFA_PLUGIN_SPECIFIED = 4052 +ER_IDENTIFIED_BY_UNSUPPORTED = 4053 +ER_INVALID_PLUGIN_FOR_REGISTRATION = 4054 +ER_PLUGIN_REQUIRES_REGISTRATION = 4055 +ER_MFA_METHOD_EXISTS = 4056 +ER_MFA_METHOD_NOT_EXISTS = 4057 +ER_AUTHENTICATION_POLICY_MISMATCH = 4058 +ER_PLUGIN_REGISTRATION_DONE = 4059 +ER_INVALID_USER_FOR_REGISTRATION = 4060 +ER_USER_REGISTRATION_FAILED = 4061 +ER_MFA_METHODS_INVALID_ORDER = 4062 +ER_MFA_METHODS_IDENTICAL = 4063 +ER_INVALID_MFA_OPERATIONS_FOR_PASSWORDLESS_USER = 4064 +ER_CHANGE_REPLICATION_SOURCE_NO_OPTIONS_FOR_GTID_ONLY = 4065 +ER_CHANGE_REP_SOURCE_CANT_DISABLE_REQ_ROW_FORMAT_WITH_GTID_ONLY = 4066 +ER_CHANGE_REP_SOURCE_CANT_DISABLE_AUTO_POSITION_WITH_GTID_ONLY = 4067 +ER_CHANGE_REP_SOURCE_CANT_DISABLE_GTID_ONLY_WITHOUT_POSITIONS = 4068 +ER_CHANGE_REP_SOURCE_CANT_DISABLE_AUTO_POS_WITHOUT_POSITIONS = 4069 +ER_CHANGE_REP_SOURCE_GR_CHANNEL_WITH_GTID_MODE_NOT_ON = 4070 +ER_CANT_USE_GTID_ONLY_WITH_GTID_MODE_NOT_ON = 4071 +ER_WARN_C_DISABLE_GTID_ONLY_WITH_SOURCE_AUTO_POS_INVALID_POS = 4072 +ER_DA_SSL_FIPS_MODE_ERROR = 4073 +CR_UNKNOWN_ERROR = 2000 +CR_SOCKET_CREATE_ERROR = 2001 +CR_CONNECTION_ERROR = 2002 +CR_CONN_HOST_ERROR = 2003 +CR_IPSOCK_ERROR = 2004 +CR_UNKNOWN_HOST = 2005 +CR_SERVER_GONE_ERROR = 2006 +CR_VERSION_ERROR = 2007 +CR_OUT_OF_MEMORY = 2008 +CR_WRONG_HOST_INFO = 2009 +CR_LOCALHOST_CONNECTION = 2010 +CR_TCP_CONNECTION = 2011 +CR_SERVER_HANDSHAKE_ERR = 2012 +CR_SERVER_LOST = 2013 +CR_COMMANDS_OUT_OF_SYNC = 2014 +CR_NAMEDPIPE_CONNECTION = 2015 +CR_NAMEDPIPEWAIT_ERROR = 2016 +CR_NAMEDPIPEOPEN_ERROR = 2017 +CR_NAMEDPIPESETSTATE_ERROR = 2018 +CR_CANT_READ_CHARSET = 2019 +CR_NET_PACKET_TOO_LARGE = 2020 +CR_EMBEDDED_CONNECTION = 2021 +CR_PROBE_SLAVE_STATUS = 2022 +CR_PROBE_SLAVE_HOSTS = 2023 +CR_PROBE_SLAVE_CONNECT = 2024 +CR_PROBE_MASTER_CONNECT = 2025 +CR_SSL_CONNECTION_ERROR = 2026 +CR_MALFORMED_PACKET = 2027 +CR_WRONG_LICENSE = 2028 +CR_NULL_POINTER = 2029 +CR_NO_PREPARE_STMT = 2030 +CR_PARAMS_NOT_BOUND = 2031 +CR_DATA_TRUNCATED = 2032 +CR_NO_PARAMETERS_EXISTS = 2033 +CR_INVALID_PARAMETER_NO = 2034 +CR_INVALID_BUFFER_USE = 2035 +CR_UNSUPPORTED_PARAM_TYPE = 2036 +CR_SHARED_MEMORY_CONNECTION = 2037 +CR_SHARED_MEMORY_CONNECT_REQUEST_ERROR = 2038 +CR_SHARED_MEMORY_CONNECT_ANSWER_ERROR = 2039 +CR_SHARED_MEMORY_CONNECT_FILE_MAP_ERROR = 2040 +CR_SHARED_MEMORY_CONNECT_MAP_ERROR = 2041 +CR_SHARED_MEMORY_FILE_MAP_ERROR = 2042 +CR_SHARED_MEMORY_MAP_ERROR = 2043 +CR_SHARED_MEMORY_EVENT_ERROR = 2044 +CR_SHARED_MEMORY_CONNECT_ABANDONED_ERROR = 2045 +CR_SHARED_MEMORY_CONNECT_SET_ERROR = 2046 +CR_CONN_UNKNOW_PROTOCOL = 2047 +CR_INVALID_CONN_HANDLE = 2048 +CR_UNUSED_1 = 2049 +CR_FETCH_CANCELED = 2050 +CR_NO_DATA = 2051 +CR_NO_STMT_METADATA = 2052 +CR_NO_RESULT_SET = 2053 +CR_NOT_IMPLEMENTED = 2054 +CR_SERVER_LOST_EXTENDED = 2055 +CR_STMT_CLOSED = 2056 +CR_NEW_STMT_METADATA = 2057 +CR_ALREADY_CONNECTED = 2058 +CR_AUTH_PLUGIN_CANNOT_LOAD = 2059 +CR_DUPLICATE_CONNECTION_ATTR = 2060 +CR_AUTH_PLUGIN_ERR = 2061 +CR_INSECURE_API_ERR = 2062 +CR_FILE_NAME_TOO_LONG = 2063 +CR_SSL_FIPS_MODE_ERR = 2064 +CR_DEPRECATED_COMPRESSION_NOT_SUPPORTED = 2065 +CR_COMPRESSION_WRONGLY_CONFIGURED = 2066 +CR_KERBEROS_USER_NOT_FOUND = 2067 +CR_LOAD_DATA_LOCAL_INFILE_REJECTED = 2068 +CR_LOAD_DATA_LOCAL_INFILE_REALPATH_FAIL = 2069 +CR_DNS_SRV_LOOKUP_FAILED = 2070 +CR_MANDATORY_TRACKER_NOT_FOUND = 2071 +CR_INVALID_FACTOR_NO = 2072 +# End MySQL Errors + +# Start X Plugin Errors +ER_X_BAD_MESSAGE = 5000 +ER_X_CAPABILITIES_PREPARE_FAILED = 5001 +ER_X_CAPABILITY_NOT_FOUND = 5002 +ER_X_INVALID_PROTOCOL_DATA = 5003 +ER_X_BAD_CONNECTION_SESSION_ATTRIBUTE_VALUE_LENGTH = 5004 +ER_X_BAD_CONNECTION_SESSION_ATTRIBUTE_KEY_LENGTH = 5005 +ER_X_BAD_CONNECTION_SESSION_ATTRIBUTE_EMPTY_KEY = 5006 +ER_X_BAD_CONNECTION_SESSION_ATTRIBUTE_LENGTH = 5007 +ER_X_BAD_CONNECTION_SESSION_ATTRIBUTE_TYPE = 5008 +ER_X_CAPABILITY_SET_NOT_ALLOWED = 5009 +ER_X_SERVICE_ERROR = 5010 +ER_X_SESSION = 5011 +ER_X_INVALID_ARGUMENT = 5012 +ER_X_MISSING_ARGUMENT = 5013 +ER_X_BAD_INSERT_DATA = 5014 +ER_X_CMD_NUM_ARGUMENTS = 5015 +ER_X_CMD_ARGUMENT_TYPE = 5016 +ER_X_CMD_ARGUMENT_VALUE = 5017 +ER_X_BAD_UPSERT_DATA = 5018 +ER_X_DUPLICATED_CAPABILITIES = 5019 +ER_X_CMD_ARGUMENT_OBJECT_EMPTY = 5020 +ER_X_CMD_INVALID_ARGUMENT = 5021 +ER_X_BAD_UPDATE_DATA = 5050 +ER_X_BAD_TYPE_OF_UPDATE = 5051 +ER_X_BAD_COLUMN_TO_UPDATE = 5052 +ER_X_BAD_MEMBER_TO_UPDATE = 5053 +ER_X_BAD_STATEMENT_ID = 5110 +ER_X_BAD_CURSOR_ID = 5111 +ER_X_BAD_SCHEMA = 5112 +ER_X_BAD_TABLE = 5113 +ER_X_BAD_PROJECTION = 5114 +ER_X_DOC_ID_MISSING = 5115 +ER_X_DUPLICATE_ENTRY = 5116 +ER_X_DOC_REQUIRED_FIELD_MISSING = 5117 +ER_X_PROJ_BAD_KEY_NAME = 5120 +ER_X_BAD_DOC_PATH = 5121 +ER_X_CURSOR_EXISTS = 5122 +ER_X_CURSOR_REACHED_EOF = 5123 +ER_X_PREPARED_STATMENT_CAN_HAVE_ONE_CURSOR = 5131 +ER_X_PREPARED_EXECUTE_ARGUMENT_NOT_SUPPORTED = 5133 +ER_X_PREPARED_EXECUTE_ARGUMENT_CONSISTENCY = 5134 +ER_X_EXPR_BAD_OPERATOR = 5150 +ER_X_EXPR_BAD_NUM_ARGS = 5151 +ER_X_EXPR_MISSING_ARG = 5152 +ER_X_EXPR_BAD_TYPE_VALUE = 5153 +ER_X_EXPR_BAD_VALUE = 5154 +ER_X_INVALID_COLLECTION = 5156 +ER_X_INVALID_ADMIN_COMMAND = 5157 +ER_X_EXPECT_NOT_OPEN = 5158 +ER_X_EXPECT_NO_ERROR_FAILED = 5159 +ER_X_EXPECT_BAD_CONDITION = 5160 +ER_X_EXPECT_BAD_CONDITION_VALUE = 5161 +ER_X_INVALID_NAMESPACE = 5162 +ER_X_BAD_NOTICE = 5163 +ER_X_CANNOT_DISABLE_NOTICE = 5164 +ER_X_BAD_CONFIGURATION = 5165 +ER_X_MYSQLX_ACCOUNT_MISSING_PERMISSIONS = 5167 +ER_X_EXPECT_FIELD_EXISTS_FAILED = 5168 +ER_X_BAD_LOCKING = 5169 +ER_X_FRAME_COMPRESSION_DISABLED = 5170 +ER_X_DECOMPRESSION_FAILED = 5171 +ER_X_BAD_COMPRESSED_FRAME = 5174 +ER_X_CAPABILITY_COMPRESSION_INVALID_ALGORITHM = 5175 +ER_X_CAPABILITY_COMPRESSION_INVALID_SERVER_STYLE = 5176 +ER_X_CAPABILITY_COMPRESSION_INVALID_CLIENT_STYLE = 5177 +ER_X_CAPABILITY_COMPRESSION_INVALID_OPTION = 5178 +ER_X_CAPABILITY_COMPRESSION_MISSING_REQUIRED_FIELDS = 5179 +ER_X_DOCUMENT_DOESNT_MATCH_EXPECTED_SCHEMA = 5180 +ER_X_COLLECTION_OPTION_DOESNT_EXISTS = 5181 +ER_X_INVALID_VALIDATION_SCHEMA = 5182 +# End X Plugin Errors diff --git a/mysql/connector/errors.py b/mysql/connector/errors.py new file mode 100644 index 0000000..53ebf72 --- /dev/null +++ b/mysql/connector/errors.py @@ -0,0 +1,336 @@ +# Copyright (c) 2009, 2023, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Python exceptions.""" +from typing import Dict, Mapping, Optional, Tuple, Type, Union + +from .locales import get_client_error +from .types import StrOrBytes +from .utils import read_bytes, read_int + + +class Error(Exception): + """Exception that is base class for all other error exceptions""" + + def __init__( + self, + msg: Optional[str] = None, + errno: Optional[int] = None, + values: Optional[Tuple[Union[int, str], ...]] = None, + sqlstate: Optional[str] = None, + ) -> None: + super().__init__() + self.msg = msg + self._full_msg = self.msg + self.errno = errno or -1 + self.sqlstate = sqlstate + + if not self.msg and (2000 <= self.errno < 3000): + self.msg = get_client_error(self.errno) + if values is not None: + try: + self.msg = self.msg % values + except TypeError as err: + self.msg = f"{self.msg} (Warning: {err})" + elif not self.msg: + self._full_msg = self.msg = "Unknown error" + + if self.msg and self.errno != -1: + fields = {"errno": self.errno, "msg": self.msg} + if self.sqlstate: + fmt = "{errno} ({state}): {msg}" + fields["state"] = self.sqlstate + else: + fmt = "{errno}: {msg}" + self._full_msg = fmt.format(**fields) + + self.args = (self.errno, self._full_msg, self.sqlstate) + + def __str__(self) -> str: + return self._full_msg + + +class Warning(Exception): # pylint: disable=redefined-builtin + """Exception for important warnings""" + + +class InterfaceError(Error): + """Exception for errors related to the interface""" + + +class DatabaseError(Error): + """Exception for errors related to the database""" + + +class InternalError(DatabaseError): + """Exception for errors internal database errors""" + + +class OperationalError(DatabaseError): + """Exception for errors related to the database's operation""" + + +class ProgrammingError(DatabaseError): + """Exception for errors programming errors""" + + +class IntegrityError(DatabaseError): + """Exception for errors regarding relational integrity""" + + +class DataError(DatabaseError): + """Exception for errors reporting problems with processed data""" + + +class NotSupportedError(DatabaseError): + """Exception for errors when an unsupported database feature was used""" + + +class PoolError(Error): + """Exception for errors relating to connection pooling""" + + +ErrorClassTypes = Union[ + Type[Error], + Type[InterfaceError], + Type[DatabaseError], + Type[InternalError], + Type[OperationalError], + Type[ProgrammingError], + Type[IntegrityError], + Type[DataError], + Type[NotSupportedError], + Type[PoolError], +] +ErrorTypes = Union[ + Error, + InterfaceError, + DatabaseError, + InternalError, + OperationalError, + ProgrammingError, + IntegrityError, + DataError, + NotSupportedError, + PoolError, + Warning, +] +# _CUSTOM_ERROR_EXCEPTIONS holds custom exceptions and is used by the +# function custom_error_exception. _ERROR_EXCEPTIONS (at bottom of module) +# is similar, but hardcoded exceptions. +_CUSTOM_ERROR_EXCEPTIONS: Dict[int, ErrorClassTypes] = {} + + +def custom_error_exception( + error: Optional[Union[int, Dict[int, Optional[ErrorClassTypes]]]] = None, + exception: Optional[ErrorClassTypes] = None, +) -> Mapping[int, Optional[ErrorClassTypes]]: + """Define custom exceptions for MySQL server errors + + This function defines custom exceptions for MySQL server errors and + returns the current set customizations. + + If error is a MySQL Server error number, then you have to pass also the + exception class. + + The error argument can also be a dictionary in which case the key is + the server error number, and value the exception to be raised. + + If none of the arguments are given, then custom_error_exception() will + simply return the current set customizations. + + To reset the customizations, simply supply an empty dictionary. + + Examples: + import mysql.connector + from mysql.connector import errorcode + + # Server error 1028 should raise a DatabaseError + mysql.connector.custom_error_exception( + 1028, mysql.connector.DatabaseError) + + # Or using a dictionary: + mysql.connector.custom_error_exception({ + 1028: mysql.connector.DatabaseError, + 1029: mysql.connector.OperationalError, + }) + + # Reset + mysql.connector.custom_error_exception({}) + + Returns a dictionary. + """ + global _CUSTOM_ERROR_EXCEPTIONS # pylint: disable=global-statement + + if isinstance(error, dict) and not error: + _CUSTOM_ERROR_EXCEPTIONS = {} + return _CUSTOM_ERROR_EXCEPTIONS + + if not error and not exception: + return _CUSTOM_ERROR_EXCEPTIONS + + if not isinstance(error, (int, dict)): + raise ValueError("The error argument should be either an integer or dictionary") + + if isinstance(error, int): + error = {error: exception} + + for errno, _exception in error.items(): + if not isinstance(errno, int): + raise ValueError("Error number should be an integer") + try: + if _exception is None or not issubclass(_exception, Exception): + raise TypeError + except TypeError as err: + raise ValueError("Exception should be subclass of Exception") from err + _CUSTOM_ERROR_EXCEPTIONS[errno] = _exception + + return _CUSTOM_ERROR_EXCEPTIONS + + +def get_mysql_exception( + errno: int, + msg: Optional[str] = None, + sqlstate: Optional[str] = None, + warning: Optional[bool] = False, +) -> ErrorTypes: + """Get the exception matching the MySQL error + + This function will return an exception based on the SQLState. The given + message will be passed on in the returned exception. + + The exception returned can be customized using the + mysql.connector.custom_error_exception() function. + + Returns an Exception + """ + try: + return _CUSTOM_ERROR_EXCEPTIONS[errno](msg=msg, errno=errno, sqlstate=sqlstate) + except KeyError: + # Error was not mapped to particular exception + pass + + try: + return _ERROR_EXCEPTIONS[errno](msg=msg, errno=errno, sqlstate=sqlstate) + except KeyError: + # Error was not mapped to particular exception + pass + + if not sqlstate: + if warning: + return Warning(errno, msg) + return DatabaseError(msg=msg, errno=errno) + + try: + return _SQLSTATE_CLASS_EXCEPTION[sqlstate[0:2]]( + msg=msg, errno=errno, sqlstate=sqlstate + ) + except KeyError: + # Return default InterfaceError + return DatabaseError(msg=msg, errno=errno, sqlstate=sqlstate) + + +def get_exception(packet: bytes) -> ErrorTypes: + """Returns an exception object based on the MySQL error + + Returns an exception object based on the MySQL error in the given + packet. + + Returns an Error-Object. + """ + errno = errmsg = None + + try: + if packet[4] != 255: + raise ValueError("Packet is not an error packet") + except IndexError as err: + return InterfaceError(f"Failed getting Error information ({err})") + + sqlstate: Optional[StrOrBytes] = None + try: + packet = packet[5:] + packet, errno = read_int(packet, 2) + if packet[0] != 35: + # Error without SQLState + if isinstance(packet, (bytes, bytearray)): + errmsg = packet.decode("utf8") + else: + errmsg = packet + else: + packet, sqlstate = read_bytes(packet[1:], 5) + sqlstate = sqlstate.decode("utf8") + errmsg = packet.decode("utf8") + except (IndexError, UnicodeError) as err: + return InterfaceError(f"Failed getting Error information ({err})") + return get_mysql_exception(errno, errmsg, sqlstate) # type: ignore[arg-type] + + +_SQLSTATE_CLASS_EXCEPTION: Dict[str, ErrorClassTypes] = { + "02": DataError, # no data + "07": DatabaseError, # dynamic SQL error + "08": OperationalError, # connection exception + "0A": NotSupportedError, # feature not supported + "21": DataError, # cardinality violation + "22": DataError, # data exception + "23": IntegrityError, # integrity constraint violation + "24": ProgrammingError, # invalid cursor state + "25": ProgrammingError, # invalid transaction state + "26": ProgrammingError, # invalid SQL statement name + "27": ProgrammingError, # triggered data change violation + "28": ProgrammingError, # invalid authorization specification + "2A": ProgrammingError, # direct SQL syntax error or access rule violation + "2B": DatabaseError, # dependent privilege descriptors still exist + "2C": ProgrammingError, # invalid character set name + "2D": DatabaseError, # invalid transaction termination + "2E": DatabaseError, # invalid connection name + "33": DatabaseError, # invalid SQL descriptor name + "34": ProgrammingError, # invalid cursor name + "35": ProgrammingError, # invalid condition number + "37": ProgrammingError, # dynamic SQL syntax error or access rule violation + "3C": ProgrammingError, # ambiguous cursor name + "3D": ProgrammingError, # invalid catalog name + "3F": ProgrammingError, # invalid schema name + "40": InternalError, # transaction rollback + "42": ProgrammingError, # syntax error or access rule violation + "44": InternalError, # with check option violation + "HZ": OperationalError, # remote database access + "XA": IntegrityError, + "0K": OperationalError, + "HY": DatabaseError, # default when no SQLState provided by MySQL server +} + +_ERROR_EXCEPTIONS: Dict[int, ErrorClassTypes] = { + 1243: ProgrammingError, + 1210: ProgrammingError, + 2002: InterfaceError, + 2013: OperationalError, + 2049: NotSupportedError, + 2055: OperationalError, + 2061: InterfaceError, + 2026: InterfaceError, +} diff --git a/mysql/connector/locales/__init__.py b/mysql/connector/locales/__init__.py new file mode 100644 index 0000000..f6727ee --- /dev/null +++ b/mysql/connector/locales/__init__.py @@ -0,0 +1,80 @@ +# Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Translations.""" + +from typing import List, Optional, Union + +__all__: List[str] = ["get_client_error"] + +from .. import errorcode + + +def get_client_error(error: Union[int, str], language: str = "eng") -> Optional[str]: + """Lookup client error + + This function will lookup the client error message based on the given + error and return the error message. If the error was not found, + None will be returned. + + Error can be either an integer or a string. For example: + error: 2000 + error: CR_UNKNOWN_ERROR + + The language attribute can be used to retrieve a localized message, when + available. + + Returns a string or None. + """ + try: + tmp = __import__( + f"mysql.connector.locales.{language}", + globals(), + locals(), + ["client_error"], + ) + except ImportError: + raise ImportError( + f"No localization support for language '{language}'" + ) from None + client_error = tmp.client_error + + if isinstance(error, int): + errno = error + for key, value in errorcode.__dict__.items(): + if value == errno: + error = key + break + + if isinstance(error, (str)): + try: + return getattr(client_error, error) + except AttributeError: + return None + + raise ValueError("error argument needs to be either an integer or string") diff --git a/mysql/connector/locales/eng/__init__.py b/mysql/connector/locales/eng/__init__.py new file mode 100644 index 0000000..2e1c02b --- /dev/null +++ b/mysql/connector/locales/eng/__init__.py @@ -0,0 +1,30 @@ +# Copyright (c) 2012, 2017, Oracle and/or its affiliates. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""English Content +""" diff --git a/mysql/connector/locales/eng/client_error.py b/mysql/connector/locales/eng/client_error.py new file mode 100644 index 0000000..89fb355 --- /dev/null +++ b/mysql/connector/locales/eng/client_error.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2013, 2022, Oracle and/or its affiliates. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""MySQL Error Messages.""" + +# This file was auto-generated. +_GENERATED_ON = "2021-08-11" +_MYSQL_VERSION = (8, 0, 27) + +# pylint: disable=line-too-long +# Start MySQL Error messages +CR_UNKNOWN_ERROR = "Unknown MySQL error" +CR_SOCKET_CREATE_ERROR = "Can't create UNIX socket (%s)" +CR_CONNECTION_ERROR = ( + "Can't connect to local MySQL server through socket '%-.100s' (%s)" +) +CR_CONN_HOST_ERROR = "Can't connect to MySQL server on '%-.100s:%u' (%s)" +CR_IPSOCK_ERROR = "Can't create TCP/IP socket (%s)" +CR_UNKNOWN_HOST = "Unknown MySQL server host '%-.100s' (%s)" +CR_SERVER_GONE_ERROR = "MySQL server has gone away" +CR_VERSION_ERROR = "Protocol mismatch; server version = %s, client version = %s" +CR_OUT_OF_MEMORY = "MySQL client ran out of memory" +CR_WRONG_HOST_INFO = "Wrong host info" +CR_LOCALHOST_CONNECTION = "Localhost via UNIX socket" +CR_TCP_CONNECTION = "%-.100s via TCP/IP" +CR_SERVER_HANDSHAKE_ERR = "Error in server handshake" +CR_SERVER_LOST = "Lost connection to MySQL server during query" +CR_COMMANDS_OUT_OF_SYNC = "Commands out of sync; you can't run this command now" +CR_NAMEDPIPE_CONNECTION = "Named pipe: %-.32s" +CR_NAMEDPIPEWAIT_ERROR = "Can't wait for named pipe to host: %-.64s pipe: %-.32s (%s)" +CR_NAMEDPIPEOPEN_ERROR = "Can't open named pipe to host: %-.64s pipe: %-.32s (%s)" +CR_NAMEDPIPESETSTATE_ERROR = ( + "Can't set state of named pipe to host: %-.64s pipe: %-.32s (%s)" +) +CR_CANT_READ_CHARSET = "Can't initialize character set %-.32s (path: %-.100s)" +CR_NET_PACKET_TOO_LARGE = "Got packet bigger than 'max_allowed_packet' bytes" +CR_EMBEDDED_CONNECTION = "Embedded server" +CR_PROBE_SLAVE_STATUS = "Error on SHOW SLAVE STATUS:" +CR_PROBE_SLAVE_HOSTS = "Error on SHOW SLAVE HOSTS:" +CR_PROBE_SLAVE_CONNECT = "Error connecting to slave:" +CR_PROBE_MASTER_CONNECT = "Error connecting to master:" +CR_SSL_CONNECTION_ERROR = "SSL connection error: %-.100s" +CR_MALFORMED_PACKET = "Malformed packet" +CR_WRONG_LICENSE = "This client library is licensed only for use with MySQL servers having '%s' license" +CR_NULL_POINTER = "Invalid use of null pointer" +CR_NO_PREPARE_STMT = "Statement not prepared" +CR_PARAMS_NOT_BOUND = "No data supplied for parameters in prepared statement" +CR_DATA_TRUNCATED = "Data truncated" +CR_NO_PARAMETERS_EXISTS = "No parameters exist in the statement" +CR_INVALID_PARAMETER_NO = "Invalid parameter number" +CR_INVALID_BUFFER_USE = ( + "Can't send long data for non-string/non-binary data types (parameter: %s)" +) +CR_UNSUPPORTED_PARAM_TYPE = "Using unsupported buffer type: %s (parameter: %s)" +CR_SHARED_MEMORY_CONNECTION = "Shared memory: %-.100s" +CR_SHARED_MEMORY_CONNECT_REQUEST_ERROR = ( + "Can't open shared memory; client could not create request event (%s)" +) +CR_SHARED_MEMORY_CONNECT_ANSWER_ERROR = ( + "Can't open shared memory; no answer event received from server (%s)" +) +CR_SHARED_MEMORY_CONNECT_FILE_MAP_ERROR = ( + "Can't open shared memory; server could not allocate file mapping (%s)" +) +CR_SHARED_MEMORY_CONNECT_MAP_ERROR = ( + "Can't open shared memory; server could not get pointer to file mapping (%s)" +) +CR_SHARED_MEMORY_FILE_MAP_ERROR = ( + "Can't open shared memory; client could not allocate file mapping (%s)" +) +CR_SHARED_MEMORY_MAP_ERROR = ( + "Can't open shared memory; client could not get pointer to file mapping (%s)" +) +CR_SHARED_MEMORY_EVENT_ERROR = ( + "Can't open shared memory; client could not create %s event (%s)" +) +CR_SHARED_MEMORY_CONNECT_ABANDONED_ERROR = ( + "Can't open shared memory; no answer from server (%s)" +) +CR_SHARED_MEMORY_CONNECT_SET_ERROR = ( + "Can't open shared memory; cannot send request event to server (%s)" +) +CR_CONN_UNKNOW_PROTOCOL = "Wrong or unknown protocol" +CR_INVALID_CONN_HANDLE = "Invalid connection handle" +CR_UNUSED_1 = "Connection using old (pre-4.1.1) authentication protocol refused (client option 'secure_auth' enabled)" +CR_FETCH_CANCELED = "Row retrieval was canceled by mysql_stmt_close() call" +CR_NO_DATA = "Attempt to read column without prior row fetch" +CR_NO_STMT_METADATA = "Prepared statement contains no metadata" +CR_NO_RESULT_SET = ( + "Attempt to read a row while there is no result set associated with the statement" +) +CR_NOT_IMPLEMENTED = "This feature is not implemented yet" +CR_SERVER_LOST_EXTENDED = "Lost connection to MySQL server at '%s', system error: %s" +CR_STMT_CLOSED = "Statement closed indirectly because of a preceding %s() call" +CR_NEW_STMT_METADATA = "The number of columns in the result set differs from the number of bound buffers. You must reset the statement, rebind the result set columns, and execute the statement again" +CR_ALREADY_CONNECTED = ( + "This handle is already connected. Use a separate handle for each connection." +) +CR_AUTH_PLUGIN_CANNOT_LOAD = "Authentication plugin '%s' cannot be loaded: %s" +CR_DUPLICATE_CONNECTION_ATTR = "There is an attribute with the same name already" +CR_AUTH_PLUGIN_ERR = "Authentication plugin '%s' reported error: %s" +CR_INSECURE_API_ERR = "Insecure API function call: '%s' Use instead: '%s'" +CR_FILE_NAME_TOO_LONG = "File name is too long" +CR_SSL_FIPS_MODE_ERR = "Set FIPS mode ON/STRICT failed" +CR_DEPRECATED_COMPRESSION_NOT_SUPPORTED = ( + "Compression protocol not supported with asynchronous protocol" +) +CR_COMPRESSION_WRONGLY_CONFIGURED = ( + "Connection failed due to wrongly configured compression algorithm" +) +CR_KERBEROS_USER_NOT_FOUND = ( + "SSO user not found, Please perform SSO authentication using kerberos." +) +CR_LOAD_DATA_LOCAL_INFILE_REJECTED = ( + "LOAD DATA LOCAL INFILE file request rejected due to restrictions on access." +) +CR_LOAD_DATA_LOCAL_INFILE_REALPATH_FAIL = ( + "Determining the real path for '%s' failed with error (%s): %s" +) +CR_DNS_SRV_LOOKUP_FAILED = "DNS SRV lookup failed with error : %s" +CR_MANDATORY_TRACKER_NOT_FOUND = ( + "Client does not recognise tracker type %s marked as mandatory by server." +) +CR_INVALID_FACTOR_NO = "Invalid first argument for MYSQL_OPT_USER_PASSWORD option. Valid value should be between 1 and 3 inclusive." +# End MySQL Error messages diff --git a/mysql/connector/logger.py b/mysql/connector/logger.py new file mode 100644 index 0000000..c64162a --- /dev/null +++ b/mysql/connector/logger.py @@ -0,0 +1,33 @@ +# Copyright (c) 2022, 2023, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +"""Setup of the `mysql.connector` logger.""" + +import logging + +logger = logging.getLogger("mysql.connector") diff --git a/mysql/connector/network.py b/mysql/connector/network.py new file mode 100644 index 0000000..e3ff973 --- /dev/null +++ b/mysql/connector/network.py @@ -0,0 +1,744 @@ +# Copyright (c) 2012, 2023, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# mypy: disable-error-code="attr-defined" + +"""Module implementing low-level socket communication with MySQL servers. +""" + +import os +import socket +import struct +import warnings +import zlib + +from abc import ABC, abstractmethod +from collections import deque + +try: + import ssl + + TLS_VERSIONS = { + "TLSv1": ssl.PROTOCOL_TLSv1, + "TLSv1.1": ssl.PROTOCOL_TLSv1_1, + "TLSv1.2": ssl.PROTOCOL_TLSv1_2, + } + # TLSv1.3 included in PROTOCOL_TLS, but PROTOCOL_TLS is not included on 3.4 + TLS_VERSIONS["TLSv1.3"] = ( + ssl.PROTOCOL_TLS + if hasattr(ssl, "PROTOCOL_TLS") + else ssl.PROTOCOL_SSLv23 # Alias of PROTOCOL_TLS + ) + TLS_V1_3_SUPPORTED = hasattr(ssl, "HAS_TLSv1_3") and ssl.HAS_TLSv1_3 +except ImportError: + # If import fails, we don't have SSL support. + TLS_V1_3_SUPPORTED = False + +from typing import Any, Deque, List, Optional, Tuple, Union + +from .errors import InterfaceError, NotSupportedError, OperationalError +from .types import StrOrBytesPath + +MIN_COMPRESS_LENGTH: int = 50 +MAX_PAYLOAD_LENGTH: int = 2**24 - 1 +PACKET_HEADER_LENGTH: int = 4 +COMPRESSED_PACKET_HEADER_LENGTH: int = 7 + + +def _strioerror(err: IOError) -> str: + """Reformat the IOError error message. + + This function reformats the IOError error message. + """ + return str(err) if not err.errno else f"{err.errno} {err.strerror}" + + +class NetworkBroker(ABC): + """Broker class interface. + + The network object is a broker used as a delegate by a socket object. Whenever the + socket wants to deliver or get packets to or from the MySQL server it needs to rely + on its network broker (netbroker). + + The netbroker sends `payloads` and receives `packets`. + + A packet is a bytes sequence, it has a header and body (referred to as payload). + The first `PACKET_HEADER_LENGTH` or `COMPRESSED_PACKET_HEADER_LENGTH` + (as appropriate) bytes correspond to the `header`, the remaining ones represent the + `payload`. + + The maximum payload length allowed to be sent per packet to the server is + `MAX_PAYLOAD_LENGTH`. When `send` is called with a payload whose length is greater + than `MAX_PAYLOAD_LENGTH` the netbroker breaks it down into packets, so the caller + of `send` can provide payloads of arbitrary length. + + Finally, data received by the netbroker comes directly from the server, expect to + get a packet for each call to `recv`. The received packet contains a header and + payload, the latter respecting `MAX_PAYLOAD_LENGTH`. + """ + + @abstractmethod + def send( + self, + sock: socket.socket, + address: str, + payload: bytes, + packet_number: Optional[int] = None, + compressed_packet_number: Optional[int] = None, + ) -> None: + """Send `payload` to the MySQL server. + + If provided a payload whose length is greater than `MAX_PAYLOAD_LENGTH`, it is + broken down into packets. + + Args: + sock: Object holding the socket connection. + address: Socket's location. + payload: Packet's body to send. + packet_number: Sequence id (packet ID) to attach to the header when sending + plain packets. + compressed_packet_number: Same as `packet_number` but used when sending + compressed packets. + + Raises: + :class:`OperationalError`: If something goes wrong while sending packets to + the MySQL server. + """ + + @abstractmethod + def recv(self, sock: socket.socket, address: str) -> bytearray: + """Get the next available packet from the MySQL server. + + Args: + sock: Object holding the socket connection. + address: Socket's location. + + Returns: + packet: A packet from the MySQL server. + + Raises: + :class:`OperationalError`: If something goes wrong while receiving packets + from the MySQL server. + :class:`InterfaceError`: If something goes wrong while receiving packets + from the MySQL server. + """ + + +class NetworkBrokerPlain(NetworkBroker): + """Broker class for MySQL socket communication.""" + + def __init__(self) -> None: + self._pktnr: int = -1 # packet number + + def _set_next_pktnr(self) -> None: + """Increment packet id.""" + self._pktnr = (self._pktnr + 1) % 256 + + def _send_pkt(self, sock: socket.socket, address: str, pkt: bytes) -> None: + """Write packet to the comm channel.""" + try: + sock.sendall(pkt) + except IOError as err: + raise OperationalError( + errno=2055, values=(address, _strioerror(err)) + ) from err + except AttributeError as err: + raise OperationalError(errno=2006) from err + + def _recv_chunk(self, sock: socket.socket, size: int = 0) -> bytearray: + """Read `size` bytes from the comm channel.""" + pkt = bytearray(size) + pkt_view = memoryview(pkt) + while size: + read = sock.recv_into(pkt_view, size) + if read == 0 and size > 0: + raise InterfaceError(errno=2013) + pkt_view = pkt_view[read:] + size -= read + return pkt + + def send( + self, + sock: socket.socket, + address: str, + payload: bytes, + packet_number: Optional[int] = None, + compressed_packet_number: Optional[int] = None, + ) -> None: + """Send payload to the MySQL server. + + If provided a payload whose length is greater than `MAX_PAYLOAD_LENGTH`, it is + broken down into packets. + """ + if packet_number is None: + self._set_next_pktnr() + else: + self._pktnr = packet_number + + # If the payload is larger than or equal to MAX_PAYLOAD_LENGTH + # the length is set to 2^24 - 1 (ff ff ff) and additional + # packets are sent with the rest of the payload until the + # payload of a packet is less than MAX_PAYLOAD_LENGTH. + if len(payload) >= MAX_PAYLOAD_LENGTH: + offset = 0 + for _ in range(len(payload) // MAX_PAYLOAD_LENGTH): + # payload_len, sequence_id, payload + self._send_pkt( + sock, + address, + b"\xff\xff\xff" + + struct.pack(" bytearray: + """Receive `one` packet from the MySQL server.""" + try: + # Read the header of the MySQL packet + header = self._recv_chunk(sock, size=PACKET_HEADER_LENGTH) + + # Pull the payload length and sequence id + payload_len, self._pktnr = ( + struct.unpack(" None: + super().__init__() + self._compressed_pktnr = -1 + self._queue_read: Deque[bytearray] = deque() + + @staticmethod + def _prepare_packets(payload: bytes, pktnr: int) -> List[bytes]: + """Prepare a payload for sending to the MySQL server.""" + pkts = [] + + # If the payload is larger than or equal to MAX_PAYLOAD_LENGTH + # the length is set to 2^24 - 1 (ff ff ff) and additional + # packets are sent with the rest of the payload until the + # payload of a packet is less than MAX_PAYLOAD_LENGTH. + if len(payload) >= MAX_PAYLOAD_LENGTH: + offset = 0 + for _ in range(len(payload) // MAX_PAYLOAD_LENGTH): + # payload length + sequence id + payload + pkts.append( + b"\xff\xff\xff" + + struct.pack(" None: + """Increment packet id.""" + self._compressed_pktnr = (self._compressed_pktnr + 1) % 256 + + def _send_pkt(self, sock: socket.socket, address: str, pkt: bytes) -> None: + """Compress packet and write it to the comm channel.""" + compressed_pkt = zlib.compress(pkt) + pkt = ( + struct.pack(" None: + """Send `payload` as compressed packets to the MySQL server. + + If provided a payload whose length is greater than `MAX_PAYLOAD_LENGTH`, it is + broken down into packets. + """ + # get next packet numbers + if packet_number is None: + self._set_next_pktnr() + else: + self._pktnr = packet_number + if compressed_packet_number is None: + self._set_next_compressed_pktnr() + else: + self._compressed_pktnr = compressed_packet_number + + payload_prep = bytearray(b"").join(self._prepare_packets(payload, self._pktnr)) + if len(payload) >= MAX_PAYLOAD_LENGTH - PACKET_HEADER_LENGTH: + # sending a MySQL payload of the size greater or equal to 2^24 - 5 + # via compression leads to at least one extra compressed packet + # WHY? let's say len(payload) is MAX_PAYLOAD_LENGTH - 3; when preparing + # the payload, a header of size PACKET_HEADER_LENGTH is pre-appended + # to the payload. This means that len(payload_prep) is + # MAX_PAYLOAD_LENGTH - 3 + PACKET_HEADER_LENGTH = MAX_PAYLOAD_LENGTH + 1 + # surpassing the maximum allowed payload size per packet. + offset = 0 + + # send several MySQL packets + for _ in range(len(payload_prep) // MAX_PAYLOAD_LENGTH): + self._send_pkt( + sock, address, payload_prep[offset : offset + MAX_PAYLOAD_LENGTH] + ) + self._set_next_compressed_pktnr() + offset += MAX_PAYLOAD_LENGTH + self._send_pkt(sock, address, payload_prep[offset:]) + else: + # send one MySQL packet + # For small packets it may be too costly to compress the packet. + # Usually payloads less than 50 bytes (MIN_COMPRESS_LENGTH) + # aren't compressed (see MySQL source code Documentation). + if len(payload) > MIN_COMPRESS_LENGTH: + # perform compression + self._send_pkt(sock, address, payload_prep) + else: + # skip compression + super()._send_pkt( + sock, + address, + struct.pack(" None: + """Handle reading of a compressed packet.""" + # compressed_pll stands for compressed payload length. + # Recalling that if uncompressed payload length == 0, the packet + # comes in uncompressed, so no decompression is needed. + compressed_pkt = super()._recv_chunk(sock, size=compressed_pll) + pkt = ( + compressed_pkt + if uncompressed_pll == 0 + else bytearray(zlib.decompress(compressed_pkt)) + ) + + offset = 0 + while offset < len(pkt): + # pll stands for payload length + pll = struct.unpack( + " len(pkt) - offset: + # More bytes need to be consumed + # Read the header of the next MySQL packet + header = super()._recv_chunk(sock, size=COMPRESSED_PACKET_HEADER_LENGTH) + + # compressed payload length, sequence id, uncompressed payload length + ( + compressed_pll, + self._compressed_pktnr, + uncompressed_pll, + ) = ( + struct.unpack(" bytearray: + """Receive `one` or `several` packets from the MySQL server, enqueue them, and + return the packet at the head. + """ + if not self._queue_read: + try: + # Read the header of the next MySQL packet + header = super()._recv_chunk(sock, size=COMPRESSED_PACKET_HEADER_LENGTH) + + # compressed payload length, sequence id, uncompressed payload length + ( + compressed_pll, + self._compressed_pktnr, + uncompressed_pll, + ) = ( + struct.unpack(" None: + """Network layer where transactions are made with plain (uncompressed) packets + is enabled by default. + """ + # holds the socket connection + self.sock: Optional[socket.socket] = None + self._connection_timeout: Optional[int] = None + self.server_host: Optional[str] = None + self._netbroker: NetworkBroker = NetworkBrokerPlain() + + def switch_to_compressed_mode(self) -> None: + """Enable network layer where transactions are made with compressed packets.""" + self._netbroker = NetworkBrokerCompressed() + + def shutdown(self) -> None: + """Shut down the socket before closing it.""" + try: + self.sock.shutdown(socket.SHUT_RDWR) + self.sock.close() + except (AttributeError, OSError): + pass + + def close_connection(self) -> None: + """Close the socket.""" + try: + self.sock.close() + except (AttributeError, OSError): + pass + + def __del__(self) -> None: + self.shutdown() + + def set_connection_timeout(self, timeout: Optional[int]) -> None: + """Set the connection timeout.""" + self._connection_timeout = timeout + if self.sock: + self.sock.settimeout(timeout) + + def switch_to_ssl( + self, + ca: StrOrBytesPath, + cert: StrOrBytesPath, + key: StrOrBytesPath, + verify_cert: bool = False, + verify_identity: bool = False, + cipher_suites: Optional[str] = None, + tls_versions: Optional[List[str]] = None, + ) -> None: + """Switch the socket to use SSL""" + if not self.sock: + raise InterfaceError(errno=2048) + + try: + if verify_cert: + cert_reqs = ssl.CERT_REQUIRED + elif verify_identity: + cert_reqs = ssl.CERT_OPTIONAL + else: + cert_reqs = ssl.CERT_NONE + + if tls_versions is None or not tls_versions: + context = ssl.create_default_context() + if not verify_identity: + context.check_hostname = False + else: + tls_versions.sort(reverse=True) + + tls_version = tls_versions[0] + if ( + not TLS_V1_3_SUPPORTED + and tls_version == "TLSv1.3" + and len(tls_versions) > 1 + ): + tls_version = tls_versions[1] + ssl_protocol = TLS_VERSIONS[tls_version] + context = ssl.SSLContext(ssl_protocol) + + if tls_version == "TLSv1.3": + if "TLSv1.2" not in tls_versions: + context.options |= ssl.OP_NO_TLSv1_2 + if "TLSv1.1" not in tls_versions: + context.options |= ssl.OP_NO_TLSv1_1 + if "TLSv1" not in tls_versions: + context.options |= ssl.OP_NO_TLSv1 + + context.check_hostname = False + context.verify_mode = cert_reqs + context.load_default_certs() + + if ca: + try: + context.load_verify_locations(ca) + except (IOError, ssl.SSLError) as err: + self.sock.close() + raise InterfaceError(f"Invalid CA Certificate: {err}") from err + if cert: + try: + context.load_cert_chain(cert, key) + except (IOError, ssl.SSLError) as err: + self.sock.close() + raise InterfaceError(f"Invalid Certificate/Key: {err}") from err + if cipher_suites: + context.set_ciphers(cipher_suites) + + if hasattr(self, "server_host"): + self.sock = context.wrap_socket( + self.sock, server_hostname=self.server_host + ) + else: + self.sock = context.wrap_socket(self.sock) + + if verify_identity: + context.check_hostname = True + hostnames: List[str] = [self.server_host] if self.server_host else [] + if os.name == "nt" and self.server_host == "localhost": + hostnames = ["localhost", "127.0.0.1"] + aliases = socket.gethostbyaddr(self.server_host) + hostnames.extend([aliases[0]] + aliases[1]) + match_found = False + errs = [] + for hostname in hostnames: + try: + # Deprecated in Python 3.7 without a replacement and + # should be removed in the future, since OpenSSL now + # performs hostname matching + # pylint: disable=deprecated-method + ssl.match_hostname(self.sock.getpeercert(), hostname) + # pylint: enable=deprecated-method + except ssl.CertificateError as err: + errs.append(str(err)) + else: + match_found = True + break + if not match_found: + self.sock.close() + raise InterfaceError( + f"Unable to verify server identity: {', '.join(errs)}" + ) + except NameError as err: + raise NotSupportedError("Python installation has no SSL support") from err + except (ssl.SSLError, IOError) as err: + raise InterfaceError( + errno=2055, values=(self.address, _strioerror(err)) + ) from err + except ssl.CertificateError as err: + raise InterfaceError(str(err)) from err + except NotImplementedError as err: + raise InterfaceError(str(err)) from err + + def send( + self, + payload: bytes, + packet_number: Optional[int] = None, + compressed_packet_number: Optional[int] = None, + ) -> None: + """Send `payload` to the MySQL server.""" + return self._netbroker.send( + self.sock, + self.address, + payload, + packet_number=packet_number, + compressed_packet_number=compressed_packet_number, + ) + + def recv(self) -> bytearray: + """Get packet from the MySQL server comm channel.""" + return self._netbroker.recv(self.sock, self.address) + + @abstractmethod + def open_connection(self) -> None: + """Open the socket.""" + + @property + @abstractmethod + def address(self) -> str: + """Get the location of the socket.""" + + +class MySQLUnixSocket(MySQLSocket): + """MySQL socket class using UNIX sockets. + + Opens a connection through the UNIX socket of the MySQL Server. + """ + + def __init__(self, unix_socket: str = "/tmp/mysql.sock") -> None: + super().__init__() + self.unix_socket: str = unix_socket + self._address: str = unix_socket + + @property + def address(self) -> str: + return self._address + + def open_connection(self) -> None: + try: + self.sock = socket.socket( + socket.AF_UNIX, socket.SOCK_STREAM # pylint: disable=no-member + ) + self.sock.settimeout(self._connection_timeout) + self.sock.connect(self.unix_socket) + except IOError as err: + raise InterfaceError( + errno=2002, values=(self.address, _strioerror(err)) + ) from err + except Exception as err: + raise InterfaceError(str(err)) from err + + def switch_to_ssl( + self, *args: Any, **kwargs: Any # pylint: disable=unused-argument + ) -> None: + """Switch the socket to use SSL.""" + warnings.warn( + "SSL is disabled when using unix socket connections", + Warning, + ) + + +class MySQLTCPSocket(MySQLSocket): + """MySQL socket class using TCP/IP. + + Opens a TCP/IP connection to the MySQL Server. + """ + + def __init__( + self, + host: str = "127.0.0.1", + port: int = 3306, + force_ipv6: bool = False, + ) -> None: + super().__init__() + self.server_host: str = host + self.server_port: int = port + self.force_ipv6: bool = force_ipv6 + self._family: int = 0 + self._address: str = f"{host}:{port}" + + @property + def address(self) -> str: + return self._address + + def open_connection(self) -> None: + """Open the TCP/IP connection to the MySQL server.""" + # pylint: disable=no-member + # Get address information + addrinfo: Union[ + Tuple[None, None, None, None, None], + Tuple[ + socket.AddressFamily, + socket.SocketKind, + int, + str, + Union[Tuple[str, int], Tuple[str, int, int, int]], + ], + ] = (None, None, None, None, None) + try: + addrinfos = socket.getaddrinfo( + self.server_host, + self.server_port, + 0, + socket.SOCK_STREAM, + socket.SOL_TCP, + ) + # If multiple results we favor IPv4, unless IPv6 was forced. + for info in addrinfos: + if self.force_ipv6 and info[0] == socket.AF_INET6: + addrinfo = info + break + if info[0] == socket.AF_INET: + addrinfo = info + break + if self.force_ipv6 and addrinfo[0] is None: + raise InterfaceError(f"No IPv6 address found for {self.server_host}") + if addrinfo[0] is None: + addrinfo = addrinfos[0] + except IOError as err: + raise InterfaceError( + errno=2003, values=(self.address, _strioerror(err)) + ) from err + + (self._family, socktype, proto, _, sockaddr) = addrinfo + + # Instanciate the socket and connect + try: + self.sock = socket.socket(self._family, socktype, proto) + self.sock.settimeout(self._connection_timeout) + self.sock.connect(sockaddr) + except IOError as err: + raise InterfaceError( + errno=2003, + values=( + self.server_host, + self.server_port, + _strioerror(err), + ), + ) from err + except Exception as err: + raise OperationalError(str(err)) from err diff --git a/mysql/connector/opentelemetry/__init__.py b/mysql/connector/opentelemetry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mysql/connector/opentelemetry/constants.py b/mysql/connector/opentelemetry/constants.py new file mode 100644 index 0000000..7e0090c --- /dev/null +++ b/mysql/connector/opentelemetry/constants.py @@ -0,0 +1,56 @@ +"""Constants used by the opentelemetry instrumentation implementation.""" +# mypy: disable-error-code="no-redef,assignment" + +# pylint: disable=unused-import +OTEL_ENABLED = True +try: + # try to load otel from the system + from opentelemetry import trace # check api + from opentelemetry.sdk.trace import TracerProvider # check sdk + from opentelemetry.semconv.trace import SpanAttributes # check semconv +except ImportError: + # falling back to the bundled installation + try: + from mysql.opentelemetry import trace + from mysql.opentelemetry.sdk.trace import TracerProvider + from mysql.opentelemetry.semconv.trace import SpanAttributes + except ImportError: + # bundled installation has missing dependencies + OTEL_ENABLED = False + + +OPTION_CNX_SPAN = "_span" +""" +Connection option name used to inject the connection span. +This connection option name must not be used, is reserved. +""" + +OPTION_CNX_TRACER = "_tracer" +""" +Connection option name used to inject the opentelemetry tracer. +This connection option name must not be used, is reserved. +""" + +CONNECTION_SPAN_NAME = "connection" +""" +Connection span name to be used by the instrumentor. +""" + +FIRST_SUPPORTED_VERSION = "8.1.0" +""" +First mysql-connector-python version to support opentelemetry instrumentation. +""" + +TRACEPARENT_HEADER_NAME = "traceparent" + +DB_SYSTEM = "mysql" +DEFAULT_THREAD_NAME = "main" +DEFAULT_THREAD_ID = 0 + +# Reference: https://github.com/open-telemetry/opentelemetry-specification/blob/main/ +# specification/trace/semantic_conventions/span-general.md +NET_SOCK_FAMILY = "net.sock.family" +NET_SOCK_PEER_ADDR = "net.sock.peer.addr" +NET_SOCK_PEER_PORT = "net.sock.peer.port" +NET_SOCK_HOST_ADDR = "net.sock.host.addr" +NET_SOCK_HOST_PORT = "net.sock.host.port" diff --git a/mysql/connector/opentelemetry/context_propagation.py b/mysql/connector/opentelemetry/context_propagation.py new file mode 100644 index 0000000..da8d60b --- /dev/null +++ b/mysql/connector/opentelemetry/context_propagation.py @@ -0,0 +1,92 @@ +"""Trace context propagation utilities.""" +# mypy: disable-error-code="no-redef" +# pylint: disable=invalid-name + +from typing import TYPE_CHECKING, Any, Callable, Union + +from .constants import OTEL_ENABLED, TRACEPARENT_HEADER_NAME + +if OTEL_ENABLED: + from .instrumentation import OTEL_SYSTEM_AVAILABLE + + if OTEL_SYSTEM_AVAILABLE: + # pylint: disable=import-error + # load otel from the system + from opentelemetry import trace + from opentelemetry.trace.span import format_span_id, format_trace_id + else: + # load otel from the bundled installation + from mysql.opentelemetry import trace + from mysql.opentelemetry.trace.span import format_span_id, format_trace_id + + +if TYPE_CHECKING: + from ..connection import MySQLConnection + from ..connection_cext import CMySQLConnection + + +def build_traceparent_header(span: Any) -> str: + """Build a traceparent header according to the provided span. + + The context information from the provided span is used to build the traceparent + header that will be propagated to the MySQL server. For particulars regarding + the header creation, refer to [1]. + + This method assumes version 0 of the W3C specification. + + Args: + span (opentelemetry.trace.span.Span): current span in trace. + + Returns: + traceparent_header (str): HTTP header field that identifies requests in a + tracing system. + + References: + [1]: https://www.w3.org/TR/trace-context/#traceparent-header + """ + ctx = span.get_span_context() + + version = "00" # version 0 of the W3C specification + trace_id = format_trace_id(ctx.trace_id) + span_id = format_span_id(ctx.span_id) + trace_flags = "00" # sampled flag is off + + return "-".join([version, trace_id, span_id, trace_flags]) + + +def with_context_propagation(method: Callable) -> Callable: + """Perform trace context propagation. + + The trace context is propagated via query attributes. The `traceparent` header + from W3C specification [1] is used, in this sense, the attribute name is + `traceparent` (is RESERVED, avoid using it), and its value is built as per + instructed in [1]. + + If opentelemetry API/SDK is unavailable or there is no recording span, + trace context propagation is skipped. + + References: + [1]: https://www.w3.org/TR/trace-context/#traceparent-header + """ + + def wrapper( + cnx: Union["MySQLConnection", "CMySQLConnection"], *args: Any, **kwargs: Any + ) -> Any: + """Context propagation decorator.""" + if not OTEL_ENABLED or not cnx.otel_context_propagation: + return method(cnx, *args, **kwargs) + + current_span = trace.get_current_span() + tp_header = None + if current_span.is_recording(): + tp_header = build_traceparent_header(current_span) + cnx.query_attrs_append(value=(TRACEPARENT_HEADER_NAME, tp_header)) + + try: + result = method(cnx, *args, **kwargs) + finally: + if tp_header is not None: + cnx.query_attrs_remove(name=TRACEPARENT_HEADER_NAME) + return result + + return wrapper diff --git a/mysql/connector/opentelemetry/instrumentation.py b/mysql/connector/opentelemetry/instrumentation.py new file mode 100644 index 0000000..cc8b376 --- /dev/null +++ b/mysql/connector/opentelemetry/instrumentation.py @@ -0,0 +1,514 @@ +"""MySQL instrumentation supporting mysql-connector.""" +# mypy: disable-error-code="no-redef" +# pylint: disable=protected-access,global-statement,invalid-name + +from __future__ import annotations + +import functools +import re + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Callable, Collection, Dict, Optional, Union + +# pylint: disable=cyclic-import +if TYPE_CHECKING: + # `TYPE_CHECKING` is always False at run time, hence circular import + # will not happen at run time (no error happens whatsoever). + # Since pylint is a static checker it happens that `TYPE_CHECKING` + # is True when analyzing the code which makes pylint believe there + # is a circular import issue when there isn't. + + from ..abstracts import MySQLConnectionAbstract + from ..connection import MySQLConnection + from ..cursor import MySQLCursor + from ..pooling import PooledMySQLConnection + + try: + from ..connection_cext import CMySQLConnection + from ..cursor_cext import CMySQLCursor + except ImportError: + # The cext is not available. + pass + +from ... import connector +from ..constants import CNX_POOL_ARGS, DEFAULT_CONFIGURATION +from ..logger import logger +from ..version import VERSION_TEXT + +try: + # pylint: disable=unused-import + # try to load otel from the system + from opentelemetry import trace # check api + from opentelemetry.sdk.trace import TracerProvider # check sdk + from opentelemetry.semconv.trace import SpanAttributes # check semconv + + OTEL_SYSTEM_AVAILABLE = True +except ImportError: + try: + # falling back to the bundled installation + from mysql.opentelemetry import trace + from mysql.opentelemetry.semconv.trace import SpanAttributes + + OTEL_SYSTEM_AVAILABLE = False + except ImportError as missing_dependencies_err: + raise connector.errors.ProgrammingError( + "Bundled installation has missing dependencies. " + "Please use `pip install mysql-connector-python[opentelemetry]`, " + "or for an editable install use `pip install -e '.[opentelemetry]'`, " + "to install the dependencies required by the bundled opentelemetry package." + ) from missing_dependencies_err + + +from .constants import ( + CONNECTION_SPAN_NAME, + DB_SYSTEM, + DEFAULT_THREAD_ID, + DEFAULT_THREAD_NAME, + FIRST_SUPPORTED_VERSION, + NET_SOCK_FAMILY, + NET_SOCK_HOST_ADDR, + NET_SOCK_HOST_PORT, + NET_SOCK_PEER_ADDR, + NET_SOCK_PEER_PORT, + OPTION_CNX_SPAN, + OPTION_CNX_TRACER, +) + +leading_comment_remover: re.Pattern = re.compile(r"^/\*.*?\*/") + + +def record_exception_event(span: trace.Span, exc: Optional[Exception]) -> None: + """Records an exeception event.""" + if not span or not span.is_recording() or not exc: + return + + span.set_status(trace.Status(trace.StatusCode.ERROR)) + span.record_exception(exc) + + +def end_span(span: trace.Span) -> None: + """Ends span.""" + if not span or not span.is_recording(): + return + + span.end() + + +def get_operation_name(operation: str) -> str: + """Parse query to extract operation name.""" + if operation and isinstance(operation, str): + # Strip leading comments so we get the operation name. + return leading_comment_remover.sub("", operation).split()[0] + return "" + + +def set_connection_span_attrs( + cnx: Optional["MySQLConnectionAbstract"], + cnx_span: trace.Span, + cnx_kwargs: Optional[Dict[str, Any]] = None, +) -> None: + """Defines connection span attributes. If `cnx` is None then we use `cnx_kwargs` + to get basic net information. Basic net attributes are defined such as: + + * DB_SYSTEM + * NET_TRANSPORT + * NET_SOCK_FAMILY + + Socket-level attributes [*] are also defined [**]. + + [*]: Socket-level attributes identify peer and host that are directly connected to + each other. Since instrumentations may have limited knowledge on network + information, instrumentations SHOULD populate such attributes to the best of + their knowledge when populate them at all. + + [**]: `CMySQLConnection` connections have no access to socket-level + details so socket-level attributes aren't included. `MySQLConnection` + connections, on the other hand, do include socket-level attributes. + + References: + [1]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/ + specification/trace/semantic_conventions/span-general.md + """ + # pylint: disable=broad-exception-caught + if not cnx_span or not cnx_span.is_recording(): + return + + if cnx_kwargs is None: + cnx_kwargs = {} + + is_tcp = not cnx._unix_socket if cnx else "unix_socket" not in cnx_kwargs + + attrs: Dict[str, Any] = { + SpanAttributes.DB_SYSTEM: DB_SYSTEM, + SpanAttributes.NET_TRANSPORT: "ip_tcp" if is_tcp else "inproc", + NET_SOCK_FAMILY: "inet" if is_tcp else "unix", + } + + # Only socket and tcp connections are supported. + if is_tcp: + attrs[SpanAttributes.NET_PEER_NAME] = ( + cnx._host if cnx else cnx_kwargs.get("host", DEFAULT_CONFIGURATION["host"]) + ) + attrs[SpanAttributes.NET_PEER_PORT] = ( + cnx._port if cnx else cnx_kwargs.get("port", DEFAULT_CONFIGURATION["port"]) + ) + + if hasattr(cnx, "_socket") and cnx._socket: + try: + ( + attrs[NET_SOCK_PEER_ADDR], + sock_peer_port, + ) = cnx._socket.sock.getpeername() + + ( + attrs[NET_SOCK_HOST_ADDR], + attrs[NET_SOCK_HOST_PORT], + ) = cnx._socket.sock.getsockname() + except Exception as sock_err: + logger.warning("Connection socket is down %s", sock_err) + else: + if attrs[SpanAttributes.NET_PEER_PORT] != sock_peer_port: + # NET_SOCK_PEER_PORT is recommended if different than net.peer.port + # and if net.sock.peer.addr is set. + attrs[NET_SOCK_PEER_PORT] = sock_peer_port + else: + # For Unix domain socket, net.sock.peer.addr attribute represents + # destination name and net.peer.name SHOULD NOT be set. + attrs[NET_SOCK_PEER_ADDR] = ( + cnx._unix_socket if cnx else cnx_kwargs.get("unix_socket") + ) + + if hasattr(cnx, "_socket") and cnx._socket: + try: + attrs[NET_SOCK_HOST_ADDR] = cnx._socket.sock.getsockname() + except Exception as sock_err: + logger.warning("Connection socket is down %s", sock_err) + + cnx_span.set_attributes(attrs) + + +def instrument_execution( + query_method: Callable, + tracer: trace.Tracer, + connection_span_link: trace.Link, + wrapped: Union["MySQLCursor", "CMySQLCursor"], + *args: Any, + **kwargs: Any, +) -> Callable: + """Instruments the execution of `query_method`. + + A query span with a link to the corresponding connection span is generated. + """ + connection: Union["MySQLConnection", "CMySQLConnection"] = ( + getattr(wrapped, "_connection") + if hasattr(wrapped, "_connection") + else getattr(wrapped, "_cnx") + ) + + # SpanAttributes.DB_NAME: connection.database or ""; introduces performance + # degradation, at this time the database attribute is something nice to have but + # not a requirement. + query_span_attributes: Dict = { + SpanAttributes.DB_SYSTEM: DB_SYSTEM, + SpanAttributes.DB_USER: connection._user, + SpanAttributes.THREAD_ID: DEFAULT_THREAD_ID, + SpanAttributes.THREAD_NAME: DEFAULT_THREAD_NAME, + "cursor_type": wrapped.__class__.__name__, + } + with tracer.start_as_current_span( + name=get_operation_name(args[0]) or "SQL statement", + kind=trace.SpanKind.CLIENT, + links=[connection_span_link], + attributes=query_span_attributes, + ): + return query_method(*args, **kwargs) + + +class BaseMySQLTracer(ABC): + """Base class that provides basic object wrapper functionality.""" + + @abstractmethod + def __init__(self) -> None: + """Must be implemented by subclasses.""" + + def __getattr__(self, attr: str) -> Any: + """Gets an attribute. + + Attributes defined in the wrapper object have higher precedence + than those wrapped object equivalent. Attributes not found in + the wrapper are then searched in the wrapped object. + """ + if attr in self.__dict__: + # this object has it + return getattr(self, attr) + # proxy to the wrapped object + return getattr(self._wrapped, attr) + + def __setattr__(self, name: str, value: Any) -> None: + if "_wrapped" not in self.__dict__: + self.__dict__["_wrapped"] = value + return + + if name in self.__dict__: + # this object has it + super().__setattr__(name, value) + return + # proxy to the wrapped object + self._wrapped.__setattr__(name, value) + + def __enter__(self) -> Any: + """Magic method.""" + self._wrapped.__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Magic method.""" + self._wrapped.__exit__(*args, **kwargs) + + def get_wrapped_class(self) -> str: + """Gets the wrapped class name.""" + return self._wrapped.__class__.__name__ + + +class TracedMySQLCursor(BaseMySQLTracer): + """Wrapper class for a `MySQLCursor` or `CMySQLCursor` object.""" + + def __init__( + self, + wrapped: Union["MySQLCursor", "CMySQLCursor"], + tracer: trace.Tracer, + connection_span: trace.Span, + ): + """Constructor.""" + self._wrapped: Union["MySQLCursor", "CMySQLCursor"] = wrapped + self._tracer: trace.Tracer = tracer + self._connection_span_link: trace.Link = trace.Link( + connection_span.get_span_context() + ) + + def execute(self, *args: Any, **kwargs: Any) -> Any: + """Instruments execute method.""" + return instrument_execution( + self._wrapped.execute, + self._tracer, + self._connection_span_link, + self._wrapped, + *args, + **kwargs, + ) + + def executemany(self, *args: Any, **kwargs: Any) -> Any: + """Instruments executemany method.""" + return instrument_execution( + self._wrapped.executemany, + self._tracer, + self._connection_span_link, + self._wrapped, + *args, + **kwargs, + ) + + def callproc(self, *args: Any, **kwargs: Any) -> Any: + """Instruments callproc method.""" + return instrument_execution( + self._wrapped.callproc, + self._tracer, + self._connection_span_link, + self._wrapped, + *args, + **kwargs, + ) + + +class TracedMySQLConnection(BaseMySQLTracer): + """Wrapper class for a `MySQLConnection` or `CMySQLConnection` object.""" + + def __init__(self, wrapped: Union["MySQLConnection", "CMySQLConnection"]) -> None: + """Constructor.""" + self._wrapped: Union["MySQLConnection", "CMySQLConnection"] = wrapped + + # call `sql_mode` so its value is cached internally and querying it does not + # interfere when recording query span events later. + _ = self._wrapped.sql_mode + + def cursor(self, *args: Any, **kwargs: Any) -> TracedMySQLCursor: + """Wraps the cursor object.""" + return TracedMySQLCursor( + wrapped=self._wrapped.cursor(*args, **kwargs), + tracer=self._tracer, + connection_span=self._span, + ) + + +def instrument_connect( + connect: Callable[ + ..., Union["MySQLConnection", "CMySQLConnection", "PooledMySQLConnection"] + ], + tracer_provider: Optional[trace.TracerProvider] = None, +) -> Callable[ + ..., Union["MySQLConnection", "CMySQLConnection", "PooledMySQLConnection"] +]: + """Retrurn the instrumented version of `connect`.""" + + # let's preserve `connect` identity. + @functools.wraps(connect) + def wrapper( + *args: Any, **kwargs: Any + ) -> Union["MySQLConnection", "CMySQLConnection", "PooledMySQLConnection"]: + """Wraps the connection object returned by the method `connect`. + + Instrumentation for PooledConnections is not supported. + """ + if any(key in kwargs for key in CNX_POOL_ARGS): + logger.warning("Instrumentation for pooled connections not supported") + return connect(*args, **kwargs) + + tracer = trace.get_tracer( + instrumenting_module_name="MySQL Connector/Python", + instrumenting_library_version=VERSION_TEXT, + tracer_provider=tracer_provider, + ) + + # The connection span is passed in as an argument so the connection object can + # keep a pointer to it. + kwargs[OPTION_CNX_SPAN] = tracer.start_span( + name=CONNECTION_SPAN_NAME, kind=trace.SpanKind.CLIENT + ) + kwargs[OPTION_CNX_TRACER] = tracer + + # Add basic net information. + set_connection_span_attrs(None, kwargs[OPTION_CNX_SPAN], kwargs) + + # Connection may fail at this point, in case it does, basic net info is already + # included so the user can check the net configuration she/he provided. + cnx = connect(*args, **kwargs) + + # connection went ok, let's refine the net information. + set_connection_span_attrs(cnx, cnx._span, kwargs) # type: ignore[arg-type] + + return TracedMySQLConnection( + wrapped=cnx, # type: ignore[return-value, arg-type] + ) + + return wrapper + + +class MySQLInstrumentor: + """MySQL instrumentation supporting mysql-connector-python.""" + + _instance: Optional[MySQLInstrumentor] = None + + def __new__(cls, *args: Any, **kwargs: Any) -> MySQLInstrumentor: + """Singlenton. + + Restricts the instantiation to a singular instance. + """ + if cls._instance is None: + # create instance + cls._instance = object.__new__(cls, *args, **kwargs) + # keep a pointer to the uninstrumented connect method + setattr(cls._instance, "_original_connect", connector.connect) + return cls._instance + + def instrumentation_dependencies(self) -> Collection[str]: + """Return a list of python packages with versions + that the will be instrumented (e.g., versions >= 8.1.0).""" + return [f"mysql-connector-python >= {FIRST_SUPPORTED_VERSION}"] + + def instrument(self, **kwargs: Any) -> None: + """Instrument the library. + + Args: + trace_module: reference to the 'trace' module from opentelemetry. + tracer_provider (optional): TracerProvider instance. + + NOTE: Instrumentation for pooled connections not supported. + """ + if connector.connect != getattr(self, "_original_connect"): + logger.warning("MySQL Connector/Python module already instrumented.") + return + connector.connect = instrument_connect( + connect=getattr(self, "_original_connect"), + tracer_provider=kwargs.get("tracer_provider"), + ) + + def instrument_connection( + self, + connection: Union["MySQLConnection", "CMySQLConnection"], + tracer_provider: Optional[trace.TracerProvider] = None, + ) -> Union["MySQLConnection", "CMySQLConnection"]: + """Enable instrumentation in a MySQL connection. + + Args: + connection: uninstrumented connection instance. + trace_module: reference to the 'trace' module from opentelemetry. + tracer_provider (optional): TracerProvider instance. + + Returns: + connection: instrumented connection instace. + + NOTE: Instrumentation for pooled connections not supported. + """ + if isinstance(connection, TracedMySQLConnection): + logger.warning("Connection already instrumented.") + return connection + + if not hasattr(connection, "_span") or not hasattr(connection, "_tracer"): + logger.warning( + "Instrumentation for class %s not supported.", + connection.__class__.__name__, + ) + return connection + + tracer = trace.get_tracer( + instrumenting_module_name="MySQL Connector/Python", + instrumenting_library_version=VERSION_TEXT, + tracer_provider=tracer_provider, + ) + connection._span = tracer.start_span( + name=CONNECTION_SPAN_NAME, kind=trace.SpanKind.CLIENT + ) + connection._tracer = tracer + + set_connection_span_attrs(connection, connection._span) + + return TracedMySQLConnection(wrapped=connection) # type: ignore[return-value] + + def uninstrument(self, **kwargs: Any) -> None: + """Uninstrument the library.""" + # pylint: disable=unused-argument + if connector.connect == getattr(self, "_original_connect"): + logger.warning("MySQL Connector/Python module already uninstrumented.") + return + connector.connect = getattr(self, "_original_connect") + + def uninstrument_connection( + self, connection: Union["MySQLConnection", "CMySQLConnection"] + ) -> Union["MySQLConnection", "CMySQLConnection"]: + """Disable instrumentation in a MySQL connection. + + Args: + connection: instrumented connection instance. + + Returns: + connection: uninstrumented connection instace. + + NOTE: Instrumentation for pooled connections not supported. + """ + if not hasattr(connection, "_span"): + logger.warning( + "Uninstrumentation for class %s not supported.", + connection.__class__.__name__, + ) + return connection + + if not isinstance(connection, TracedMySQLConnection): + logger.warning("Connection already uninstrumented.") + return connection + + # stop connection span recording + if connection._span and connection._span.is_recording(): + connection._span.end() + connection._span = None + + return connection._wrapped diff --git a/mysql/connector/optionfiles.py b/mysql/connector/optionfiles.py new file mode 100644 index 0000000..13f7132 --- /dev/null +++ b/mysql/connector/optionfiles.py @@ -0,0 +1,357 @@ +# Copyright (c) 2014, 2022, Oracle and/or its affiliates. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2.0, as +# published by the Free Software Foundation. +# +# This program is also distributed with certain software (including +# but not limited to OpenSSL) that is licensed under separate terms, +# as designated in a particular file or component or in included license +# documentation. The authors of MySQL hereby grant you an +# additional permission to link the program and your derivative works +# with the separately licensed software that they have included with +# MySQL. +# +# Without limiting anything contained in the foregoing, this file, +# which is part of MySQL Connector/Python, is also subject to the +# Universal FOSS Exception, version 1.0, a copy of which can be found at +# http://oss.oracle.com/licenses/universal-foss-exception. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License, version 2.0, for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# mypy: disable-error-code="attr-defined" + +"""Implements parser to parse MySQL option files.""" + +import codecs +import io +import os +import re + +from configparser import ConfigParser as SafeConfigParser, MissingSectionHeaderError +from typing import Any, Dict, List, Optional, Tuple, Union + +from .constants import CNX_POOL_ARGS, DEFAULT_CONFIGURATION + +DEFAULT_EXTENSIONS: Dict[str, Tuple[str, ...]] = { + "nt": ("ini", "cnf"), + "posix": ("cnf",), +} + + +def read_option_files(**config: Union[str, List[str]]) -> Dict[str, Any]: + """ + Read option files for connection parameters. + + Checks if connection arguments contain option file arguments, and then + reads option files accordingly. + """ + if "option_files" in config: + try: + if isinstance(config["option_groups"], str): + config["option_groups"] = [config["option_groups"]] + groups = config["option_groups"] + del config["option_groups"] + except KeyError: + groups = ["client", "connector_python"] + + if isinstance(config["option_files"], str): + config["option_files"] = [config["option_files"]] + option_parser = MySQLOptionsParser( + list(config["option_files"]), keep_dashes=False + ) + del config["option_files"] + + config_from_file = option_parser.get_groups_as_dict_with_priority(*groups) + config_options: Dict[str, Tuple[str, int]] = {} + for group in groups: + try: + for option, value in config_from_file[group].items(): + try: + if option == "socket": + option = "unix_socket" + + if option not in CNX_POOL_ARGS and option != "failover": + _ = DEFAULT_CONFIGURATION[option] + + if ( + option not in config_options + or config_options[option][1] <= value[1] + ): + config_options[option] = value + except KeyError: + if group == "connector_python": + raise AttributeError( + f"Unsupported argument '{option}'" + ) from None + except KeyError: + continue + + not_evaluate = ("password", "passwd") + for option, value in config_options.items(): + if option not in config: + try: + if option in not_evaluate: + config[option] = value[0] + else: + config[option] = eval(value[0]) # pylint: disable=eval-used + except (NameError, SyntaxError): + config[option] = value[0] + + return config + + +class MySQLOptionsParser(SafeConfigParser): + """This class implements methods to parse MySQL option files""" + + def __init__( + self, files: Optional[Union[List[str], str]] = None, keep_dashes: bool = True + ) -> None: + """Initialize + + If defaults is True, default option files are read first + + Raises ValueError if defaults is set to True but defaults files + cannot be found. + """ + + # Regular expression to allow options with no value(For Python v2.6) + self.optcre: re.Pattern = re.compile( + r"(?P