Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,3 @@
**What are the steps to reproduce the issue or verify the changes?**

**How do I run the relevant unit/integration tests?**

## 📷 Preview

**If applicable, include a screenshot or code snippet of this change. Otherwise, please remove this section.**
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ jobs:
uses: actions/checkout@v6

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
queries: security-and-quality

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
10 changes: 9 additions & 1 deletion .github/workflows/e2e-test-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ on:
pull_request_number:
description: 'The number of the PR.'
required: false
test_report_upload:
description: 'Indicates whether to upload the test report to object storage. Defaults to "false"'
required: false
default: 'false'
type: choice
options:
- 'true'
- 'false'

name: PR E2E Tests

Expand Down Expand Up @@ -101,7 +109,7 @@ jobs:
LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }}

- name: Upload test results
if: always()
if: always() && github.repository == 'linode/linode_api4-python' && (github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.test_report_upload == 'true'))
run: |
filename=$(ls | grep -E '^[0-9]{12}_sdk_test_report\.xml$')
python3 e2e_scripts/tod_scripts/xml_to_obj_storage/scripts/add_gha_info_to_xml.py \
Expand Down
17 changes: 13 additions & 4 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ on:
options:
- 'true'
- 'false'
test_report_upload:
description: 'Indicates whether to upload the test report to object storage. Defaults to "false"'
type: choice
required: false
default: 'false'
options:
- 'true'
- 'false'
push:
branches:
- main
Expand Down Expand Up @@ -97,7 +105,7 @@ jobs:

- name: Upload Test Report as Artifact
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: test-report-file
if-no-files-found: ignore
Expand Down Expand Up @@ -172,7 +180,8 @@ jobs:
process-upload-report:
runs-on: ubuntu-latest
needs: [integration-tests]
if: always() && github.repository == 'linode/linode_api4-python' # Run even if integration tests fail and only on main repository
# Run even if integration tests fail on main repository AND push event OR test_report_upload is true in case of manual run
if: always() && github.repository == 'linode/linode_api4-python' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.test_report_upload == 'true'))
outputs:
summary: ${{ steps.set-test-summary.outputs.summary }}

Expand All @@ -184,7 +193,7 @@ jobs:
submodules: 'recursive'

- name: Download test report
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
name: test-report-file

Expand Down Expand Up @@ -271,4 +280,4 @@ jobs:
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
thread_ts: "${{ steps.main_message.outputs.ts }}"
text: "${{ needs.process-upload-report.outputs.summary }}"
text: "${{ needs.process-upload-report.outputs.summary }}"
5 changes: 4 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Include all files under test/ directory in source distribution only
graft test

# Exclude Python bytecode
global-exclude *.pyc
include baked_version
global-exclude __pycache__
9 changes: 9 additions & 0 deletions docs/linode_api4/linode_client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ Includes methods for interacting with our Longview service.
:members:
:special-members:

LockGroup
^^^^^^^^^^^^^

Includes methods for interacting with our Lock service.

.. autoclass:: linode_api4.linode_client.LockGroup
:members:
:special-members:

NetworkingGroup
^^^^^^^^^^^^^^^

Expand Down
2 changes: 2 additions & 0 deletions linode_api4/groups/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
from .database import *
from .domain import *
from .image import *
from .image_share_group import *
from .linode import *
from .lke import *
from .lke_tier import *
from .lock import *
from .longview import *
from .maintenance import *
from .monitor import *
Expand Down
142 changes: 142 additions & 0 deletions linode_api4/groups/image_share_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from typing import Optional

from linode_api4.groups import Group
from linode_api4.objects import (
ImageShareGroup,
ImageShareGroupImagesToAdd,
ImageShareGroupToken,
)
from linode_api4.objects.base import _flatten_request_body_recursive
from linode_api4.util import drop_null_keys


class ImageShareGroupAPIGroup(Group):
"""
Collections related to Private Image Sharing.
NOTE: Private Image Sharing features are in beta and may not be generally available.
"""

def __call__(self, *filters):
"""
Retrieves a list of Image Share Groups created by the user (producer).
You can filter this query to retrieve only Image Share Groups
relevant to a specific query, for example::
filtered_share_groups = client.sharegroups(
ImageShareGroup.label == "my-label")
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-sharegroups
:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
for more details on filtering.
:returns: A list of Image Share Groups.
:rtype: PaginatedList of ImageShareGroup
"""
return self.client._get_and_filter(ImageShareGroup, *filters)

def sharegroups_by_image_id(self, image_id: str):
"""
Retrieves a list of Image Share Groups that share a specific Private Image.
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-images-sharegroups-image
:param image_id: The ID of the Image to query for.
:type image_id: str
:returns: A list of Image Share Groups sharing the specified Image.
:rtype: PaginatedList of ImageShareGroup
"""
return self.client._get_and_filter(
ImageShareGroup, endpoint="/images/{}/sharegroups".format(image_id)
)

def tokens(self, *filters):
"""
Retrieves a list of Image Share Group Tokens created by the user (consumer).
You can filter this query to retrieve only Image Share Group Tokens
relevant to a specific query, for example::
filtered_share_group_tokens = client.sharegroups.tokens(
ImageShareGroupToken.label == "my-label")
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-user-tokens
:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
for more details on filtering.
:returns: A list of Image Share Group Tokens.
:rtype: PaginatedList of ImageShareGroupToken
"""
return self.client._get_and_filter(ImageShareGroupToken, *filters)

