Skip to content

Release v5.24.0 #472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Nov 13, 2024
20 changes: 17 additions & 3 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,30 @@ on:
description: 'The hash value of the commit'
required: false
default: ''
python-version:
description: 'Specify Python version to use'
required: false
run-eol-python-version:
description: 'Run EOL python version?'
required: false
default: 'false'
type: choice
options:
- 'true'
- 'false'
push:
branches:
- main
- dev

env:
DEFAULT_PYTHON_VERSION: "3.9"
EOL_PYTHON_VERSION: "3.8"
EXIT_STATUS: 0

jobs:
integration-tests:
runs-on: ubuntu-latest
env:
EXIT_STATUS: 0
steps:
- name: Clone Repository with SHA
if: ${{ inputs.sha != '' }}
Expand All @@ -40,7 +54,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
python-version: ${{ inputs.run-eol-python-version == 'true' && env.EOL_PYTHON_VERSION || inputs.python-version || env.DEFAULT_PYTHON_VERSION }}

- name: Install Python deps
run: pip install -U setuptools wheel boto3 certifi
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8','3.9','3.10','3.11', '3.12']
python-version: ['3.9','3.10','3.11', '3.12']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/release-notify-slack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Notify Dev DX Channel on Release
on:
release:
types: [published]
workflow_dispatch: null

jobs:
notify:
if: github.repository == 'linode/linode_api4-python'
runs-on: ubuntu-latest
steps:
- name: Notify Slack - Main Message
id: main_message
uses: slackapi/[email protected]
with:
channel-id: ${{ secrets.DEV_DX_SLACK_CHANNEL_ID }}
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*New Release Published: _linode_api4-python_ <${{ github.event.release.html_url }}|${{ github.event.release.tag_name }}> is now live!* :tada:"
}
}
]
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
121 changes: 119 additions & 2 deletions linode_api4/errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# Necessary to maintain compatibility with Python < 3.11
from __future__ import annotations

from builtins import super
from json import JSONDecodeError
from typing import Any, Dict, Optional

from requests import Response


class ApiError(RuntimeError):
Expand All @@ -8,14 +15,90 @@ class ApiError(RuntimeError):
often, this will be caused by invalid input to the API.
"""

def __init__(self, message, status=400, json=None):
def __init__(
self,
message: str,
status: int = 400,
json: Optional[Dict[str, Any]] = None,
response: Optional[Response] = None,
):
super().__init__(message)

self.status = status
self.json = json
self.response = response

self.errors = []

if json and "errors" in json and isinstance(json["errors"], list):
self.errors = [e["reason"] for e in json["errors"]]

@classmethod
def from_response(
cls,
response: Response,
message: Optional[str] = None,
disable_formatting: bool = False,
) -> Optional[ApiError]:
"""
Creates an ApiError object from the given response,
or None if the response does not contain an error.

:arg response: The response to create an ApiError from.
:arg message: An optional message to prepend to the error's message.
:arg disable_formatting: If true, the error's message will not automatically be formatted
with details from the API response.

:returns: The new API error.
"""

if response.status_code < 400 or response.status_code > 599:
# No error was found
return None

request = response.request

try:
response_json = response.json()
except JSONDecodeError:
response_json = None

# Use the user-defined message is formatting is disabled
if disable_formatting:
return cls(
message,
status=response.status_code,
json=response_json,
response=response,
)

# Build the error string
error_fmt = "N/A"

if response_json is not None and "errors" in response_json:
errors = []

for error in response_json["errors"]:
field = error.get("field")
reason = error.get("reason")
errors.append(f"{field + ': ' if field else ''}{reason}")

error_fmt = "; ".join(errors)

elif len(response.text or "") > 0:
error_fmt = response.text

return cls(
(
f"{message + ': ' if message is not None else ''}"
f"{f'{request.method} {request.path_url}: ' if request else ''}"
f"[{response.status_code}] {error_fmt}"
),
status=response.status_code,
json=response_json,
response=response,
)


class UnexpectedResponseError(RuntimeError):
"""
Expand All @@ -26,7 +109,41 @@ class UnexpectedResponseError(RuntimeError):
library, and should be fixed with changes to this codebase.
"""

def __init__(self, message, status=200, json=None):
def __init__(
self,
message: str,
status: int = 200,
json: Optional[Dict[str, Any]] = None,
response: Optional[Response] = None,
):
super().__init__(message)

self.status = status
self.json = json
self.response = response

@classmethod
def from_response(
cls,
message: str,
response: Response,
) -> Optional[UnexpectedResponseError]:
"""
Creates an UnexpectedResponseError object from the given response and message.

:arg message: The message to create this error with.
:arg response: The response to create an UnexpectedResponseError from.
:returns: The new UnexpectedResponseError.
"""

try:
response_json = response.json()
except JSONDecodeError:
response_json = None

return cls(
message,
status=response.status_code,
json=response_json,
response=response,
)
2 changes: 1 addition & 1 deletion linode_api4/groups/lke.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def types(self, *filters):
"""
Returns a :any:`PaginatedList` of :any:`LKEType` objects that represents a valid LKE type.

API Documentation: TODO
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-lke-types

:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
Expand Down
2 changes: 1 addition & 1 deletion linode_api4/groups/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ def transfer_prices(self, *filters):
"""
Returns a :any:`PaginatedList` of :any:`NetworkTransferPrice` objects that represents a valid network transfer price.

API Documentation: TODO
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-network-transfer-prices

:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
Expand Down
2 changes: 1 addition & 1 deletion linode_api4/groups/nodebalancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def types(self, *filters):
"""
Returns a :any:`PaginatedList` of :any:`NodeBalancerType` objects that represents a valid NodeBalancer type.

API Documentation: TODO
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-node-balancer-types

:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
Expand Down
2 changes: 1 addition & 1 deletion linode_api4/groups/volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def types(self, *filters):
"""
Returns a :any:`PaginatedList` of :any:`VolumeType` objects that represents a valid Volume type.

API Documentation: TODO
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-volume-types

:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
Expand Down
20 changes: 3 additions & 17 deletions linode_api4/linode_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,23 +287,9 @@ def _api_call(
if warning:
logger.warning("Received warning from server: {}".format(warning))

if 399 < response.status_code < 600:
j = None
error_msg = "{}: ".format(response.status_code)
try:
j = response.json()
if "errors" in j.keys():
for e in j["errors"]:
msg = e.get("reason", "")
field = e.get("field", None)

error_msg += "{}{}; ".format(
f"[{field}] " if field is not None else "",
msg,
)
except:
pass
raise ApiError(error_msg, status=response.status_code, json=j)
api_error = ApiError.from_response(response)
if api_error is not None:
raise api_error

if response.status_code != 204:
j = response.json()
Expand Down
11 changes: 5 additions & 6 deletions linode_api4/login_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,9 @@ def oauth_redirect():
)

if r.status_code != 200:
raise ApiError(
"OAuth token exchange failed",
status=r.status_code,
json=r.json(),
raise ApiError.from_response(
r,
message="OAuth token exchange failed",
)

token = r.json()["access_token"]
Expand Down Expand Up @@ -479,7 +478,7 @@ def refresh_oauth_token(self, refresh_token):
)

if r.status_code != 200:
raise ApiError("Refresh failed", r)
raise ApiError.from_response(r, message="Refresh failed")

token = r.json()["access_token"]
scopes = OAuthScopes.parse(r.json()["scopes"])
Expand Down Expand Up @@ -516,5 +515,5 @@ def expire_token(self, token):
)

if r.status_code != 200:
raise ApiError("Failed to expire token!", r)
raise ApiError.from_response(r, "Failed to expire token!")
return True
15 changes: 7 additions & 8 deletions linode_api4/objects/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,8 +436,10 @@ def thumbnail(self, dump_to=None):
)

if not result.status_code == 200:
raise ApiError(
"No thumbnail found for OAuthClient {}".format(self.id)
raise ApiError.from_response(
result,
"No thumbnail found for OAuthClient {}".format(self.id),
disable_formatting=True,
)

if dump_to:
Expand Down Expand Up @@ -472,12 +474,9 @@ def set_thumbnail(self, thumbnail):
data=thumbnail,
)

if not result.status_code == 200:
errors = []
j = result.json()
if "errors" in j:
errors = [e["reason"] for e in j["errors"]]
raise ApiError("{}: {}".format(result.status_code, errors), json=j)
api_exc = ApiError.from_response(result)
if api_exc is not None:
raise api_exc

return True

Expand Down
8 changes: 4 additions & 4 deletions linode_api4/objects/lke.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class LKEType(Base):
Currently the LKEType can only be retrieved by listing, i.e.:
types = client.lke.types()

API documentation: TODO
API documentation: https://techdocs.akamai.com/linode-api/reference/get-lke-types
"""

properties = {
Expand Down Expand Up @@ -338,7 +338,7 @@ def control_plane_acl(self) -> LKEClusterControlPlaneACL:

NOTE: Control Plane ACLs may not currently be available to all users.

API Documentation: TODO
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-lke-cluster-acl

:returns: The cluster's control plane ACL configuration.
:rtype: LKEClusterControlPlaneACL
Expand Down Expand Up @@ -529,7 +529,7 @@ def control_plane_acl_update(

NOTE: Control Plane ACLs may not currently be available to all users.

API Documentation: TODO
API Documentation: https://techdocs.akamai.com/linode-api/reference/put-lke-cluster-acl

:param acl: The ACL configuration to apply to this cluster.
:type acl: LKEClusterControlPlaneACLOptions or Dict[str, Any]
Expand Down Expand Up @@ -560,7 +560,7 @@ def control_plane_acl_delete(self):

NOTE: Control Plane ACLs may not currently be available to all users.

API Documentation: TODO
API Documentation: https://techdocs.akamai.com/linode-api/reference/delete-lke-cluster-acl
"""
self._client.delete(
f"{LKECluster.api_endpoint}/control_plane_acl", model=self
Expand Down
2 changes: 1 addition & 1 deletion linode_api4/objects/networking.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ class NetworkTransferPrice(Base):
Currently the NetworkTransferPrice can only be retrieved by listing, i.e.:
types = client.networking.transfer_prices()

API documentation: TODO
API documentation: https://techdocs.akamai.com/linode-api/reference/get-network-transfer-prices
"""

properties = {
Expand Down
Loading
Loading