diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml new file mode 100644 index 0000000..0563ed9 --- /dev/null +++ b/.github/workflows/publish-python.yml @@ -0,0 +1,28 @@ +name: Publish Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + working-directory: ./python + - name: Publish package + uses: pypa/gh-action-pypi-publish@v1.4.2 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + packages_dir: python/dist \ No newline at end of file diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml new file mode 100644 index 0000000..d27cc13 --- /dev/null +++ b/.github/workflows/python-test.yml @@ -0,0 +1,20 @@ +name: Python Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ./python[dev] + - name: Test with pytest + run: | + PYTHONPATH=python pytest python/tests \ No newline at end of file diff --git a/buf.gen.yaml b/buf.gen.yaml index 9b90cbb..4527b19 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -1,3 +1,8 @@ -version: "v2" +version: v2 managed: enabled: true +plugins: + - remote: buf.build/protocolbuffers/python:v26.1 + out: python + - remote: buf.build/grpc/python:v1.64.1 + out: python \ No newline at end of file diff --git a/python/.gitignore b/python/.gitignore new file mode 100644 index 0000000..ba9f831 --- /dev/null +++ b/python/.gitignore @@ -0,0 +1,110 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyderworkspace + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ \ No newline at end of file diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..6ac43ad --- /dev/null +++ b/python/README.md @@ -0,0 +1,20 @@ +# Python Libraries + +This directory contains Python libraries for the AEP types. + +## Development + +To install dependencies and run tests, use the following commands: + +```bash +pip install -e .[dev] +pytest +``` + +## Regenerating Protobuf Files + +When the source `.proto` files in the repository are updated, the generated Python files must be regenerated. To do this, run the following script from the root of the repository: + +```bash +./python/regenerate.sh +``` diff --git a/python/aep_types/aep/type/decimal_pb2.py b/python/aep_types/aep/type/decimal_pb2.py new file mode 100644 index 0000000..d5cf13e --- /dev/null +++ b/python/aep_types/aep/type/decimal_pb2.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: aep/type/decimal.proto +# Protobuf Python Version: 5.26.1 +"""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\x16\x61\x65p/type/decimal.proto\x12\x08\x61\x65p.type\"G\n\x07\x44\x65\x63imal\x12 \n\x0bsignificand\x18\x01 \x01(\x03R\x0bsignificand\x12\x1a\n\x08\x65xponent\x18\x02 \x01(\x05R\x08\x65xponentB`\n\x0c\x63om.aep.typeB\x0c\x44\x65\x63imalProtoP\x01\xf8\x01\x01\xa2\x02\x03\x41TX\xaa\x02\x08\x41\x65p.Type\xca\x02\x08\x41\x65p\\Type\xe2\x02\x14\x41\x65p\\Type\\GPBMetadata\xea\x02\tAep::Typeb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'aep.type.decimal_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\014com.aep.typeB\014DecimalProtoP\001\370\001\001\242\002\003ATX\252\002\010Aep.Type\312\002\010Aep\\Type\342\002\024Aep\\Type\\GPBMetadata\352\002\tAep::Type' + _globals['_DECIMAL']._serialized_start=36 + _globals['_DECIMAL']._serialized_end=107 +# @@protoc_insertion_point(module_scope) diff --git a/python/aep_types/aep/type/decimal_pb2_grpc.py b/python/aep_types/aep/type/decimal_pb2_grpc.py new file mode 100644 index 0000000..8a93939 --- /dev/null +++ b/python/aep_types/aep/type/decimal_pb2_grpc.py @@ -0,0 +1,3 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc diff --git a/python/aep_types/aep/type/interval_pb2.py b/python/aep_types/aep/type/interval_pb2.py new file mode 100644 index 0000000..22773ea --- /dev/null +++ b/python/aep_types/aep/type/interval_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: aep/type/interval.proto +# Protobuf Python Version: 5.26.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from buf.validate import validate_pb2 as buf_dot_validate_dot_validate__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x61\x65p/type/interval.proto\x12\x08\x61\x65p.type\x1a\x1b\x62uf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xcf\x01\n\x08Interval\x12\x39\n\nstart_time\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tstartTime\x12\x35\n\x08\x65nd_time\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x07\x65ndTime:Q\xbaHN\x1aL\n\x12Interval.non-empty\x12\x14start must be <= end\x1a this.start_time <= this.end_timeBa\n\x0c\x63om.aep.typeB\rIntervalProtoP\x01\xf8\x01\x01\xa2\x02\x03\x41TX\xaa\x02\x08\x41\x65p.Type\xca\x02\x08\x41\x65p\\Type\xe2\x02\x14\x41\x65p\\Type\\GPBMetadata\xea\x02\tAep::Typeb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'aep.type.interval_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\014com.aep.typeB\rIntervalProtoP\001\370\001\001\242\002\003ATX\252\002\010Aep.Type\312\002\010Aep\\Type\342\002\024Aep\\Type\\GPBMetadata\352\002\tAep::Type' + _globals['_INTERVAL']._loaded_options = None + _globals['_INTERVAL']._serialized_options = b'\272HN\032L\n\022Interval.non-empty\022\024start must be <= end\032 this.start_time <= this.end_time' + _globals['_INTERVAL']._serialized_start=100 + _globals['_INTERVAL']._serialized_end=307 +# @@protoc_insertion_point(module_scope) diff --git a/python/aep_types/aep/type/interval_pb2_grpc.py b/python/aep_types/aep/type/interval_pb2_grpc.py new file mode 100644 index 0000000..8a93939 --- /dev/null +++ b/python/aep_types/aep/type/interval_pb2_grpc.py @@ -0,0 +1,3 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc diff --git a/python/aep_types/aep/type/money_pb2.py b/python/aep_types/aep/type/money_pb2.py new file mode 100644 index 0000000..a7b9bd8 --- /dev/null +++ b/python/aep_types/aep/type/money_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: aep/type/money.proto +# Protobuf Python Version: 5.26.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from aep.type import decimal_pb2 as aep_dot_type_dot_decimal__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14\x61\x65p/type/money.proto\x12\x08\x61\x65p.type\x1a\x16\x61\x65p/type/decimal.proto\"[\n\x05Money\x12#\n\rcurrency_code\x18\x01 \x01(\tR\x0c\x63urrencyCode\x12-\n\x08quantity\x18\x02 \x01(\x0b\x32\x11.aep.type.DecimalR\x08quantityB^\n\x0c\x63om.aep.typeB\nMoneyProtoP\x01\xf8\x01\x01\xa2\x02\x03\x41TX\xaa\x02\x08\x41\x65p.Type\xca\x02\x08\x41\x65p\\Type\xe2\x02\x14\x41\x65p\\Type\\GPBMetadata\xea\x02\tAep::Typeb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'aep.type.money_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\014com.aep.typeB\nMoneyProtoP\001\370\001\001\242\002\003ATX\252\002\010Aep.Type\312\002\010Aep\\Type\342\002\024Aep\\Type\\GPBMetadata\352\002\tAep::Type' + _globals['_MONEY']._serialized_start=58 + _globals['_MONEY']._serialized_end=149 +# @@protoc_insertion_point(module_scope) diff --git a/python/aep_types/aep/type/money_pb2_grpc.py b/python/aep_types/aep/type/money_pb2_grpc.py new file mode 100644 index 0000000..8a93939 --- /dev/null +++ b/python/aep_types/aep/type/money_pb2_grpc.py @@ -0,0 +1,3 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc diff --git a/python/aep_types/aep/type/phone_number_pb2.py b/python/aep_types/aep/type/phone_number_pb2.py new file mode 100644 index 0000000..71c00c5 --- /dev/null +++ b/python/aep_types/aep/type/phone_number_pb2.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: aep/type/phone_number.proto +# Protobuf Python Version: 5.26.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from buf.validate import validate_pb2 as buf_dot_validate_dot_validate__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1b\x61\x65p/type/phone_number.proto\x12\x08\x61\x65p.type\x1a\x1b\x62uf/validate/validate.proto\"\xaa\x02\n\x0bPhoneNumber\x12\x35\n\x06number\x18\x01 \x01(\tB\x1b\xbaH\x18r\x16\x32\x14^\\+[0-9]{1,3}[0-9]+$H\x00R\x06number\x12@\n\nshort_code\x18\x02 \x01(\x0b\x32\x1f.aep.type.PhoneNumber.ShortCodeH\x00R\tshortCode\x12%\n\textension\x18\x03 \x01(\tB\x07\xbaH\x04r\x02\x18(R\textension\x1as\n\tShortCode\x12=\n\x0bregion_code\x18\x01 \x01(\tB\x1c\xbaH\x19r\x17\x32\x15^([A-Z]{2}|[0-9]{3})$R\nregionCode\x12\'\n\x06number\x18\x02 \x01(\tB\x0f\xbaH\x0cr\n2\x08^[0-9]+$R\x06numberB\x06\n\x04kindBd\n\x0c\x63om.aep.typeB\x10PhoneNumberProtoP\x01\xf8\x01\x01\xa2\x02\x03\x41TX\xaa\x02\x08\x41\x65p.Type\xca\x02\x08\x41\x65p\\Type\xe2\x02\x14\x41\x65p\\Type\\GPBMetadata\xea\x02\tAep::Typeb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'aep.type.phone_number_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\014com.aep.typeB\020PhoneNumberProtoP\001\370\001\001\242\002\003ATX\252\002\010Aep.Type\312\002\010Aep\\Type\342\002\024Aep\\Type\\GPBMetadata\352\002\tAep::Type' + _globals['_PHONENUMBER_SHORTCODE'].fields_by_name['region_code']._loaded_options = None + _globals['_PHONENUMBER_SHORTCODE'].fields_by_name['region_code']._serialized_options = b'\272H\031r\0272\025^([A-Z]{2}|[0-9]{3})$' + _globals['_PHONENUMBER_SHORTCODE'].fields_by_name['number']._loaded_options = None + _globals['_PHONENUMBER_SHORTCODE'].fields_by_name['number']._serialized_options = b'\272H\014r\n2\010^[0-9]+$' + _globals['_PHONENUMBER'].fields_by_name['number']._loaded_options = None + _globals['_PHONENUMBER'].fields_by_name['number']._serialized_options = b'\272H\030r\0262\024^\\+[0-9]{1,3}[0-9]+$' + _globals['_PHONENUMBER'].fields_by_name['extension']._loaded_options = None + _globals['_PHONENUMBER'].fields_by_name['extension']._serialized_options = b'\272H\004r\002\030(' + _globals['_PHONENUMBER']._serialized_start=71 + _globals['_PHONENUMBER']._serialized_end=369 + _globals['_PHONENUMBER_SHORTCODE']._serialized_start=246 + _globals['_PHONENUMBER_SHORTCODE']._serialized_end=361 +# @@protoc_insertion_point(module_scope) diff --git a/python/aep_types/aep/type/phone_number_pb2_grpc.py b/python/aep_types/aep/type/phone_number_pb2_grpc.py new file mode 100644 index 0000000..8a93939 --- /dev/null +++ b/python/aep_types/aep/type/phone_number_pb2_grpc.py @@ -0,0 +1,3 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc diff --git a/python/aep_types/aep_type/__init__.py b/python/aep_types/aep_type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/aep_types/aep_type/aep/__init__.py b/python/aep_types/aep_type/aep/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/aep_types/aep_type/aep/type/__init__.py b/python/aep_types/aep_type/aep/type/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/aep_types/aep_type/aep/type/decimal_pb2.py b/python/aep_types/aep_type/aep/type/decimal_pb2.py new file mode 100644 index 0000000..1cadb72 --- /dev/null +++ b/python/aep_types/aep_type/aep/type/decimal_pb2.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: aep-type/aep/type/decimal.proto +# Protobuf Python Version: 6.31.1 +"""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, + 6, + 31, + 1, + '', + 'aep-type/aep/type/decimal.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x61\x65p-type/aep/type/decimal.proto\x12\x08\x61\x65p.type\"0\n\x07\x44\x65\x63imal\x12\x13\n\x0bsignificand\x18\x01 \x01(\x03\x12\x10\n\x08\x65xponent\x18\x02 \x01(\x05\x42\'\n\x0c\x64\x65v.aep.typeB\x0c\x44\x65\x63imalProtoP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45Pb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'aep_type.aep.type.decimal_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\014dev.aep.typeB\014DecimalProtoP\001\370\001\001\242\002\003AEP' + _globals['_DECIMAL']._serialized_start=45 + _globals['_DECIMAL']._serialized_end=93 +# @@protoc_insertion_point(module_scope) diff --git a/python/aep_types/aep_type/aep/type/decimal_pb2_grpc.py b/python/aep_types/aep_type/aep/type/decimal_pb2_grpc.py new file mode 100644 index 0000000..80002b9 --- /dev/null +++ b/python/aep_types/aep_type/aep/type/decimal_pb2_grpc.py @@ -0,0 +1,24 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + + +GRPC_GENERATED_VERSION = '1.75.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in aep_type/aep/type/decimal_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) diff --git a/python/aep_types/decimal/__init__.py b/python/aep_types/decimal/__init__.py new file mode 100644 index 0000000..4bd38bb --- /dev/null +++ b/python/aep_types/decimal/__init__.py @@ -0,0 +1,49 @@ +import yaml +from importlib import resources +from decimal import Decimal +from typing import Dict, Any +import jsonschema + +from aep_types.aep.type import decimal_pb2 + +# Load the schema +schema_file = resources.files('aep_types').joinpath('schemas', 'type', 'decimal.yaml') +with schema_file.open('r') as f: + DECIMAL_SCHEMA = yaml.safe_load(f) + +def to_proto(d: Decimal) -> decimal_pb2.Decimal: + """Converts a Python `decimal.Decimal` to a protobuf `Decimal` message.""" + sign, digits, exponent = d.as_tuple() + + significand = int("".join(map(str, digits))) + if sign: + significand = -significand + + return decimal_pb2.Decimal(significand=significand, exponent=exponent) + +def from_proto(p: decimal_pb2.Decimal) -> Decimal: + """Converts a protobuf `Decimal` message to a Python `decimal.Decimal`.""" + sign = 1 if p.significand < 0 else 0 + digits = tuple(map(int, str(abs(p.significand)))) + return Decimal((sign, digits, p.exponent)) + +def to_json(d: Decimal) -> Dict[str, Any]: + """Converts a Python `decimal.Decimal` to a JSON-serializable dictionary.""" + sign, digits, exponent = d.as_tuple() + + significand = int("".join(map(str, digits))) + if sign: + significand = -significand + + return {"significand": significand, "exponent": exponent} + +def from_json(j: Dict[str, Any]) -> Decimal: + """Converts a JSON-serializable dictionary to a Python `decimal.Decimal`.""" + jsonschema.validate(j, DECIMAL_SCHEMA) + + significand = j["significand"] + exponent = j["exponent"] + + sign = 1 if significand < 0 else 0 + digits = tuple(map(int, str(abs(significand)))) + return Decimal((sign, digits, exponent)) \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..3cb8e7a --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "aep-types" +version = "0.0.1" +authors = [ + { name="Richard Frankel", email="richard@frankel.tv" }, +] +description = "AEP Types" +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +dependencies = [ + "jsonschema", + "PyYAML", +] + +[project.urls] +"Homepage" = "https://aep.dev" +"Bug Tracker" = "https://github.com/aep-dev/aep-components/issues" + +[project.optional-dependencies] +dev = [ + "pytest", + "grpcio-tools", + "protobuf", +] + +[tool.hatch.build] +packages = ["aep_types"] +force-include = { "../json_schema/type/decimal.yaml" = "aep_types/schemas/type/decimal.yaml" } \ No newline at end of file diff --git a/python/regenerate.sh b/python/regenerate.sh new file mode 100755 index 0000000..cb01e9f --- /dev/null +++ b/python/regenerate.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -euo pipefail + +# This script regenerates the Python protobuf-generated files using buf. +# It is designed to be run from the root of the repository. + +# --- Main Logic --- +echo "Regenerating Python protobuf files using 'buf generate'..." + +# The `buf generate` command reads the `buf.gen.yaml` file for configuration, +# which specifies the plugins to use and the output directory. +# We target the `proto/aep-type` module to ensure we only generate code +# for the types, not the APIs or other modules. +buf generate proto/aep-type + +echo "Successfully regenerated Python files." \ No newline at end of file diff --git a/python/tests/test_decimal.py b/python/tests/test_decimal.py new file mode 100644 index 0000000..c0d39c4 --- /dev/null +++ b/python/tests/test_decimal.py @@ -0,0 +1,52 @@ +import pytest +import jsonschema +from decimal import Decimal +from aep_types.decimal import to_proto, from_proto, to_json, from_json +from aep_types.aep.type import decimal_pb2 + +def test_to_proto(): + d = Decimal("12.3") + p = to_proto(d) + assert isinstance(p, decimal_pb2.Decimal) + assert p.significand == 123 + assert p.exponent == -1 + +def test_from_proto(): + p = decimal_pb2.Decimal(significand=-456, exponent=2) + d = from_proto(p) + assert d == Decimal("-45600") + +def test_to_json(): + d = Decimal("0.789") + j = to_json(d) + assert j == {"significand": 789, "exponent": -3} + +def test_from_json(): + j = {"significand": -101, "exponent": 4} + d = from_json(j) + assert d == Decimal("-1010000") + +def test_round_trip_proto(): + d_original = Decimal("-123.456e7") + p = to_proto(d_original) + d_new = from_proto(p) + assert d_original == d_new + +def test_round_trip_json(): + d_original = Decimal("789.012e-3") + j = to_json(d_original) + d_new = from_json(j) + assert d_original == d_new + +def test_spec_example(): + # 33.5 million === {significand: 335, exponent: 5} + d = from_json({"significand": 335, "exponent": 5}) + assert d == Decimal("33500000") + +def test_from_json_invalid(): + with pytest.raises(jsonschema.ValidationError): + from_json({"significand": 123}) # Missing exponent + with pytest.raises(jsonschema.ValidationError): + from_json({"exponent": -1}) # Missing significand + with pytest.raises(jsonschema.ValidationError): + from_json({"significand": "123", "exponent": -1}) # Wrong type \ No newline at end of file