Skip to content

Add AllocationDailyBillableUsage model and improve tests#317

Open
jimmysway wants to merge 26 commits into
nerc-project:mainfrom
jimmysway:copilot/add-usage-info-database-table
Open

Add AllocationDailyBillableUsage model and improve tests#317
jimmysway wants to merge 26 commits into
nerc-project:mainfrom
jimmysway:copilot/add-usage-info-database-table

Conversation

@jimmysway

Copy link
Copy Markdown
Contributor

Closes #292

Copilot AI and others added 21 commits February 10, 2026 20:05
…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.
reconfigured imports
@jimmysway jimmysway force-pushed the copilot/add-usage-info-database-table branch from 01b3089 to 8781083 Compare May 28, 2026 20:07
Comment thread src/coldfront_plugin_cloud/apps.py Outdated

def ready(self):
import coldfront_plugin_cloud.signals # noqa: F401
import coldfront_plugin_cloud.models.daily_billable_usage # noqa: F401

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why is this being imported here?

@jimmysway jimmysway Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would adding the import in models/__init__.py instead of here work?

@knikolla knikolla left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@jimmysway jimmysway Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Comment thread src/coldfront_plugin_cloud/apps.py Outdated

def ready(self):
import coldfront_plugin_cloud.signals # noqa: F401
import coldfront_plugin_cloud.models.daily_billable_usage # noqa: F401

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would adding the import in models/__init__.py instead of here work?

@knikolla knikolla requested a review from Copilot June 4, 2026 20:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 AllocationDailyBillableUsage model + initial migration to store daily usage per (allocation, date, SU type).
  • Extend fetch_daily_billable_usage to write daily usage rows to DB and add --remove to delete rows for a given date.
  • Add DB-backed read helpers (billable_usage.py), a seed_daily_billable_usage command, 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.

Comment thread src/coldfront_plugin_cloud/tests/unit/test_daily_billable_usage.py Outdated
Comment on lines +310 to +313
time.sleep(0.02)
row.value = Decimal("150.00")
row.save()
row.refresh_from_db()
Comment on lines +40 to +43
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)
Comment on lines +21 to +23
Raises:
TypeError: If rows is None.
ValueError: If a row has an empty su_type.
@jimmysway

jimmysway commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

I will go over all the comments but I also have one question right now there is a management/commands/fetch_daily_billable_usage.py which is the fetching code for S3 and also my newly written models/daily_billable_usage.py which is fetching from the database.

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.

@jimmysway jimmysway force-pushed the copilot/add-usage-info-database-table branch from 805e059 to 39cdb54 Compare June 9, 2026 18:49
loaded ORM with __init__.py

Rewrote testing logic
@jimmysway jimmysway force-pushed the copilot/add-usage-info-database-table branch from 39cdb54 to 7f67ab7 Compare June 9, 2026 20:38
@jimmysway jimmysway requested a review from knikolla June 12, 2026 16:59
@jimmysway

Copy link
Copy Markdown
Contributor Author

I will also update the table implementation to use this injection pattern instead

@jimmysway jimmysway force-pushed the copilot/add-usage-info-database-table branch from 0b8cb59 to 5809ec7 Compare June 15, 2026 17:54

@QuanMPhm QuanMPhm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The structure looks good to me. I have comments on some lower-level details

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we want related_name to be daily_usage_records?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +27 to +33
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"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Comment on lines +36 to +37
if rows is None:
raise TypeError("rows must not be None")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Comment on lines +45 to +46
if not row.su_type:
raise ValueError(f"usage row id={row.pk} has empty su_type")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When would su_type be empty, and do we want to have empty su_types ("")

Comment on lines +80 to +81
if not isinstance(date, str) or not date.strip():
raise ValueError("date must be a non-empty YYYY-MM-DD string")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Comment thread .gitignore
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
*.db

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@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?

Comment on lines +40 to +42
self.assertEqual(usage.root["OpenStack CPU"], Decimal("100"))
self.assertEqual(usage.root["Storage"], Decimal("30.12"))
self.assertEqual(usage.total_charges, Decimal("130.12"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For all of these test cases, I think its more clear to just create a UsageInfo or dict "answer", and compare them directly

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.

Create new table AllocationDailyBillableUsage

5 participants