Add AllocationDailyBillableUsage model and improve tests#317
Add AllocationDailyBillableUsage model and improve tests#317jimmysway wants to merge 26 commits into
Conversation
…mmand Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
…mpty usage, remove duplicate print Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
…odel Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
…Model from the start Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
Co-authored-by: knikolla <4123322+knikolla@users.noreply.github.com>
Added tests to validate ORM layer and fetching layer that interacts with ORM
…ueries and standardizing string formatting in models and tests.
…nfo-database-table
reconfigured imports
01b3089 to
8781083
Compare
|
|
||
| def ready(self): | ||
| import coldfront_plugin_cloud.signals # noqa: F401 | ||
| import coldfront_plugin_cloud.models.daily_billable_usage # noqa: F401 |
There was a problem hiding this comment.
why is this being imported here?
There was a problem hiding this comment.
I don't really understand everything here but I stumbled across this article when trying to figure out why .signals was being imported which led me to this article in the django documentation
https://docs.djangoproject.com/en/6.0/ref/applications/#how-applications-are-loaded
Where it says
Then Django attempts to import the models submodule of each application, if there is one.
You must define or import all models in your application’s models.py or models/init.py. Otherwise, the application registry may not be fully populated at this point, which could cause the ORM to malfunction.
Once this stage completes, APIs that operate on models such as get_model() become usable.
We have historically left the __init__.py blank and I don't know if this daily_billable_usage model is fundamentally different than the other ones because it is ORM related? I might need some help understanding this
There was a problem hiding this comment.
Would adding the import in models/__init__.py instead of here work?
knikolla
left a comment
There was a problem hiding this comment.
Did a first pass at reviewing timeboxed to 25mins. Overall looks in the right direction. Please instead of creating a new command enhance the testing code to create the testing setup you need.
| return f"{today.year}-{today.month:02d}" | ||
|
|
||
|
|
||
| class Command(BaseCommand): |
There was a problem hiding this comment.
Please don't create a new command for this. Instead look into either mocking the tests so that the values that you need are returned from the fictional CSV or sample values inserted into the DB.
There was a problem hiding this comment.
Just to clarify, this seeding logic is primarily for testing of the downstream graphical UI elements in coldfront-nerc. I will find another way to insert sample values directly into the DB
|
|
||
| def ready(self): | ||
| import coldfront_plugin_cloud.signals # noqa: F401 | ||
| import coldfront_plugin_cloud.models.daily_billable_usage # noqa: F401 |
There was a problem hiding this comment.
Would adding the import in models/__init__.py instead of here work?
There was a problem hiding this comment.
Pull request overview
Adds persistent storage and retrieval for per-allocation daily billable usage by introducing an AllocationDailyBillableUsage model/table, wiring the existing fetch_daily_billable_usage command to upsert rows (and support deleting a day’s rows via --remove), and expanding test coverage plus a local-dev seeding command.
Changes:
- Introduce
AllocationDailyBillableUsagemodel + initial migration to store daily usage per (allocation, date, SU type). - Extend
fetch_daily_billable_usageto write daily usage rows to DB and add--removeto delete rows for a given date. - Add DB-backed read helpers (
billable_usage.py), aseed_daily_billable_usagecommand, and new/expanded unit tests.
Reviewed changes
Copilot reviewed 9 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/coldfront_plugin_cloud/tests/unit/test_fetch_daily_billable_usage.py | Adds tests covering DB insert/update behavior and the new --remove path. |
| src/coldfront_plugin_cloud/tests/unit/test_daily_billable_usage.py | New unit tests for DB read helpers and the new model’s constraints/behavior. |
| src/coldfront_plugin_cloud/models/daily_billable_usage.py | Adds the AllocationDailyBillableUsage Django model (FK to Allocation, date, su_type, value). |
| src/coldfront_plugin_cloud/migrations/0001_initial.py | Creates the new table, indexes, and unique constraint. |
| src/coldfront_plugin_cloud/migrations/init.py | Migration package marker. |
| src/coldfront_plugin_cloud/management/commands/seed_daily_billable_usage.py | New command to seed daily usage rows for local development/testing. |
| src/coldfront_plugin_cloud/management/commands/fetch_daily_billable_usage.py | Persists fetched usage to DB and adds --remove option to delete a date’s rows. |
| src/coldfront_plugin_cloud/billable_usage.py | New helper APIs to read daily usage (single day and date range) from the DB. |
| src/coldfront_plugin_cloud/apps.py | Imports the new model module during app initialization. |
| setup.cfg | Adds django-model-utils dependency for TimeStampedModel. |
| .gitignore | Ignores *.db files (likely local sqlite DB artifacts). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| time.sleep(0.02) | ||
| row.value = Decimal("150.00") | ||
| row.save() | ||
| row.refresh_from_db() |
| for su_type, amount in base.items(): | ||
| step = _RAMP_STEP.get(su_type, 0) | ||
| ramped[su_type] = str(float(amount) + day_index * step) | ||
| return usage_models.UsageInfo(ramped) |
| Raises: | ||
| TypeError: If rows is None. | ||
| ValueError: If a row has an empty su_type. |
|
I will go over all the comments but I also have one question right now there is a I find it confusing sometimes for which is which do you think I should rename these to distinguish between them better or is it fine. |
805e059 to
39cdb54
Compare
loaded ORM with __init__.py Rewrote testing logic
39cdb54 to
7f67ab7
Compare
|
I will also update the table implementation to use this injection pattern instead |
0b8cb59 to
5809ec7
Compare
QuanMPhm
left a comment
There was a problem hiding this comment.
The structure looks good to me. I have comments on some lower-level details
There was a problem hiding this comment.
If this file is intended to inject test data that will be used to test the UI graphs in coldfront-nerc, shouldn't this file be added in that PR instead? I'm not familiar with the UI code or how testing will be done with it yet, but that's my reaction. It's odd seeing a file added in a PR that doesn't it.
@jimmysway Is this only way to test the UI graphs?
There was a problem hiding this comment.
Well this file tests the injection pattern that the UI charts will get their data from, injecting sample data into a database tests the entire flow rather than just mocking it at a higher level.
I can for sure add this to another PR but it is very dependent on the contents of this PR so it was the easiest way for me to write a script to write some sample data into the db
| allocation = models.ForeignKey( | ||
| Allocation, | ||
| on_delete=models.CASCADE, | ||
| related_name="daily_usage_records", |
There was a problem hiding this comment.
Why do we want related_name to be daily_usage_records?
There was a problem hiding this comment.
It lets you write allocation.daily_usage_records to get all the usage rows for a given allocation, instead of Django's ugly default name (allocation.allocationdailybillableusage_set). It's just for readability/convenience when querying.
| db_table = "coldfront_plugin_cloud_allocationdailybillableusage" | ||
| unique_together = [["allocation", "date", "su_type"]] | ||
| indexes = [ | ||
| models.Index(fields=["allocation", "date"]), | ||
| models.Index(fields=["date"]), | ||
| ] | ||
| ordering = ["-date", "allocation", "su_type"] |
There was a problem hiding this comment.
aside from unique_together, why are any of the other Meta options nessecary? I suppose indexes gives a performance boost.
unique_together can also be simplified to ["allocation", "date", "su_type"], no need for nested list
There was a problem hiding this comment.
db_table = "coldfront_plugin_cloud_allocationdailybillableusage"
Will be auto generated by django yes, but the preference for being explicit to navigate the db without a lot of prior understanding behind the magic autogeneration pattern that django would do on its own
| if rows is None: | ||
| raise TypeError("rows must not be None") |
There was a problem hiding this comment.
I impression with the majority of the raised errors in this module is that they are unnecessary. This one, for example, wouldn't be anymore helpful than the TypeError we'll get below from for row in rows:
I would only raise an error if the code encounters a case where we have deliberately decided that the expected behavior should be a raised error. I think raising errors like in this example only adds a slightly more helpful error message, and more mental burden when maintaining and reviewing code. @knikolla Do you have an opinion?
| if not row.su_type: | ||
| raise ValueError(f"usage row id={row.pk} has empty su_type") |
There was a problem hiding this comment.
When would su_type be empty, and do we want to have empty su_types ("")
| if not isinstance(date, str) or not date.strip(): | ||
| raise ValueError("date must be a non-empty YYYY-MM-DD string") |
There was a problem hiding this comment.
I think this is unnecessary since validate_date_str would validate the string anyways
| return usage_info | ||
|
|
||
|
|
||
| def get_daily_billable_usage(allocation: Allocation, date: str) -> UsageInfo: |
There was a problem hiding this comment.
Can't we remove this function, since get_daily_billable_usage_by_date can accomplish the same thing by passing the same date to both start_date and end_date?
Alternatively, why not use this in get_daily_billable_usage_by_date? You make a few more queries, but it usually won't be more than a month of queries, yes?
| [._]s[a-rt-v][a-z] | ||
| [._]ss[a-gi-z] | ||
| [._]sw[a-p] | ||
| *.db |
There was a problem hiding this comment.
I don't think this should be here. I don't think we'll ever need to commit a .db file, but this doesn't seem nessecary
There was a problem hiding this comment.
@knikolla It seems if we were to run fetch_daily_billable_usage twice on the same day, and the S3 invoicing data has changed such that a project is gone from the invoices, then the project's usage entry would still persist in the Coldfront database, unless we run --remove and rerun fetch_daily_billable_usage normally again. This is fine?
| self.assertEqual(usage.root["OpenStack CPU"], Decimal("100")) | ||
| self.assertEqual(usage.root["Storage"], Decimal("30.12")) | ||
| self.assertEqual(usage.total_charges, Decimal("130.12")) |
There was a problem hiding this comment.
For all of these test cases, I think its more clear to just create a UsageInfo or dict "answer", and compare them directly
Closes #292