Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/publish-python.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: python/dist
20 changes: 20 additions & 0 deletions .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
@@ -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
110 changes: 110 additions & 0 deletions python/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
12 changes: 12 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 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
```
Empty file.
Empty file.
Empty file.
37 changes: 37 additions & 0 deletions python/aep_types/aep_type/aep/type/decimal_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions python/aep_types/aep_type/aep/type/decimal_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high-level what is the strategy for these generated files? it seems like we'll check it in, but is there a how-to on how to generate these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a regenerate.sh script that basically just runs buf generate.

"""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}.'
)
49 changes: 49 additions & 0 deletions python/aep_types/decimal/__init__.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this can be a module (decimal.py)? simple enough that it doesn't need to be a package.

Original file line number Diff line number Diff line change
@@ -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.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))
37 changes: 37 additions & 0 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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="[email protected]" },
]
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" }
52 changes: 52 additions & 0 deletions python/tests/test_decimal.py
Original file line number Diff line number Diff line change
@@ -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.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