Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Optional SQLAlchemy Integrations #24

Merged
merged 7 commits into from
May 24, 2024
Merged

Add Optional SQLAlchemy Integrations #24

merged 7 commits into from
May 24, 2024

Conversation

GrammAcc
Copy link
Owner

This PR adds a new linearmoney.ext sub-package that contains the sqlalchemy module for integrations with the SQLAlchemy ORM.

There are two column types added:

  • VectorMoney
  • AtomicMoney

The difference is that VectorMoney stores the MoneyVector as a serialized string to preserve all information including the currency space, and AtomicMoney stores the MoneyVector as an integer representing its atomic value in the currency specified on column declaration.

Both do implicit serialization of the vectors to a primitive type, and both are non-destructive, but AtomicMoney will error if provided a vector in a currency space with more than one dimension or that doesn't match the currency it was declared with. This prevents accidental data corruption when writing to the db.

See the docstrings for these two classes for more details.

This PR also adds some minor changes in the pyproject.toml file. While developing on this feature, I found that having a way to quickly run the test suite on only one python version and specify arguments to the test command were useful, so I've added {args} to the test commands and also a new quicktest command in the default environment that runs the test:suite command with only the latest interpreter version instead of the full matrix. I also added environments py310 and py311, which simply define a different interpreter version from the default environment that uses 3.12. This is useful for exploratory testing/debugging since we can quickly start a shell using a different interpreter version with hatch without having to make the binary available in the current shell under a different name such as python3.10.

Close #22

I have added an `ext` namespace package to the library.

This package currently contains a single module `sqlalchemy.py` that
provides sqlalchemy column types for storing MoneyVectors in a sql
database.

The `ext` namespace package is distributed with the regular package, but
it is not imported in the main `__init__.py` file, so it doesn't cause
any import issues if the user hasn't installed the optional
`linearmoney[sqlalchemy]` dependencies.

I have added two column types `VectorMoney` and `AtomicMoney`.
VectorMoney stores the MoneyVector as a serialized string, so it
preserves all information including the currency space. AtomicMoney
stores the MoneyVector as an integer of the smallest denomination of
whatever currency is provided to the column constructor on model
declaration. For this reason, AtomicMoney is intended to only be used by
single-currency applcations that want to take advantage of the more
efficient storage format and the ability to use aggregate functions
inside the db. Multi-currency applications should prefer VectorMoney.

I have added some basic test cases to the new
`tests/ext/sqlalchemy_test.py` test file, but they are not
comprehensive. They test the basic serialization correctness, which is
the most important thing, but they don't exercise any of the expected
error conditions. Specifically, I need to add tests for SpaceError when
attempting to store a MoneyVector with a different currency space in an
AtomicMoney column.

One last thing that needs to get taken care of before this feature can
be merged into dev is to get mypy passing. Currently, mypy is failing
because it can't find type definitions for sqlalchemy. I need to look
into this further to figure out why mypy can't resolve the types for
sqlalchemy. This branch can't be merged until this is sorted out.
Mypy isn't able to find types for sqlalchemy unless it is installed
with "sqlalchemy[mypy]". I have added this as a dependency to the
`types` hatch environment.

I also had to add a couple of type: ignore comments to the sqlalchemy
integrations to get better type checking.

SQLAlchemy types the `value` argument to the `process_bind_param` and
`process_result_value` methods of the `TypeDecorator` class as `Any |
None`, which makes sense for methods that need to be overriden with any
value, but it essentially disables static type checking on those
methods.

We can add a type annotation for the specific types we want to support
in the override, but mypy complains about Liskov since this is
technically a violation of type theory (subtypes can only extend their
parent types). Violating Liskov doesn't make any difference in this case
since the supertype is untyped, so I have added `type: ignore[override]`
comments to these lines. This should ignore the Liskov error but still
provide type checking against the provided annotations.

This could potentially cause a typing problem if someone tries to
subclass `VectorMoney` or `AtomicMoney` for some reason. That's what the
Liskov Substitution Principle is intended to address, but subclassing
those types would be stupid, so I'm not worried about it.
I added a couple test cases that make sure the AtomicMoney column is
raising errors when passed a vector that will break the math model.

The AtomicMoney column uses a single-currency space to evaluate the
stored vector, so SpaceError gets raised if the user tries to store
any vectors of more than one dimension in this column. This will be
caught by sqlalchemy and reraised as a `sqlalchemy.exc.StatementError`.

I also fixed a bug in the test suite for the sqlalchemy extension.
The test cases for the AtomicMoney column were using the VectorMoney
column in the test code, so the AtomicMoney type actually wasn't being
exercised in the test suite. This is because I copy/pasted the test
functions for the VectorMoney type and changed the setup, but I forgot
to update the model class used in the sqlalchemy queries, which is a
very facepalmy thing to do, but stuff happens. This is why we write
lots of tests. I realized this when I added test cases that expected
an error to be raised by the AtomicMoney column that didn't get raised
because it was using the VectorMoney column instead. :)

These test cases could be parametrized, but there's enough difference
in their setup that it's not worth it for only two inputs.

All tests are passing and testing the correct classes. Coverage
report is also showing 100% coverage of both classes.
The docstrings for VectorMoney and AtomicMoney in the sqlalchemy
integrations were not formatted correctly, so this was causing failures
in the doctests. I have updated the docstrings to use the correct
formatting.

I also added the `linearmoney.ext` module to the modules collected in
the `documentation/run_doctests.py` script since that script explicitly
collects docstring examples for packages, so it wasn't running the
doctests for the namespace package before.
I modified the module structure a bit to allow pdoc to find the
`linearmoney.ext.sqlalchemy` module and generate api documentation for
it.

There were a couple of ways to make this work. One way was to simply
pass in the namespace package in the call to `pdoc` on the commandline.
Another way was to make the `ext` package a regular python package with
an `__init__.py` file and let pdoc find and document everything based on
the `__all__` values just like the rest of the framework. I chose the
latter because there wasn't any reason to use a namespace package in the
first place, and having the `__init__.py` file allows me to document the
package itself as well as its contents, which is helpful for the api
doc site.

I also added some improvements to the docstrings in the sqlalchemy
module, and I made the `currency`, `forex`, and `space` class vars
read-only properties instead of mutable class members. This makes the
intended usage contractual just like the classes in the core library.

Lastly, I fixed a couple of typos in the docstrings of the
`linearmoney.vector` module. I was using `Munny` instead of `Money`.
This is a remnant of the library when I was still working on it in
stealth mode.

I did build the docs in this commit because I wanted to keep the source
changes visible in the commit diff. I built the docs locally and
verified that everything is generating correctly, so I don't anticipate
any problems, but I will double-check everything when I build the
documentation for the next release.
The test suite didn't depend explicitly on `sqlalchemy[asyncio]` even
though it is testing async sqlalchemy usage in the extensions tests.

This wasn't a problem on my linux machine since greenlet is already
installed, but CI failed on the MaxOS runner because greenlet is not
already installed on Mac. Adding the `sqlalchemy[asyncio]` explicit
dependency to the `test` environment should fix this.

See: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#asyncio-platform-installation-notes-including-apple-m1
@GrammAcc GrammAcc merged commit ee6cbba into dev May 24, 2024
2 checks passed
@GrammAcc GrammAcc deleted the sqlalchemy branch May 24, 2024 00:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add SQLAlchemy Integrations
1 participant