diff --git a/atlasapi/__init__.py b/atlasapi/__init__.py index 98bc840..e679a57 100644 --- a/atlasapi/__init__.py +++ b/atlasapi/__init__.py @@ -15,4 +15,4 @@ # __init__.py # Version of the realpython-reader package -__version__ = "2.0.7" +__version__ = "3.0.1b1" diff --git a/atlasapi/atlas.py b/atlasapi/atlas.py index 81cd37b..71a4465 100644 --- a/atlasapi/atlas.py +++ b/atlasapi/atlas.py @@ -44,6 +44,7 @@ from requests.auth import HTTPBasicAuth, HTTPDigestAuth from atlasapi.events_event_types import AtlasEventTypes from atlasapi.events import AtlasEvent +from atlasapi.invoices_pydantic import ApiInvoiceView import gzip logger = logging.getLogger('Atlas') @@ -81,6 +82,7 @@ def __init__(self, user: str, password: str, group: str = None, self.CloudBackups = Atlas._CloudBackups(self) self.Projects = Atlas._Projects(self) self.Organizations = Atlas._Organizations(self) + self.Invoices = Atlas._Invoices(self) if not self.group: self.logger.warning("Note! The Atlas client has been initialized without a Group/Project, some endpoints" "will not function without a Group or project.") @@ -1478,7 +1480,6 @@ def create_snapshot_for_cluster(self, cluster_name: str, retention_days: int = 7 logger.warning(f'Create response: {response}') return CloudBackupSnapshot(response) - def get_backup_snapshots_for_cluster(self, cluster_name: str) -> Iterable[CloudBackupSnapshot]: """Get backup snapshots for a cluster. @@ -2096,6 +2097,51 @@ def get_all_projects_for_org(self, org_id: str) -> Iterable[Project]: for each_project in page.get("results"): yield Project.from_dict(each_project) + class _Invoices: + """INvoices API + + see: https://docs.atlas.mongodb.com/reference/api/invoices/ + + Constructor + + Args: + atlas (Atlas): Atlas instance + """ + + def __init__(self, atlas): + self.atlas = atlas + + def count_for_org_id(self, org_id: str) -> int: + uri = Settings.BASE_URL + Settings.api_resources["Invoices"][ + "Get All Invoices for One Organization"].format(ORG_ID=org_id) + response = self.atlas.network.get(uri=uri, params={'includeCount': True}) + for page in response: + logger.info(f"Total of {page.get('totalCount')} invoices to be returned") + return page.get('totalCount') + + def get_all_for_org_id(self, org_id: str): + uri = Settings.BASE_URL + Settings.api_resources["Invoices"][ + "Get All Invoices for One Organization"].format(ORG_ID=org_id) + response = self.atlas.network.get(uri=uri) + for page in response: + logger.info(f"Total of {page.get('totalCount')} invoices to be returned") + for each_invoice in page.get("results"): + yield ApiInvoiceView.parse_obj(each_invoice) + + def get_pending_for_org_id(self, org_id: str) -> ApiInvoiceView: + uri = Settings.BASE_URL + Settings.api_resources["Invoices"][ + "Get All Pending Invoices for One Organization"].format(ORG_ID=org_id) + response = self.atlas.network.get(uri=uri) + for page in response: + return ApiInvoiceView.parse_obj(page) + + def get_single_invoice_for_org(self, org_id: str, invoice_id=str) -> ApiInvoiceView: + uri = Settings.BASE_URL + Settings.api_resources["Invoices"][ + "Get One Organization Invoice"].format(ORG_ID=org_id, INVOICE_ID=invoice_id) + response = self.atlas.network.get(uri=uri) + for page in response: + return ApiInvoiceView.parse_obj(page) + class AtlasPagination: """Atlas Pagination Generic Implementation diff --git a/atlasapi/invoices_pydantic.py b/atlasapi/invoices_pydantic.py new file mode 100644 index 0000000..f68e82b --- /dev/null +++ b/atlasapi/invoices_pydantic.py @@ -0,0 +1,313 @@ +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from ipaddress import IPv4Address +from typing import Any, Dict, List, Optional, Union +from uuid import UUID + +from pydantic import BaseModel, EmailStr, Extra, Field, confloat, conint, constr + + +class InvoiceStatus(Enum): + INVOICED = "INVOICED" + PAID = "PAID" + PREPAID = "PREPAID" + FREE = "FREE" + PENDING = "PENDING" + FORGIVEN = "FORGIVEN" + FAILED = "FAILED" + CLOSED = "CLOSED" + + + + +class Link(BaseModel): + href: Optional[str] = Field( + None, + description='Uniform Resource Locator (URL) that points another API resource to which this response has some relationship. This URL often begins with `https://mms.mongodb.com`.', + example='https://mms.mongodb.com/api/atlas/v1.0/groups/{groupId}/serverless/{instanceName1}/backup/snapshots', + ) + rel: Optional[str] = Field( + None, + description='Uniform Resource Locator (URL) that defines the semantic relationship between this resource and another API resource. This URL often begins with `https://mms.mongodb.com`.', + example='https://mms.mongodb.com/snapshots', + ) + + +class LinkAtlas(BaseModel): + href: Optional[str] = Field( + None, + description='Uniform Resource Locator (URL) that points another API resource to which this response has some relationship. This URL often begins with `https://mms.mongodb.com`.', + example='https://mms.mongodb.com/api/atlas/v1.0/groups/{groupId}/serverless/{instanceName1}/backup/snapshots', + ) + rel: Optional[str] = Field( + None, + description='Uniform Resource Locator (URL) that defines the semantic relationship between this resource and another API resource. This URL often begins with `https://mms.mongodb.com`.', + example='https://mms.mongodb.com/snapshots', + ) + + +class ApiPaymentView(BaseModel): + """ + Funds transferred to MongoDB to cover the specified service in this invoice. + """ + + amount_billed_cents: Optional[int] = Field( + None, + alias='amountBilledCents', + description='Sum of services that the specified organization consumed in the period covered in this invoice. This parameter expresses its value in cents (100ths of one US Dollar) and calculates its value as **subtotalCents** + **salesTaxCents** - **startingBalanceCents**.', + ) + amount_paid_cents: Optional[int] = Field( + None, + alias='amountPaidCents', + description='Sum that the specified organization paid toward the associated invoice. This parameter expresses its value in cents (100ths of one US Dollar).', + ) + created: Optional[datetime] = Field( + None, + description='Date and time when the customer made this payment attempt. This parameter expresses its value in the ISO 8601 timestamp format in UTC.', + ) + id: Optional[ + constr(regex=r'^([a-f0-9]{24})$', min_length=24, max_length=24) + ] = Field( + None, + description='Unique 24-hexadecimal digit string that identifies this payment toward the associated invoice.', + example='6fb669797183aa180bc5c09a', + ) + sales_tax_cents: Optional[int] = Field( + None, + alias='salesTaxCents', + description='Sum of sales tax applied to this invoice. This parameter expresses its value in cents (100ths of one US Dollar).', + ) + status_name: Optional[str] = Field( + None, + alias='statusName', + description="Phase of payment processing for the associated invoice when you made this request.\n\nThese phases include:\n\n| Phase Value | Reason |\n|---|---|\n| `CANCELLED` | Customer or MongoDB cancelled the payment. |\n| `ERROR` | Issue arose when attempting to complete payment. |\n| `FAILED` | MongoDB tried to charge the credit card without success. |\n| `FAILED_AUTHENTICATION` | Strong Customer Authentication has failed. Confirm that your payment method is authenticated. |\n| `FORGIVEN` | Customer initiated payment which MongoDB later forgave. |\n| `INVOICED` | MongoDB issued an invoice that included this line item. |\n| `NEW` | Customer provided a method of payment, but MongoDB hasn't tried to charge the credit card. |\n| `PAID` | Customer submitted a successful payment. |\n| `PARTIAL_PAID` | Customer paid for part of this line item. |\n", + ) + subtotal_cents: Optional[int] = Field( + None, + alias='subtotalCents', + description='Sum of all positive invoice line items contained in this invoice. This parameter expresses its value in cents (100ths of one US Dollar).', + ) + updated: Optional[datetime] = Field( + None, + description='Date and time when the customer made an update to this payment attempt. This parameter expresses its value in the ISO 8601 timestamp format in UTC.', + ) + + +class ApiRefundView(BaseModel): + """ + One payment that MongoDB returned to the organization for this invoice. + """ + + amount_cents: Optional[int] = Field( + None, + alias='amountCents', + description='Sum of the funds returned to the specified organization expressed in cents (100th of US Dollar).', + ) + created: Optional[datetime] = Field( + None, + description='Date and time when MongoDB Cloud created this refund. This parameter expresses its value in the ISO 8601 timestamp format in UTC.', + ) + payment_id: Optional[ + constr(regex=r'^([a-f0-9]{24})$', min_length=24, max_length=24) + ] = Field( + None, + alias='paymentId', + description='Unique 24-hexadecimal digit string that identifies the payment that the organization had made.', + ) + reason: Optional[str] = Field( + None, + description='Justification that MongoDB accepted to return funds to the organization.', + ) + + +class ApiLineItemView(BaseModel): + """ + One service included in this invoice. + """ + + cluster_name: Optional[ + constr( + regex=r'^([a-zA-Z0-9]([a-zA-Z0-9-]){0,21}(?=3.7', packages=find_packages(exclude=("tests",)), install_requires=['requests', 'python-dateutil', 'isodate', 'future', 'pytz','coolname', - 'humanfriendly', 'pydantic', 'humps', 'nose'], + 'humanfriendly', 'pydantic', 'pyhumps', 'nose'], setup_requires=['wheel'], # Metadata author="Matthew G. Monteleone", @@ -22,7 +22,7 @@ # 3 - Alpha # 4 - Beta # 5 - Production/Stable - 'Development Status :: 5 - Production/Stable', + 'Development Status :: 4 - Beta', # Indicate who your project is intended for 'Intended Audience :: Developers', diff --git a/tests/test_invoices.py b/tests/test_invoices.py new file mode 100644 index 0000000..a400a6b --- /dev/null +++ b/tests/test_invoices.py @@ -0,0 +1,66 @@ +""" +Nose2 Unit Tests for the clusters module. + + +""" +import datetime +from pprint import pprint +from os import environ, getenv +from atlasapi.atlas import Atlas +from atlasapi.projects import Project, ProjectSettings +from atlasapi.teams import TeamRoles +from atlasapi.atlas_users import AtlasUser +from atlasapi.invoices_pydantic import ApiInvoiceView, ApiRefundView, ApiPaymentView, ApiLineItemView, InvoiceStatus +from json import dumps +from tests import BaseTests +import logging +from time import sleep + +logger = logging.getLogger('test') + + +class InvoiceTests(BaseTests): + + def test_01_get_invoices_for_org(self): + for each in self.a.Organizations.organizations: + org_id = each.id + expected_count = self.a.Invoices.count_for_org_id(org_id) + invoices = 0 + for each_item in self.a.Invoices.get_all_for_org_id(org_id): + invoices += 1 + self.assertIsInstance(each_item, ApiInvoiceView) + self.assertIsInstance(each_item.status_name, InvoiceStatus) + print(f'Checked invoice # {invoices}') + print(f'✅Checked {invoices} invoices for org {each.name}') + print(f'💎Expected {expected_count} invoices!') + self.assertEqual(expected_count, invoices, + "The number of returned invoices should match the expencted number" + "of invoices sent in the totalCount response") + break + test_01_get_invoices_for_org.basic = True + + def test_02_get_one_invoice_for_org(self): + for each in self.a.Organizations.organizations: + org_id = each.id + for each_item in self.a.Invoices.get_all_for_org_id(org_id): + invoice_id = each_item.id + detail_invoice: ApiInvoiceView = self.a.Invoices.get_single_invoice_for_org(org_id=org_id, + invoice_id=invoice_id) + pprint(detail_invoice) + break + break + test_02_get_one_invoice_for_org.basic = True + + def test_03_get_pending_invoice_for_org(self): + for each in self.a.Organizations.organizations: + org_id = each.id + invoices = 0 + pending_invoice = self.a.Invoices.get_pending_for_org_id(org_id) + pprint(pending_invoice) + self.assertIsInstance(pending_invoice, ApiInvoiceView) + self.assertIsInstance(pending_invoice.status_name, InvoiceStatus) + for each_line in pending_invoice.line_items: + self.assertIsInstance(each_line, ApiLineItemView) + + break + test_03_get_pending_invoice_for_org.basic = True \ No newline at end of file