From d6dbcb279c6dac0e8b1bee855f0e07ffd9db6159 Mon Sep 17 00:00:00 2001 From: Kristi Nikolla Date: Fri, 12 Apr 2024 18:46:01 -0400 Subject: [PATCH 1/2] Add pre-commit workflow Implements linting with black, among other checks. --- .github/workflows/pre-commit.yaml | 15 ++ .pre-commit-config.yaml | 16 +++ src/openstack_billing_db/__init__.py | 1 - src/openstack_billing_db/billing.py | 62 +++++---- src/openstack_billing_db/fetch.py | 38 +++-- src/openstack_billing_db/main.py | 130 +++++++++--------- src/openstack_billing_db/model.py | 46 +++---- .../tests/unit/test_instance.py | 103 +++++++------- 8 files changed, 233 insertions(+), 178 deletions(-) create mode 100644 .github/workflows/pre-commit.yaml create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 0000000..8653c98 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,15 @@ +name: pre-commit + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2d5955c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: check-merge-conflict + - id: end-of-file-fixer + - id: check-added-large-files + - id: check-case-conflict + - id: detect-private-key + - id: check-yaml + + - repo: https://github.com/ambv/black + rev: 23.1.0 + hooks: + - id: black diff --git a/src/openstack_billing_db/__init__.py b/src/openstack_billing_db/__init__.py index 8b13789..e69de29 100644 --- a/src/openstack_billing_db/__init__.py +++ b/src/openstack_billing_db/__init__.py @@ -1 +0,0 @@ - diff --git a/src/openstack_billing_db/billing.py b/src/openstack_billing_db/billing.py index a1d6398..e79fd26 100644 --- a/src/openstack_billing_db/billing.py +++ b/src/openstack_billing_db/billing.py @@ -91,7 +91,7 @@ def collect_invoice_data_from_openstack(database, billing_start, billing_end, ra institution="", instances=project.instances, invoice_interval=f"{billing_start.date()} - {billing_end.date()}", - rates=rates + rates=rates, ) for i in project.instances: # type: model.Instance @@ -126,8 +126,8 @@ def collect_invoice_data_from_openstack(database, billing_start, billing_end, ra return invoices -def load_flavors_cache(flavors_cache_file) -> dict[int: model.Flavor]: - with open(flavors_cache_file, 'r') as f: +def load_flavors_cache(flavors_cache_file) -> dict[int : model.Flavor]: + with open(flavors_cache_file, "r") as f: cache = json.load(f) flavors = [] @@ -138,12 +138,12 @@ def load_flavors_cache(flavors_cache_file) -> dict[int: model.Flavor]: def write_flavors_cache(flavors_cache_file, flavors): - with open(flavors_cache_file, 'w') as f: + with open(flavors_cache_file, "w") as f: f.write(json.dumps(flavors, indent=4)) def merge_coldfront_data(invoices, coldfront_data_file): - with open(coldfront_data_file, 'r') as f: + with open(coldfront_data_file, "r") as f: allocations = json.load(f) by_project_id = { @@ -155,16 +155,17 @@ def merge_coldfront_data(invoices, coldfront_data_file): a = by_project_id[invoice.project_id] invoice.project_name = a["attributes"]["Allocated Project Name"] invoice.institution_specific_code = a["attributes"].get( - "Institution-Specific Code", "N/A") + "Institution-Specific Code", "N/A" + ) invoice.pi = a["project"]["pi"] except KeyError: continue def write(invoices, output, invoice_month=None): - with open(output, 'w', newline='') as f: + with open(output, "w", newline="") as f: csv_invoice_writer = csv.writer( - f, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL + f, delimiter=",", quotechar="|", quoting=csv.QUOTE_MINIMAL ) # Write Headers csv_invoice_writer.writerow( @@ -186,7 +187,12 @@ def write(invoices, output, invoice_month=None): for invoice in invoices: for invoice_type in [ - 'cpu', 'gpu_a100sxm4', 'gpu_a100', 'gpu_v100', 'gpu_k80', 'gpu_a2' + "cpu", + "gpu_a100sxm4", + "gpu_a100", + "gpu_v100", + "gpu_k80", + "gpu_a2", ]: # Each project gets two rows, one for CPU and one for GPU hours = invoice.__getattribute__(f"{invoice_type}_su_hours") @@ -194,10 +200,13 @@ def write(invoices, output, invoice_month=None): su_name = invoice.rates.__getattribute__(f"{invoice_type}_su_name") cost = invoice.__getattribute__(f"{invoice_type}_su_cost") if hours > 0: - csv_invoice_writer.writerow( [ - invoice_month if invoice_month else invoice.invoice_interval, + ( + invoice_month + if invoice_month + else invoice.invoice_interval + ), invoice.project_name, invoice.project_id, invoice.pi, @@ -213,13 +222,17 @@ def write(invoices, output, invoice_month=None): ) -def generate_billing(start, end, output, rates, - coldfront_data_file=None, - invoice_month=None, - upload_to_s3=False, - sql_dump_file=None, - upload_to_primary_location=True): - +def generate_billing( + start, + end, + output, + rates, + coldfront_data_file=None, + invoice_month=None, + upload_to_s3=False, + sql_dump_file=None, + upload_to_primary_location=True, +): database = model.Database(start, sql_dump_file) invoices = collect_invoice_data_from_openstack(database, start, end, rates) @@ -228,15 +241,18 @@ def generate_billing(start, end, output, rates, write(invoices, output, invoice_month) if upload_to_s3: - s3_endpoint = os.getenv("S3_OUTPUT_ENDPOINT_URL", - "https://s3.us-east-005.backblazeb2.com") + s3_endpoint = os.getenv( + "S3_OUTPUT_ENDPOINT_URL", "https://s3.us-east-005.backblazeb2.com" + ) s3_bucket = os.getenv("S3_OUTPUT_BUCKET", "nerc-invoicing") s3_key_id = os.getenv("S3_OUTPUT_ACCESS_KEY_ID") s3_secret = os.getenv("S3_OUTPUT_SECRET_ACCESS_KEY") if not s3_key_id or not s3_secret: - raise Exception("Must provide S3_OUTPUT_ACCESS_KEY_ID and" - " S3_OUTPUT_SECRET_ACCESS_KEY environment variables.") + raise Exception( + "Must provide S3_OUTPUT_ACCESS_KEY_ID and" + " S3_OUTPUT_SECRET_ACCESS_KEY environment variables." + ) if not invoice_month: raise Exception("No invoice month specified. Required for S3 upload.") @@ -255,7 +271,7 @@ def generate_billing(start, end, output, rates, s3.upload_file(output, Bucket=s3_bucket, Key=primary_location) logger.info(f"Uploaded to {primary_location}.") - timestamp = datetime.utcnow().strftime('%Y%m%dT%H%M%SZ') + timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") secondary_location = ( f"Invoices/{invoice_month}/" f"Archive/NERC OpenStack {invoice_month} {timestamp}.csv" diff --git a/src/openstack_billing_db/fetch.py b/src/openstack_billing_db/fetch.py index ca588f5..f95363b 100644 --- a/src/openstack_billing_db/fetch.py +++ b/src/openstack_billing_db/fetch.py @@ -41,15 +41,18 @@ def download_latest_dump_from_s3() -> str: } """ - s3_endpoint = os.getenv("S3_INPUT_ENDPOINT_URL", - "https://holecs.rc.fas.harvard.edu") + s3_endpoint = os.getenv( + "S3_INPUT_ENDPOINT_URL", "https://holecs.rc.fas.harvard.edu" + ) s3_bucket = os.getenv("S3_INPUT_BUCKET", "nerc-osp-backups") s3_key_id = os.getenv("S3_INPUT_ACCESS_KEY_ID") s3_secret = os.getenv("S3_INPUT_SECRET_ACCESS_KEY") if not s3_key_id or not s3_secret: - raise Exception("Must provide S3_INPUT_ACCESS_KEY_ID and" - " S3_INPUT_SECRET_ACCESS_KEY environment variables.") + raise Exception( + "Must provide S3_INPUT_ACCESS_KEY_ID and" + " S3_INPUT_SECRET_ACCESS_KEY environment variables." + ) s3 = boto3.client( "s3", @@ -58,9 +61,8 @@ def download_latest_dump_from_s3() -> str: aws_secret_access_key=s3_secret, ) - today = datetime.today().strftime('%Y%m%d') - dumps = s3.list_objects_v2(Bucket=s3_bucket, - Prefix=f"dbs/nerc-ctl-0/nova-{today}") + today = datetime.today().strftime("%Y%m%d") + dumps = s3.list_objects_v2(Bucket=s3_bucket, Prefix=f"dbs/nerc-ctl-0/nova-{today}") if len(dumps["Contents"]) == 0: raise Exception(f"No database dumps found for {today}") @@ -109,8 +111,10 @@ def convert_mysqldump_to_sqlite(path_to_dump) -> str: command = subprocess.run(["mysql2sqlite", path_to_dump], stdout=f) if command.returncode != 0: - raise Exception(f"Error converting {path_to_dump} to SQLite compatible" - f" at {destination_path}.") + raise Exception( + f"Error converting {path_to_dump} to SQLite compatible" + f" at {destination_path}." + ) logger.info(f"Converted at {destination_path}.") return destination_path @@ -120,15 +124,19 @@ def get_keycloak_session(): """Authenticate as a client with Keycloak to receive an access token.""" keycloak_token_url = os.getenv( "KEYCLOAK_TOKEN_URL", - ("https://keycloak.mss.mghpcc.org/auth/realms/mss" - "/protocol/openid-connect/token") + ( + "https://keycloak.mss.mghpcc.org/auth/realms/mss" + "/protocol/openid-connect/token" + ), ) keycloak_client_id = os.getenv("KEYCLOAK_CLIENT_ID") keycloak_client_secret = os.getenv("KEYCLOAK_CLIENT_SECRET") if not keycloak_client_id or not keycloak_client_secret: - raise Exception("Must provide KEYCLOAK_CLIENT_ID and" - " KEYCLOAK_CLIENT_SECRET environment variables.") + raise Exception( + "Must provide KEYCLOAK_CLIENT_ID and" + " KEYCLOAK_CLIENT_SECRET environment variables." + ) r = requests.post( keycloak_token_url, @@ -145,14 +153,14 @@ def get_keycloak_session(): session.headers.update(headers) return session + def download_coldfront_data(download_location=None) -> str: """Downloads allocation data from the ColdFront API. Returns location of downloaded JSON file. """ - colfront_url = os.getenv("COLDFRONT_URL", - "https://coldfront.mss.mghpcc.org") + colfront_url = os.getenv("COLDFRONT_URL", "https://coldfront.mss.mghpcc.org") allocations_url = f"{colfront_url}/api/allocations?all=true" logger.info(f"Making request to ColdFront at {allocations_url}") diff --git a/src/openstack_billing_db/main.py b/src/openstack_billing_db/main.py index 04c0359..c5f2aef 100644 --- a/src/openstack_billing_db/main.py +++ b/src/openstack_billing_db/main.py @@ -12,7 +12,7 @@ def parse_time_argument(arg): if isinstance(arg, str): - return datetime.strptime(arg, '%Y-%m-%d') + return datetime.strptime(arg, "%Y-%m-%d") return arg @@ -38,127 +38,132 @@ def main(): "--start", default=default_start_argument(), type=parse_time_argument, - help=("Start of the invoicing period. (YYYY-MM-DD)." - " Defaults to start of last month if 1st of a month," - " or start of this month otherwise.") + help=( + "Start of the invoicing period. (YYYY-MM-DD)." + " Defaults to start of last month if 1st of a month," + " or start of this month otherwise." + ), ) parser.add_argument( "--end", default=default_end_argument(), type=parse_time_argument, - help=("End of the invoicing period. (YYYY-MM-DD)." - " Not inclusive. Defaults to today.") + help=( + "End of the invoicing period. (YYYY-MM-DD)." + " Not inclusive. Defaults to today." + ), ) parser.add_argument( "--invoice-month", - default=default_start_argument().strftime('%Y-%m'), - help=("Use the first column for Invoice Month, rather than Interval." - " Defaults to month of start. (YYYY-MM).") + default=default_start_argument().strftime("%Y-%m"), + help=( + "Use the first column for Invoice Month, rather than Interval." + " Defaults to month of start. (YYYY-MM)." + ), ) parser.add_argument( "--coldfront-data-file", default=None, - help=("Path to JSON Output of ColdFront's /api/allocations." - "Used for populating project names and PIs. If" - " --download-coldfront-data option is applied, this" - " location will be used to save the downloaded output.") + help=( + "Path to JSON Output of ColdFront's /api/allocations." + "Used for populating project names and PIs. If" + " --download-coldfront-data option is applied, this" + " location will be used to save the downloaded output." + ), ) parser.add_argument( "--download-coldfront-data", default=False, - help=("Download ColdFront data from ColdFront. Requires the environment" - " variables KEYCLOAK_CLIENT_ID and KEYCLOAK_CLIENT_SECRET." - " Default to NERC Keycloak and ColdFront but can be" - " configure using KEYCLOAK_TOKEN_URL and COLDFRONT_URL environment" - " variables.") + help=( + "Download ColdFront data from ColdFront. Requires the environment" + " variables KEYCLOAK_CLIENT_ID and KEYCLOAK_CLIENT_SECRET." + " Default to NERC Keycloak and ColdFront but can be" + " configure using KEYCLOAK_TOKEN_URL and COLDFRONT_URL environment" + " variables." + ), ) parser.add_argument( "--sql-dump-file", default="", - help=("Path to SQL Dump of Nova DB. Must have been converted to SQLite3" - "compatible format using https://github.com/dumblob/mysql2sqlite.") + help=( + "Path to SQL Dump of Nova DB. Must have been converted to SQLite3" + "compatible format using https://github.com/dumblob/mysql2sqlite." + ), ) parser.add_argument( "--convert-sql-dump-file-to-sqlite", default=True, - help=("Automatically convert SQL dump to SQlite3 compatible format using" - " https://github.com/dumblob/mysql2sqlite.") + help=( + "Automatically convert SQL dump to SQlite3 compatible format using" + " https://github.com/dumblob/mysql2sqlite." + ), ) parser.add_argument( "--download-sql-dump-from-s3", default=False, - help=("Downloads Nova DB Dump from S3." - " Must provide S3_INPUT_ACCESS_KEY_ID and" - " S3_INPUT_SECRET_ACCESS_KEY environment variables." - " Defaults to Backblaze and to nerc-invoicing bucket" - " but can be configured through S3_INPUT_BUCKET and" - " S3_OUTPUT_ENDPOINT_URL environment variables." - " Automatically decompresses the file if gzipped.") + help=( + "Downloads Nova DB Dump from S3." + " Must provide S3_INPUT_ACCESS_KEY_ID and" + " S3_INPUT_SECRET_ACCESS_KEY environment variables." + " Defaults to Backblaze and to nerc-invoicing bucket" + " but can be configured through S3_INPUT_BUCKET and" + " S3_OUTPUT_ENDPOINT_URL environment variables." + " Automatically decompresses the file if gzipped." + ), ) parser.add_argument( - "--rate-cpu-su", - default=0, - type=Decimal, - help="Rate of CPU SU/hr" + "--rate-cpu-su", default=0, type=Decimal, help="Rate of CPU SU/hr" ) parser.add_argument( "--rate-gpu-a100sxm4-su", default=0, type=Decimal, - help="Rate of GPU A100SXM4 SU/hr" + help="Rate of GPU A100SXM4 SU/hr", ) parser.add_argument( - "--rate-gpu-a100-su", - default=0, - type=Decimal, - help="Rate of GPU A100 SU/hr" + "--rate-gpu-a100-su", default=0, type=Decimal, help="Rate of GPU A100 SU/hr" ) parser.add_argument( - "--rate-gpu-v100-su", - default=0, - type=Decimal, - help="Rate of GPU V100 SU/hr" + "--rate-gpu-v100-su", default=0, type=Decimal, help="Rate of GPU V100 SU/hr" ) parser.add_argument( - "--rate-gpu-k80-su", - default=0, - type=Decimal, - help="Rate of GPU K80 SU/hr" + "--rate-gpu-k80-su", default=0, type=Decimal, help="Rate of GPU K80 SU/hr" ) parser.add_argument( - "--rate-gpu-a2-su", - default=0, - type=Decimal, - help="Rate of GPU A2 SU/hr" + "--rate-gpu-a2-su", default=0, type=Decimal, help="Rate of GPU A2 SU/hr" ) parser.add_argument( "--include-stopped-runtime", default=False, type=bool, - help="Include stopped runtime for instances." + help="Include stopped runtime for instances.", ) parser.add_argument( "--upload-to-s3", default=False, type=bool, - help=("Uploads the CSV result to S3 compatible storage." - " Must provide S3_OUTPUT_ACCESS_KEY_ID and" - " S3_OUTPUT_SECRET_ACCESS_KEY environment variables." - " Defaults to Backblaze and to nerc-invoicing bucket" - " but can be configured through S3_OUTPUT_BUCKET and" - " S3_OUTPUT_ENDPOINT_URL environment variables.") + help=( + "Uploads the CSV result to S3 compatible storage." + " Must provide S3_OUTPUT_ACCESS_KEY_ID and" + " S3_OUTPUT_SECRET_ACCESS_KEY environment variables." + " Defaults to Backblaze and to nerc-invoicing bucket" + " but can be configured through S3_OUTPUT_BUCKET and" + " S3_OUTPUT_ENDPOINT_URL environment variables." + ), ) parser.add_argument( "--upload-to-primary-location", default=True, type=bool, - help=("When uploading to S3, upload both to primary and" - " archive location, or just archive location.") + help=( + "When uploading to S3, upload both to primary and" + " archive location, or just archive location." + ), ) parser.add_argument( "--output-file", default="/tmp/openstack_invoices.csv", - help="Output path for invoice in CSV format." + help="Output path for invoice in CSV format.", ) args = parser.parse_args() @@ -176,8 +181,9 @@ def main(): dump_file = fetch.convert_mysqldump_to_sqlite(dump_file) if not dump_file: - raise Exception("Must provide either --sql_dump_file" - "or --download_dump_from_s3.") + raise Exception( + "Must provide either --sql_dump_file" "or --download_dump_from_s3." + ) coldfront_data_file = args.coldfront_data_file if args.download_coldfront_data: diff --git a/src/openstack_billing_db/model.py b/src/openstack_billing_db/model.py index 283b415..2cb003c 100644 --- a/src/openstack_billing_db/model.py +++ b/src/openstack_billing_db/model.py @@ -1,6 +1,5 @@ import json -from abc import ABC, abstractmethod -import math +from abc import abstractmethod import datetime from dataclasses import dataclass from dataclasses_json import dataclass_json @@ -8,7 +7,6 @@ from typing import Optional - @dataclass_json() @dataclass() class Flavor(object): @@ -22,10 +20,12 @@ class Flavor(object): def service_units(self): if "gpu" not in self.name: # 1 CPU SU = 0 GPU, 1 CPU, 4 GB RAM, 20 GB - return int(max( - self.vcpus, - self.memory / 4096, - )) + return int( + max( + self.vcpus, + self.memory / 4096, + ) + ) else: # The flavor for 2 SUs of V100 is inconsistent with previous # naming scheme. @@ -116,7 +116,8 @@ def get_runtime_during(self, start_time, end_time): # Count stopped time from last known stop. if last_stop: runtime.total_seconds_stopped += ( - last_start - last_stop).total_seconds() + last_start - last_stop + ).total_seconds() last_stop = None # Some deleted instances do not have a delete event, they do @@ -130,7 +131,8 @@ def get_runtime_during(self, start_time, end_time): # Count running time from last known start. if last_start: runtime.total_seconds_running += ( - last_stop - last_start).total_seconds() + last_stop - last_start + ).total_seconds() last_start = None if event.name == "delete": @@ -175,11 +177,10 @@ def projects(self) -> list[Project]: class Database(BaseDatabase): - def __init__(self, start, sql_dump_location: str): self.db_nova = sqlite3.connect(":memory:") self.db_nova.row_factory = sqlite3.Row - with open(sql_dump_location, 'r') as sql: + with open(sql_dump_location, "r") as sql: self.db_nova.executescript(sql.read()) self.start = start @@ -196,21 +197,21 @@ def get_events(self, instance_uuid) -> list[InstanceEvent]: cursor = self.db_nova.cursor() cursor.execute( f"select created_at, action, message from instance_actions where" - f" instance_uuid = \"{instance_uuid}\" order by created_at" + f' instance_uuid = "{instance_uuid}" order by created_at' ) return [ InstanceEvent( - time=event["created_at"], - name=event["action"], - message=event["message"] - ) for event in cursor.fetchall() + time=event["created_at"], name=event["action"], message=event["message"] + ) + for event in cursor.fetchall() ] def get_instances(self, project) -> list[Instance]: instances = [] cursor = self.db_nova.cursor() - cursor.execute(f""" + cursor.execute( + f""" select instances.uuid, hostname, @@ -225,7 +226,8 @@ def get_instances(self, project) -> list[Instance]: instances.project_id = "{project}" and (instances.deleted_at > "{self.start.isoformat()}" or instances.deleted = 0) - """) + """ + ) for instance in cursor.fetchall(): pci_info = json.loads(instance["pci_requests"]) @@ -254,7 +256,7 @@ def get_instances(self, project) -> list[Instance]: if pci_name not in ["a100", "a100-sxm4", "v100", "k80"]: raise Exception(f"Invalid pci_name {pci_name}.") - count = pci_info[0]['count'] + count = pci_info[0]["count"] su_name = f"gpu-{pci_name}.{count}" flavor = Flavor( @@ -279,8 +281,6 @@ def get_projects(self) -> list[Project]: cursor = self.db_nova.cursor() cursor.execute("select distinct project_id from instances") return [ - Project( - uuid=project[0], - instances=self.get_instances(project[0]) - ) for project in cursor.fetchall() + Project(uuid=project[0], instances=self.get_instances(project[0])) + for project in cursor.fetchall() ] diff --git a/src/openstack_billing_db/tests/unit/test_instance.py b/src/openstack_billing_db/tests/unit/test_instance.py index 3c75d26..c5753d0 100644 --- a/src/openstack_billing_db/tests/unit/test_instance.py +++ b/src/openstack_billing_db/tests/unit/test_instance.py @@ -3,13 +3,7 @@ from openstack_billing_db.model import Instance, InstanceEvent, Flavor -FLAVORS = { - 1: Flavor(id=1, - name="TestFlavor", - vcpus=1, - memory=4096, - storage=10) -} +FLAVORS = {1: Flavor(id=1, name="TestFlavor", vcpus=1, memory=4096, storage=10)} MINUTE = 60 HOUR = 60 * MINUTE @@ -21,16 +15,15 @@ def test_instance_simple_runtime(): time = datetime(year=2000, month=1, day=2, hour=0, minute=0, second=0) events = [ InstanceEvent(time=time, name="create", message=""), - InstanceEvent(time=time + timedelta(minutes=30), name="delete", message="") + InstanceEvent(time=time + timedelta(minutes=30), name="delete", message=""), ] - i = Instance(uuid=uuid.uuid4().hex, - name=uuid.uuid4().hex, - flavor=FLAVORS[1], - events=events) + i = Instance( + uuid=uuid.uuid4().hex, name=uuid.uuid4().hex, flavor=FLAVORS[1], events=events + ) r = i.get_runtime_during( datetime(year=2000, month=1, day=1, hour=0, minute=0, second=0), - datetime(year=2000, month=2, day=2, hour=0, minute=0, second=0) + datetime(year=2000, month=2, day=2, hour=0, minute=0, second=0), ) assert r.total_seconds_running == 30 * MINUTE assert r.total_seconds_stopped == 0 @@ -40,16 +33,15 @@ def test_instance_runtime_started_before(): time = datetime(year=1991, month=1, day=2, hour=0, minute=0, second=0) events = [ InstanceEvent(time=time, name="create", message=""), - InstanceEvent(time=time + timedelta(minutes=30), name="delete", message="") + InstanceEvent(time=time + timedelta(minutes=30), name="delete", message=""), ] - i = Instance(uuid=uuid.uuid4().hex, - name=uuid.uuid4().hex, - flavor=FLAVORS[1], - events=events) + i = Instance( + uuid=uuid.uuid4().hex, name=uuid.uuid4().hex, flavor=FLAVORS[1], events=events + ) r = i.get_runtime_during( datetime(year=2000, month=1, day=1, hour=0, minute=0, second=0), - datetime(year=2000, month=2, day=2, hour=0, minute=0, second=0) + datetime(year=2000, month=2, day=2, hour=0, minute=0, second=0), ) assert r.total_seconds_running == 0 assert r.total_seconds_stopped == 0 @@ -57,17 +49,14 @@ def test_instance_runtime_started_before(): def test_instance_runtime_started_before_still_running(): time = datetime(year=1991, month=1, day=2, hour=0, minute=0, second=0) - events = [ - InstanceEvent(time=time, name="create", message="") - ] - i = Instance(uuid=uuid.uuid4().hex, - name=uuid.uuid4().hex, - flavor=FLAVORS[1], - events=events) + events = [InstanceEvent(time=time, name="create", message="")] + i = Instance( + uuid=uuid.uuid4().hex, name=uuid.uuid4().hex, flavor=FLAVORS[1], events=events + ) r = i.get_runtime_during( datetime(year=2000, month=1, day=1, hour=0, minute=0, second=0), - datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0) + datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0), ) assert r.total_seconds_running == MONTH assert r.total_seconds_stopped == 0 @@ -79,16 +68,17 @@ def test_instance_runtime_stopped_and_started(): InstanceEvent(time=time, name="create", message=""), InstanceEvent(time=time + timedelta(minutes=40), name="stop", message=""), InstanceEvent(time=time + timedelta(days=1), name="start", message=""), - InstanceEvent(time=time + timedelta(days=1, minutes=40), name="delete", message="") + InstanceEvent( + time=time + timedelta(days=1, minutes=40), name="delete", message="" + ), ] - i = Instance(uuid=uuid.uuid4().hex, - name=uuid.uuid4().hex, - flavor=FLAVORS[1], - events=events) + i = Instance( + uuid=uuid.uuid4().hex, name=uuid.uuid4().hex, flavor=FLAVORS[1], events=events + ) r = i.get_runtime_during( datetime(year=2000, month=1, day=1, hour=0, minute=0, second=0), - datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0) + datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0), ) assert r.total_seconds_running == (40 * MINUTE) + (40 * MINUTE) assert r.total_seconds_stopped == DAY - (40 * MINUTE) @@ -99,16 +89,18 @@ def test_instance_no_delete_action(): events = [ InstanceEvent(time=time, name="create", message=""), ] - i = Instance(uuid=uuid.uuid4().hex, - name=uuid.uuid4().hex, - flavor=FLAVORS[1], - events=events, - deleted_at=time + timedelta(days=1, minutes=40)) + i = Instance( + uuid=uuid.uuid4().hex, + name=uuid.uuid4().hex, + flavor=FLAVORS[1], + events=events, + deleted_at=time + timedelta(days=1, minutes=40), + ) # In current billing cycle r = i.get_runtime_during( datetime(year=2000, month=1, day=1, hour=0, minute=0, second=0), - datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0) + datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0), ) assert r.total_seconds_running == DAY + (40 * MINUTE) assert r.total_seconds_stopped == 0 @@ -116,14 +108,14 @@ def test_instance_no_delete_action(): # Outside billing cycles r = i.get_runtime_during( datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0), - datetime(year=2000, month=3, day=1, hour=0, minute=0, second=0) + datetime(year=2000, month=3, day=1, hour=0, minute=0, second=0), ) assert r.total_seconds_running == 0 assert r.total_seconds_stopped == 0 r = i.get_runtime_during( datetime(year=1999, month=11, day=1, hour=0, minute=0, second=0), - datetime(year=2000, month=12, day=1, hour=0, minute=0, second=0) + datetime(year=2000, month=12, day=1, hour=0, minute=0, second=0), ) assert r.total_seconds_running == 0 assert r.total_seconds_stopped == 0 @@ -135,15 +127,17 @@ def test_instance_no_delete_action_stopped(): InstanceEvent(time=time, name="create", message=""), InstanceEvent(time=time + timedelta(minutes=40), name="stop", message=""), ] - i = Instance(uuid=uuid.uuid4().hex, - name=uuid.uuid4().hex, - flavor=FLAVORS[1], - events=events, - deleted_at=time + timedelta(days=1, minutes=40)) + i = Instance( + uuid=uuid.uuid4().hex, + name=uuid.uuid4().hex, + flavor=FLAVORS[1], + events=events, + deleted_at=time + timedelta(days=1, minutes=40), + ) r = i.get_runtime_during( datetime(year=2000, month=1, day=1, hour=0, minute=0, second=0), - datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0) + datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0), ) assert r.total_seconds_running == 40 * MINUTE assert r.total_seconds_stopped == DAY @@ -156,16 +150,17 @@ def test_instance_no_delete_action_stopped_restarted(): InstanceEvent(time=time + timedelta(minutes=40), name="stop", message=""), InstanceEvent(time=time + timedelta(days=1), name="start", message=""), ] - i = Instance(uuid=uuid.uuid4().hex, - name=uuid.uuid4().hex, - flavor=FLAVORS[1], - events=events, - deleted_at=time + timedelta(days=1, minutes=40)) + i = Instance( + uuid=uuid.uuid4().hex, + name=uuid.uuid4().hex, + flavor=FLAVORS[1], + events=events, + deleted_at=time + timedelta(days=1, minutes=40), + ) r = i.get_runtime_during( datetime(year=2000, month=1, day=1, hour=0, minute=0, second=0), - datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0) + datetime(year=2000, month=2, day=1, hour=0, minute=0, second=0), ) assert r.total_seconds_running == (40 * MINUTE) + (40 * MINUTE) assert r.total_seconds_stopped == DAY - (40 * MINUTE) - From c6e96aa6caef6f6619ceefd3a7f45bda819641df Mon Sep 17 00:00:00 2001 From: Kristi Nikolla Date: Tue, 23 Apr 2024 18:29:36 -0400 Subject: [PATCH 2/2] Update black repo and version --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d5955c..a4513b3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: detect-private-key - id: check-yaml - - repo: https://github.com/ambv/black - rev: 23.1.0 + - repo: https://github.com/psf/black + rev: 24.4.0 hooks: - id: black