def create_sharegroup(
self,
label: Optional[str] = None,
description: Optional[str] = None,
images: Optional[ImageShareGroupImagesToAdd] = None,
):
"""
Creates a new Image Share Group.
API Documentation: https://techdocs.akamai.com/linode-api/reference/post-sharegroups
:param label: The label for the resulting Image Share Group.
:type label: str
:param description: The description for the new Image Share Group.
:type description: str
:param images: A list of Images to share in the new Image Share Group, formatted in JSON.
:type images: Optional[ImageShareGroupImagesToAdd]
:returns: The new Image Share Group.
:rtype: ImageShareGroup
"""
params = {
"label": label,
"description": description,
}

if images:
params["images"] = images

result = self.client.post(
"/images/sharegroups",
data=_flatten_request_body_recursive(drop_null_keys(params)),
)

return ImageShareGroup(self.client, result["id"], result)

def create_token(
self, valid_for_sharegroup_uuid: str, label: Optional[str] = None
):
"""
Creates a new Image Share Group Token and returns the token value.
API Documentation: https://techdocs.akamai.com/linode-api/reference/post-sharegroup-tokens
:param valid_for_sharegroup_uuid: The UUID of the Image Share Group that this token will be valid for.
:type valid_for_sharegroup_uuid: Optional[str]
:param label: The label for the resulting Image Share Group Token.
:type label: str
:returns: The new Image Share Group Token object and the one-time use token itself.
:rtype: (ImageShareGroupToken, str)
"""
params = {"valid_for_sharegroup_uuid": valid_for_sharegroup_uuid}

if label:
params["label"] = label

result = self.client.post(
"/images/sharegroups/tokens",
data=_flatten_request_body_recursive(drop_null_keys(params)),
)

token_value = result.pop("token", None)
token_obj = ImageShareGroupToken(
self.client, result["token_uuid"], result
)
return token_obj, token_value
6 changes: 1 addition & 5 deletions linode_api4/groups/linode.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
NetworkInterface,
_expand_placement_group_assignment,
)
from linode_api4.objects.linode_interfaces import (
LinodeInterfaceOptions,
)
from linode_api4.objects.linode_interfaces import LinodeInterfaceOptions
from linode_api4.util import drop_null_keys


Expand Down Expand Up @@ -337,8 +335,6 @@ def instance_create(
:type network_helper: bool
:param maintenance_policy: The slug of the maintenance policy to apply during maintenance.
If not provided, the default policy (linode/migrate) will be applied.
NOTE: This field is in beta and may only
function if base_url is set to `https://api.linode.com/v4beta`.
:type maintenance_policy: str

:returns: A new Instance object, or a tuple containing the new Instance and
Expand Down
11 changes: 10 additions & 1 deletion linode_api4/groups/lke.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ def cluster_create(
self,
region,
label,
node_pools,
kube_version,
node_pools: Optional[list] = None,
control_plane: Union[
LKEClusterControlPlaneOptions, Dict[str, Any]
] = None,
Expand Down Expand Up @@ -119,6 +119,15 @@ def cluster_create(
:returns: The new LKE Cluster
:rtype: LKECluster
"""
if node_pools is None:
node_pools = []

if len(node_pools) == 0 and (
tier is None or tier.lower() != "enterprise"
):
raise ValueError(
"LKE standard clusters must have at least one node pool."
)

params = {
"label": label,
Expand Down
72 changes: 72 additions & 0 deletions linode_api4/groups/lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from typing import Union

from linode_api4.errors import UnexpectedResponseError
from linode_api4.groups import Group
from linode_api4.objects import Lock, LockType

__all__ = ["LockGroup"]


class LockGroup(Group):
"""
Encapsulates methods for interacting with Resource Locks.

Resource locks prevent deletion or modification of resources.
Currently, only Linode instances can be locked.
"""

def __call__(self, *filters):
"""
Returns a list of all Resource Locks on the account.

This is intended to be called off of the :any:`LinodeClient`
class, like this::

locks = client.locks()

API Documentation: TBD

:param filters: Any number of filters to apply to this query.
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
for more details on filtering.

:returns: A list of Resource Locks on the account.
:rtype: PaginatedList of Lock
"""
return self.client._get_and_filter(Lock, *filters)

def create(
self,
entity_type: str,
entity_id: Union[int, str],
lock_type: Union[LockType, str],
) -> Lock:
"""
Creates a new Resource Lock for the specified entity.

API Documentation: TBD

:param entity_type: The type of entity to lock (e.g., "linode").
:type entity_type: str
:param entity_id: The ID of the entity to lock.
:type entity_id: int | str
:param lock_type: The type of lock to create. Defaults to "cannot_delete".
:type lock_type: LockType | str

:returns: The newly created Resource Lock.
:rtype: Lock
"""
params = {
"entity_type": entity_type,
"entity_id": entity_id,
"lock_type": lock_type,
}

result = self.client.post("/locks", data=params)

if "id" not in result:
raise UnexpectedResponseError(
"Unexpected response when creating lock!", json=result
)

return Lock(self.client, result["id"], result)
Loading
Loading