From c32c1925c52fc7c0f807e8c5854bd10a44d877c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Mon, 23 Oct 2023 16:34:10 +0200 Subject: [PATCH] Add: Implement organization based GitHub billing API This API is very limited but easy to implement. --- pontos/github/api/api.py | 8 +++ pontos/github/api/billing.py | 103 ++++++++++++++++++++++++++++ pontos/github/models/billing.py | 80 +++++++++++++++++++++ tests/github/api/test_billing.py | 68 ++++++++++++++++++ tests/github/models/test_billing.py | 66 ++++++++++++++++++ 5 files changed, 325 insertions(+) create mode 100644 pontos/github/api/billing.py create mode 100644 pontos/github/models/billing.py create mode 100644 tests/github/api/test_billing.py create mode 100644 tests/github/models/test_billing.py diff --git a/pontos/github/api/api.py b/pontos/github/api/api.py index d8dd7ebc7..4b8585336 100644 --- a/pontos/github/api/api.py +++ b/pontos/github/api/api.py @@ -22,6 +22,7 @@ import httpx from pontos.github.api.artifacts import GitHubAsyncRESTArtifacts +from pontos.github.api.billing import GitHubAsyncRESTBilling from pontos.github.api.branch import GitHubAsyncRESTBranches from pontos.github.api.client import GitHubAsyncRESTClient from pontos.github.api.code_scanning import GitHubAsyncRESTCodeScanning @@ -89,6 +90,13 @@ def artifacts(self) -> GitHubAsyncRESTArtifacts: """ return GitHubAsyncRESTArtifacts(self._client) + @property + def billing(self) -> GitHubAsyncRESTBilling: + """ + Billing related API + """ + return GitHubAsyncRESTBilling(self._client) + @property def branches(self) -> GitHubAsyncRESTBranches: """ diff --git a/pontos/github/api/billing.py b/pontos/github/api/billing.py new file mode 100644 index 000000000..91c45bdba --- /dev/null +++ b/pontos/github/api/billing.py @@ -0,0 +1,103 @@ +# SPDX-FileCopyrightText: 2023 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from pontos.github.api.client import GitHubAsyncREST +from pontos.github.models.billing import ( + ActionsBilling, + PackagesBilling, + StorageBilling, +) + + +class GitHubAsyncRESTBilling(GitHubAsyncREST): + async def actions(self, organization: str) -> ActionsBilling: + """ + Get the summary of the free and paid GitHub Actions minutes used + + https://docs.github.com/en/rest/billing/billing#get-github-actions-billing-for-an-organization + + Args: + organization: The organization name + + Raises: + HTTPStatusError: A httpx.HTTPStatusError is raised if the request + failed. + + Returns: + Information about the Actions billing + + Example: + .. code-block:: python + + from pontos.github.api import GitHubAsyncRESTApi + + async with GitHubAsyncRESTApi(token) as api: + billing = await api.billing.actions("foo") + print(billing) + """ + api = f"/orgs/{organization}/settings/billing/actions" + response = await self._client.get(api) + response.raise_for_status() + return ActionsBilling.from_dict(response.json()) + + async def packages(self, organization: str) -> PackagesBilling: + """ + Get the free and paid storage used for GitHub Packages in gigabytes + + https://docs.github.com/en/rest/billing/billing#get-github-packages-billing-for-an-organization + + Args: + organization: The organization name + + Raises: + HTTPStatusError: A httpx.HTTPStatusError is raised if the request + failed. + + Returns: + Information about the Packages billing + + Example: + .. code-block:: python + + from pontos.github.api import GitHubAsyncRESTApi + + async with GitHubAsyncRESTApi(token) as api: + billing = await api.billing.packages("foo") + print(billing) + """ + api = f"/orgs/{organization}/settings/billing/packages" + response = await self._client.get(api) + response.raise_for_status() + return PackagesBilling.from_dict(response.json()) + + async def storage(self, organization: str) -> StorageBilling: + """ + Get the estimated paid and estimated total storage used for GitHub + Actions and GitHub Packages + + https://docs.github.com/en/rest/billing/billing#get-shared-storage-billing-for-an-organization + + Args: + organization: The organization name + + Raises: + HTTPStatusError: A httpx.HTTPStatusError is raised if the request + failed. + + Returns: + Information about the storage billing + + Example: + .. code-block:: python + + from pontos.github.api import GitHubAsyncRESTApi + + async with GitHubAsyncRESTApi(token) as api: + billing = await api.billing.storage("foo") + print(billing) + """ + api = f"/orgs/{organization}/settings/billing/shared-storage" + response = await self._client.get(api) + response.raise_for_status() + return StorageBilling.from_dict(response.json()) diff --git a/pontos/github/models/billing.py b/pontos/github/models/billing.py new file mode 100644 index 000000000..36ac81c69 --- /dev/null +++ b/pontos/github/models/billing.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: 2023 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from dataclasses import dataclass +from typing import Optional + +from pontos.github.models.base import GitHubModel + + +@dataclass +class ActionsMinutesUsedBreakdown(GitHubModel): + """ + Attributes: + UBUNTU: Total minutes used on Ubuntu runner machines + MACOS: Total minutes used on macOS runner machines + WINDOWS: Total minutes used on Windows runner machines + total: Total minutes used on all runner machines + """ + + UBUNTU: Optional[int] = None + MACOS: Optional[int] = None + WINDOWS: Optional[int] = None + total: Optional[int] = None + + +@dataclass +class ActionsBilling(GitHubModel): + """ + Billing Information for using GitHub Actions + + Attributes: + total_minutes_used: The sum of the free and paid GitHub Actions minutes + used + total_paid_minutes_used: The total paid GitHub Actions minutes used + included_minutes: The amount of free GitHub Actions minutes available + minutes_used_breakdown: + """ + + total_minutes_used: int + total_paid_minutes_used: int + included_minutes: int + minutes_used_breakdown: ActionsMinutesUsedBreakdown + + +@dataclass +class PackagesBilling(GitHubModel): + """ + Billing Information for using GitHub Packages + + Attributes: + total_gigabytes_bandwidth_used: Sum of the free and paid storage space + (GB) for GitHub Packages + total_paid_gigabytes_bandwidth_used: Total paid storage space (GB) for + GitHub Packages + included_gigabytes_bandwidth: Free storage space (GB) for GitHub + Packages + """ + + total_gigabytes_bandwidth_used: int + total_paid_gigabytes_bandwidth_used: int + included_gigabytes_bandwidth: int + + +@dataclass +class StorageBilling(GitHubModel): + """ + Billing Information for using GitHub storage + + Attributes: + days_left_in_billing_cycle: Numbers of days left in billing cycle + estimated_paid_storage_for_month: Estimated storage space (GB) used in + billing cycle + estimated_storage_for_month: Estimated sum of free and paid storage + space (GB) used in billing cycle + """ + + days_left_in_billing_cycle: int + estimated_paid_storage_for_month: int + estimated_storage_for_month: int diff --git a/tests/github/api/test_billing.py b/tests/github/api/test_billing.py new file mode 100644 index 000000000..1de0a5edb --- /dev/null +++ b/tests/github/api/test_billing.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: 2023 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# ruff: noqa:E501 + +from pontos.github.api.billing import GitHubAsyncRESTBilling +from tests.github.api import GitHubAsyncRESTTestCase, create_response + + +class GitHubAsyncRESTBillingTestCase(GitHubAsyncRESTTestCase): + api_cls = GitHubAsyncRESTBilling + + async def test_actions(self): + response = create_response() + response.json.return_value = { + "total_minutes_used": 305, + "total_paid_minutes_used": 0, + "included_minutes": 3000, + "minutes_used_breakdown": { + "UBUNTU": 205, + "MACOS": 10, + "WINDOWS": 90, + }, + } + self.client.get.return_value = response + + billing = await self.api.actions("foo") + + self.client.get.assert_awaited_once_with( + "/orgs/foo/settings/billing/actions", + ) + + self.assertEqual(billing.total_minutes_used, 305) + + async def test_packages(self): + response = create_response() + response.json.return_value = { + "total_gigabytes_bandwidth_used": 50, + "total_paid_gigabytes_bandwidth_used": 40, + "included_gigabytes_bandwidth": 10, + } + self.client.get.return_value = response + + billing = await self.api.packages("foo") + + self.client.get.assert_awaited_once_with( + "/orgs/foo/settings/billing/packages", + ) + + self.assertEqual(billing.total_gigabytes_bandwidth_used, 50) + + async def test_storage(self): + response = create_response() + response.json.return_value = { + "days_left_in_billing_cycle": 20, + "estimated_paid_storage_for_month": 15, + "estimated_storage_for_month": 40, + } + self.client.get.return_value = response + + billing = await self.api.storage("foo") + + self.client.get.assert_awaited_once_with( + "/orgs/foo/settings/billing/shared-storage", + ) + + self.assertEqual(billing.days_left_in_billing_cycle, 20) diff --git a/tests/github/models/test_billing.py b/tests/github/models/test_billing.py new file mode 100644 index 000000000..67d6bb639 --- /dev/null +++ b/tests/github/models/test_billing.py @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: 2023 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# ruff: noqa:E501 + +import unittest + +from pontos.github.models.billing import ( + ActionsBilling, + PackagesBilling, + StorageBilling, +) + + +class ActionsBillingTestCase(unittest.TestCase): + def test_from_dict(self): + billing = ActionsBilling.from_dict( + { + "total_minutes_used": 305, + "total_paid_minutes_used": 0, + "included_minutes": 3000, + "minutes_used_breakdown": { + "UBUNTU": 205, + "MACOS": 10, + "WINDOWS": 90, + }, + } + ) + + self.assertEqual(billing.total_minutes_used, 305) + self.assertEqual(billing.total_paid_minutes_used, 0) + self.assertEqual(billing.included_minutes, 3000) + self.assertEqual(billing.minutes_used_breakdown.UBUNTU, 205) + self.assertEqual(billing.minutes_used_breakdown.MACOS, 10) + self.assertEqual(billing.minutes_used_breakdown.WINDOWS, 90) + self.assertIsNone(billing.minutes_used_breakdown.total) + + +class PackagesBillingTestCase(unittest.TestCase): + def test_from_dict(self): + billing = PackagesBilling.from_dict( + { + "total_gigabytes_bandwidth_used": 50, + "total_paid_gigabytes_bandwidth_used": 40, + "included_gigabytes_bandwidth": 10, + } + ) + + self.assertEqual(billing.total_gigabytes_bandwidth_used, 50) + self.assertEqual(billing.total_paid_gigabytes_bandwidth_used, 40) + self.assertEqual(billing.included_gigabytes_bandwidth, 10) + + +class StorageBillingTestCase(unittest.TestCase): + def test_from_dict(self): + billing = StorageBilling.from_dict( + { + "days_left_in_billing_cycle": 20, + "estimated_paid_storage_for_month": 15, + "estimated_storage_for_month": 40, + } + ) + self.assertEqual(billing.days_left_in_billing_cycle, 20) + self.assertEqual(billing.estimated_paid_storage_for_month, 15) + self.assertEqual(billing.estimated_storage_for_month, 40)