Repository: linode/linode_api4-python (language: python)

Issue:
project: DC-Specific Pricing
## 📝 Description

This PR will merge the DC-specific pricing project into the `dev` branch.

## ✔️ How to Test

```
pytest test/unit
```



Discussion / hints:

Fix patch:
diff --git a/Makefile b/Makefile
index 1cb2da01b..9ed3e4eb8 100644
--- a/Makefile
+++ b/Makefile
@@ -28,7 +28,7 @@ release: build
 	twine upload dist/*
 
 @PHONEY: install
-install: clean
+install: clean requirements
 	python3 -m pip install .
 
 @PHONEY: requirements
diff --git a/linode_api4/objects/linode.py b/linode_api4/objects/linode.py
index 3bc402b0d..d928b31b6 100644
--- a/linode_api4/objects/linode.py
+++ b/linode_api4/objects/linode.py
@@ -232,6 +232,7 @@ class Type(Base):
         "label": Property(),
         "network_out": Property(),
         "price": Property(),
+        "region_prices": Property(),
         "addons": Property(),
         "memory": Property(),
         "transfer": Property(),


Test patch:
diff --git a/.github/workflows/e2e-test-pr.yml b/.github/workflows/e2e-test-pr.yml
index af7e1bcaf..00391cf19 100644
--- a/.github/workflows/e2e-test-pr.yml
+++ b/.github/workflows/e2e-test-pr.yml
@@ -78,11 +78,37 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
-      - run: make INTEGRATION_TEST_PATH="${{ inputs.test_path }}" testint
-        if: ${{ steps.validate-tests.outputs.match == '' }}
+      - name: Run Integration tests
+        run: |
+          timestamp=$(date +'%Y%m%d%H%M')
+          report_filename="${timestamp}_sdk_test_report.xml"
+          status=0
+          if ! python3 -m pytest test/integration/${INTEGRATION_TEST_PATH} --junitxml="${report_filename}"; then
+            echo "Tests failed, but attempting to upload results anyway"
+          fi
         env:
           LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }}
 
+      - name: Set release version env
+        run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
+
+      - name: Add additional information to XML report
+        run: |
+          filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$') 
+          python test/script/add_to_xml_test_report.py \
+          --branch_name "${{ env.RELEASE_VERSION }}" \
+          --gha_run_id "$GITHUB_RUN_ID" \
+          --gha_run_number "$GITHUB_RUN_NUMBER" \
+          --xmlfile "${filename}"
+
+      - name: Upload test results
+        run: |
+          report_filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$')
+          python3 test/script/test_report_upload_script.py "${report_filename}"
+        env:
+          LINODE_CLI_OBJ_ACCESS_KEY: ${{ secrets.LINODE_CLI_OBJ_ACCESS_KEY }}
+          LINODE_CLI_OBJ_SECRET_KEY: ${{ secrets.LINODE_CLI_OBJ_SECRET_KEY }}
+
       - uses: actions/github-script@v6
         id: update-check-run
         if: ${{ inputs.pull_request_number != '' && fromJson(steps.commit-hash.outputs.data).repository.pullRequest.headRef.target.oid == inputs.sha }}
diff --git a/test/fixtures/account_transfer.json b/test/fixtures/account_transfer.json
new file mode 100644
index 000000000..ce4658a6a
--- /dev/null
+++ b/test/fixtures/account_transfer.json
@@ -0,0 +1,14 @@
+{
+    "quota": 471,
+    "used": 737373,
+    "billable": 0,
+
+    "region_transfers": [
+        {
+            "id": "ap-west",
+            "used": 1,
+            "quota": 5010,
+            "billable": 0
+        }
+    ]
+}
\ No newline at end of file
diff --git a/test/fixtures/linode_types.json b/test/fixtures/linode_types.json
index b270da778..c864082e8 100644
--- a/test/fixtures/linode_types.json
+++ b/test/fixtures/linode_types.json
@@ -1,8 +1,8 @@
 {
-"results": 4,
-"pages": 1,
-"page": 1,
-"data": [
+  "results": 4,
+  "pages": 1,
+  "page": 1,
+  "data": [
     {
       "disk": 20480,
       "memory": 1024,
@@ -12,7 +12,19 @@
           "price": {
             "hourly": 0.003,
             "monthly": 2
-          }
+          },
+          "region_prices": [
+            {
+              "id": "ap-west",
+              "hourly": 0.02,
+              "monthly": 20
+            },
+            {
+              "id": "ap-northeast",
+              "hourly": 0.02,
+              "monthly": 20
+            }
+          ]
         }
       },
       "class": "nanode",
@@ -25,6 +37,18 @@
         "hourly": 0.0075,
         "monthly": 5
       },
+      "region_prices": [
+        {
+          "id": "us-east",
+          "hourly": 0.02,
+          "monthly": 20
+        },
+        {
+          "id": "ap-northeast",
+          "hourly": 0.02,
+          "monthly": 20
+        }
+      ],
       "successor": null
     },
     {
@@ -36,7 +60,19 @@
           "price": {
             "hourly": 0.008,
             "monthly": 5
-          }
+          },
+          "region_prices": [
+            {
+              "id": "ap-west",
+              "hourly": 0.02,
+              "monthly": 20
+            },
+            {
+              "id": "ap-northeast",
+              "hourly": 0.02,
+              "monthly": 20
+            }
+          ]
         }
       },
       "class": "highmem",
@@ -49,6 +85,18 @@
         "hourly": 0.09,
         "monthly": 60
       },
+      "region_prices": [
+        {
+          "id": "us-east",
+          "hourly": 0.02,
+          "monthly": 20
+        },
+        {
+          "id": "ap-northeast",
+          "hourly": 0.02,
+          "monthly": 20
+        }
+      ],
       "successor": null
     },
     {
@@ -60,7 +108,19 @@
           "price": {
             "hourly": 0.004,
             "monthly": 2.5
-          }
+          },
+          "region_prices": [
+            {
+              "id": "ap-west",
+              "hourly": 0.02,
+              "monthly": 20
+            },
+            {
+              "id": "ap-northeast",
+              "hourly": 0.02,
+              "monthly": 20
+            }
+          ]
         }
       },
       "class": "standard",
@@ -73,6 +133,18 @@
         "hourly": 0.015,
         "monthly": 10
       },
+      "region_prices": [
+        {
+          "id": "us-east",
+          "hourly": 0.02,
+          "monthly": 20
+        },
+        {
+          "id": "ap-northeast",
+          "hourly": 0.02,
+          "monthly": 20
+        }
+      ],
       "successor": null
     },
     {
@@ -84,7 +156,19 @@
           "price": {
             "hourly": 0.008,
             "monthly": 5
-          }
+          },
+          "region_prices": [
+            {
+              "id": "ap-west",
+              "hourly": 0.02,
+              "monthly": 20
+            },
+            {
+              "id": "ap-northeast",
+              "hourly": 0.02,
+              "monthly": 20
+            }
+          ]
         }
       },
       "class": "gpu",
@@ -97,7 +181,19 @@
         "hourly": 0.03,
         "monthly": 20
       },
+      "region_prices": [
+        {
+          "id": "us-east",
+          "hourly": 0.02,
+          "monthly": 20
+        },
+        {
+          "id": "ap-northeast",
+          "hourly": 0.02,
+          "monthly": 20
+        }
+      ],
       "successor": null
     }
   ]
-}
+}
\ No newline at end of file
diff --git a/test/integration/conftest.py b/test/integration/conftest.py
index b3fa15fb2..a17a6f847 100644
--- a/test/integration/conftest.py
+++ b/test/integration/conftest.py
@@ -3,9 +3,11 @@
 
 import pytest
 
-from linode_api4.linode_client import LinodeClient, LongviewSubscription
+from linode_api4.linode_client import LinodeClient
 
 ENV_TOKEN_NAME = "LINODE_TOKEN"
+ENV_API_URL_NAME = "LINODE_API_URL"
+ENV_API_CA_NAME = "LINODE_API_CA"
 RUN_LONG_TESTS = "RUN_LONG_TESTS"
 
 
@@ -13,6 +15,15 @@ def get_token():
     return os.environ.get(ENV_TOKEN_NAME, None)
 
 
+def get_api_url():
+    return os.environ.get(ENV_API_URL_NAME, "https://api.linode.com/v4beta")
+
+
+def get_api_ca_file():
+    result = os.environ.get(ENV_API_CA_NAME, None)
+    return result if result != "" else None
+
+
 def run_long_tests():
     return os.environ.get(RUN_LONG_TESTS, None)
 
@@ -71,7 +82,13 @@ def ssh_key_gen():
 @pytest.fixture(scope="session")
 def get_client():
     token = get_token()
-    client = LinodeClient(token)
+    api_url = get_api_url()
+    api_ca_file = get_api_ca_file()
+    client = LinodeClient(
+        token,
+        base_url=api_url,
+        ca_path=api_ca_file,
+    )
     return client
 
 
diff --git a/test/integration/models/test_linode.py b/test/integration/models/test_linode.py
index 4427c7d5d..22f5709c7 100644
--- a/test/integration/models/test_linode.py
+++ b/test/integration/models/test_linode.py
@@ -398,6 +398,22 @@ def test_get_linode_types(get_client):
     assert "g6-nanode-1" in ids
 
 
+def test_get_linode_types_overrides(get_client):
+    types = get_client.linode.types()
+
+    target_types = [
+        v
+        for v in types
+        if len(v.region_prices) > 0 and v.region_prices[0].hourly > 0
+    ]
+
+    assert len(target_types) > 0
+
+    for linode_type in target_types:
+        assert linode_type.region_prices[0].hourly >= 0
+        assert linode_type.region_prices[0].monthly >= 0
+
+
 def test_get_linode_type_by_id(get_client):
     pytest.skip(
         "Might need Type to match how other object models are behaving e.g. client.load(Type, 123)"
diff --git a/test/script/add_to_xml_test_report.py b/test/script/add_to_xml_test_report.py
new file mode 100644
index 000000000..d486028be
--- /dev/null
+++ b/test/script/add_to_xml_test_report.py
@@ -0,0 +1,41 @@
+import argparse
+import xml.etree.ElementTree as ET
+
+# Parse command-line arguments
+parser = argparse.ArgumentParser(
+    description="Modify XML with workflow information"
+)
+parser.add_argument("--branch_name", required=True)
+parser.add_argument("--gha_run_id", required=True)
+parser.add_argument("--gha_run_number", required=True)
+parser.add_argument(
+    "--xmlfile", required=True
+)  # Added argument for XML file path
+
+args = parser.parse_args()
+
+# Open and parse the XML file
+xml_file_path = args.xmlfile
+tree = ET.parse(xml_file_path)
+root = tree.getroot()
+
+# Create new elements for the information
+branch_name_element = ET.Element("branch_name")
+branch_name_element.text = args.branch_name
+
+gha_run_id_element = ET.Element("gha_run_id")
+gha_run_id_element.text = args.gha_run_id
+
+gha_run_number_element = ET.Element("gha_run_number")
+gha_run_number_element.text = args.gha_run_number
+
+# Add the new elements to the root of the XML
+root.append(branch_name_element)
+root.append(gha_run_id_element)
+root.append(gha_run_number_element)
+
+# Save the modified XML
+modified_xml_file_path = xml_file_path  # Overwrite it
+tree.write(modified_xml_file_path)
+
+print(f"Modified XML saved to {modified_xml_file_path}")
diff --git a/test/script/test_report_upload_script.py b/test/script/test_report_upload_script.py
new file mode 100644
index 000000000..5dd1a9e31
--- /dev/null
+++ b/test/script/test_report_upload_script.py
@@ -0,0 +1,43 @@
+import os
+import sys
+
+import boto3
+from botocore.exceptions import NoCredentialsError
+
+ACCESS_KEY = os.environ.get("LINODE_CLI_OBJ_ACCESS_KEY")
+SECRET_KEY = os.environ.get("LINODE_CLI_OBJ_SECRET_KEY")
+BUCKET_NAME = "dx-test-results"
+
+linode_obj_config = {
+    "aws_access_key_id": ACCESS_KEY,
+    "aws_secret_access_key": SECRET_KEY,
+    "endpoint_url": "https://us-southeast-1.linodeobjects.com",
+}
+
+
+def upload_to_linode_object_storage(file_name):
+    try:
+        s3 = boto3.client("s3", **linode_obj_config)
+
+        s3.upload_file(Filename=file_name, Bucket=BUCKET_NAME, Key=file_name)
+
+        print(f"Successfully uploaded {file_name} to Linode Object Storage.")
+
+    except NoCredentialsError:
+        print(
+            "Credentials not available. Ensure you have set your AWS credentials."
+        )
+
+
+if __name__ == "__main__":
+    if len(sys.argv) != 2:
+        print("Usage: python upload_to_linode.py <file_name>")
+        sys.exit(1)
+
+    file_name = sys.argv[1]
+
+    if not file_name:
+        print("Error: The provided file name is empty or invalid.")
+        sys.exit(1)
+
+    upload_to_linode_object_storage(file_name)
diff --git a/test/unit/linode_client_test.py b/test/unit/linode_client_test.py
index 498c35509..69af304f7 100644
--- a/test/unit/linode_client_test.py
+++ b/test/unit/linode_client_test.py
@@ -476,6 +476,22 @@ def test_join_beta_program(self):
             )
             self.assertEqual(m.call_url, join_beta_url)
 
+    def test_account_transfer(self):
+        """
+        Tests that payments can be retrieved
+        """
+        transfer = self.client.account.transfer()
+
+        self.assertEqual(transfer.quota, 471)
+        self.assertEqual(transfer.used, 737373)
+        self.assertEqual(transfer.billable, 0)
+
+        self.assertEqual(len(transfer.region_transfers), 1)
+        self.assertEqual(transfer.region_transfers[0].id, "ap-west")
+        self.assertEqual(transfer.region_transfers[0].used, 1)
+        self.assertEqual(transfer.region_transfers[0].quota, 5010)
+        self.assertEqual(transfer.region_transfers[0].billable, 0)
+
 
 class BetaProgramGroupTest(ClientBaseCase):
     """
diff --git a/test/unit/objects/linode_test.py b/test/unit/objects/linode_test.py
index 1f75f8c1a..2aa280fef 100644
--- a/test/unit/objects/linode_test.py
+++ b/test/unit/objects/linode_test.py
@@ -533,6 +533,8 @@ def test_get_types(self):
             self.assertIsNotNone(t.type_class)
             self.assertIsNotNone(t.gpus)
             self.assertIsNone(t.successor)
+            self.assertIsNotNone(t.region_prices)
+            self.assertIsNotNone(t.addons.backups.region_prices)
 
     def test_get_type_by_id(self):
         """
@@ -546,6 +548,7 @@ def test_get_type_by_id(self):
         self.assertEqual(t.label, "Linode 1024")
         self.assertEqual(t.disk, 20480)
         self.assertEqual(t.type_class, "nanode")
+        self.assertEqual(t.region_prices[0].id, "us-east")
 
     def test_get_type_gpu(self):
         """