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

[Issue 1135] Setup lookup value logic within the API #1136

Merged
merged 4 commits into from
Feb 9, 2024

Conversation

chouinar
Copy link
Collaborator

@chouinar chouinar commented Feb 2, 2024

Summary

Fixes #1135

Time to review: 15 mins

Changes proposed

Added the ability to configure lookup values across the API and database as Python enums - this includes:

  • Various utility container classes for connecting the components of the approach
  • A global LookupRegistry where we validate and store the configurations
  • A SQLAlchemy column type which handles converting the DB lookup IDs into enums automatically
  • Updated the DB migrations to automatically update the lookup tables
  • Automatically handle converting enums to integers in the DB and back using SQLAlchemy type decorators.

Context for reviewers

This implementation is largely borrowed from a prior project that used the template repo as well, and worked well there.

While this is quite a bit, it's a lot of boilerplatey-setup, testing, and validation to make usage of it pretty foolproof (ie. hard to setup wrong). Actual usage of it should be pretty simple, just create/update your enum, update the config, and attach it to a table. Adding a new value just requires a 2-line change and the API, DB table, and all validation "just works". See the new .md file added for examples.

I'm not 100% on the exact file naming/structure, but there's a lot of risk for circular dependencies with how it is currently setup, and this is what I could get to work, happy to adjust if you've got any suggestions.

A few pre-emptive answers to questions:

  • Why use foreign keys in the DB instead of enums or check constraints?
    • Alembic struggles to detect changes to allowed values, and even a custom approach would be very difficult. Modifying an existing enum/check constraints is either complex or requires dropping and remaking the constraint. Also, this approach lets the ID in the DB be separate from the value which is nice for renaming them.
  • Why make a separate LookupConfig class instead of putting the value in the Enum/why not make a class that does both?
    • I looked into this before. The very short version is that adding more info to an existing Python enum is difficult, you can make the values something like a tuple, but it causes a lot of its convenience to break (can't do str to enum). Also looked into building my own equivalent to Python enums and that gets into complex metaclass logic, which even if you make a proper implementation, python typing libraries like MyPy struggle to interpret it like a Python enum (which under the hood is really really strange as each enum member is also the class itself?).

Additional information

Running migrations locally creates the lk_opportunity_category table and populates it with the values we've configured:
Screenshot 2024-02-02 at 3 16 50 PM

The opportunity table has has a foreign key column with this field:
Screenshot 2024-02-02 at 3 17 54 PM

Note that the factory we use to generate data locally just knows about the enum, the LookupColumn that we have configured automatically handles converting that to the integer without us needing to be aware of it.

@chouinar chouinar requested review from acouch and jamesbursa February 2, 2024 20:30
@github-actions github-actions bot added documentation Improvements or additions to documentation python api database labels Feb 2, 2024
Copy link
Collaborator

@jamesbursa jamesbursa left a comment

Choose a reason for hiding this comment

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

Looks good, thanks.

if value is None:
return None

if not isinstance(value, self.lookup_enum):
if not isinstance(value, (StrEnum, IntEnum)):
Copy link
Collaborator

Choose a reason for hiding this comment

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

An idea for making this check stronger (currently it could allow the wrong enum type to be set I think).

  • Add an enum_type argument to __init__() and store it as a member variable.
  • Change this line to isinstance(value, self.enum_type)
  • Wherever LookupColumn is created, add the enum type. For example:
   category: Mapped[OpportunityCategory | None] = mapped_column(
        "opportunity_category_id",
        LookupColumn(LkOpportunityCategory, OpportunityCategory),  # 2nd argument added
        ForeignKey(LkOpportunityCategory.opportunity_category_id),
        index=True,
    )

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I could also just make it so each Lk___ class has the enum specifically registered with it and use that? It registers the mapping of enum values, but if I just add the enum itself that would accomplish the same thing

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In the LookupConfig we already were grabbing all of the enums that were associated with a config (note that while not used now - this is setup to allow multiple python enums for a given Lk table), just piped that through via the LookupRegistry

@chouinar chouinar merged commit 5f468ef into main Feb 9, 2024
8 checks passed
@chouinar chouinar deleted the chouinar/lookup-setup branch February 9, 2024 17:04
chouinar added a commit that referenced this pull request Feb 9, 2024
## Summary
Fixes #1166

This requires #1136 to be
merged first as it builds ontop of that work

### Time to review: __5 mins__

## Changes proposed
Created the opportunity tables for the expanded data model

https://app.gitbook.com/o/cFcvhi6d0nlLyH2VzVgn/s/v1V0jIH7mb7Yb3jlNrgk/engineering/learnings/opportunity-endpoint-data-model#overview

Setup factories to generate data in a reasonable way for these tables

## Context for reviewers
This sets up all of the tables for our expanded opportunity data model -
tables we'll copy to for DMS that will later be transformed to these
tables will be a separate PR.

The factories try to make somewhat realistic data with some variability.


Note that I did adjust a few things from the tech spec, which I just
haven't changed in that document yet (can't merge the changes myself)
- The `other_value` fields in the link tables actually make sense to
stay in the summary table, I found examples of them working different
than I had assumed, so it makes more sense to keep them in a general
place.
- The `opportunity_agency` table idea I've dropped - if we want to
adjust how agency data is stored, will deal with it later when we deal
with the rest of the agency data
- Didn't adjust the primary keys - just keeping everything on
`opportunity_id` - already was going to keep several that way - don't
see a reason to have several different primary/foreign key setups.
- Miscellaneous small renames to more closely match the design of the
front-end

## Additional information
Running `make db-seed-local` after the migrations shows the data in
various tables in what I would expect:
![Screenshot 2024-02-07 at 12 55 48
PM](https://github.com/HHS/simpler-grants-gov/assets/46358556/1432481d-0563-45d2-9660-66721c16c4ba)
![Screenshot 2024-02-07 at 12 56 14
PM](https://github.com/HHS/simpler-grants-gov/assets/46358556/5d9a63fb-4339-42cc-b445-f204f9a7842c)
![Screenshot 2024-02-07 at 12 56 21
PM](https://github.com/HHS/simpler-grants-gov/assets/46358556/d61999ed-f2c9-4bbe-9c09-8337fdf38914)
![Screenshot 2024-02-07 at 12 56 26
PM](https://github.com/HHS/simpler-grants-gov/assets/46358556/478002bb-6c41-4429-ae6b-d088e3f8297f)
![Screenshot 2024-02-07 at 12 56 44
PM](https://github.com/HHS/simpler-grants-gov/assets/46358556/2211c4cb-6f99-4655-b814-09db66b4aeb9)

---------

Co-authored-by: nava-platform-bot <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api database documentation Improvements or additions to documentation python
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Task]: Setup lookup value logic within the API
3 participants