Skip to content

Commit

Permalink
Add: Implement organization based GitHub billing API
Browse files Browse the repository at this point in the history
This API is very limited but easy to implement.
  • Loading branch information
bjoernricks committed Oct 24, 2023
1 parent 472ec14 commit c32c192
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 0 deletions.
8 changes: 8 additions & 0 deletions pontos/github/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down
103 changes: 103 additions & 0 deletions pontos/github/api/billing.py
Original file line number Diff line number Diff line change
@@ -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())
80 changes: 80 additions & 0 deletions pontos/github/models/billing.py
Original file line number Diff line number Diff line change
@@ -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
68 changes: 68 additions & 0 deletions tests/github/api/test_billing.py
Original file line number Diff line number Diff line change
@@ -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)
66 changes: 66 additions & 0 deletions tests/github/models/test_billing.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit c32c192

Please sign in to comment.