Skip to content

Commit 61e7d94

Browse files
Merge pull request #303 from linode/dev
Release v5.6.0
2 parents 68d7c74 + 83e8269 commit 61e7d94

63 files changed

Lines changed: 3434 additions & 156 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/e2e-test-pr.yml

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
on:
2+
pull_request:
3+
workflow_dispatch:
4+
inputs:
5+
test_path:
6+
description: 'Enter specific test path. E.g. linode_client/test_linode_client.py, models/test_account.py'
7+
required: false
8+
sha:
9+
description: 'The hash value of the commit.'
10+
required: true
11+
pull_request_number:
12+
description: 'The number of the PR.'
13+
required: false
14+
15+
name: PR E2E Tests
16+
17+
jobs:
18+
integration-fork-ubuntu:
19+
runs-on: ubuntu-latest
20+
if:
21+
github.event_name == 'workflow_dispatch' && inputs.sha != ''
22+
23+
steps:
24+
- uses: actions-ecosystem/action-regex-match@v2
25+
id: validate-tests
26+
with:
27+
text: ${{ inputs.test_path }}
28+
regex: '[^a-z0-9-:.\/_]' # Tests validation
29+
flags: gi
30+
31+
# Check out merge commit
32+
- name: Checkout PR
33+
uses: actions/checkout@v3
34+
with:
35+
ref: ${{ inputs.sha }}
36+
37+
- name: Get the hash value of the latest commit from the PR branch
38+
uses: octokit/graphql-action@v2.x
39+
id: commit-hash
40+
if: ${{ inputs.pull_request_number != '' }}
41+
with:
42+
query: |
43+
query PRHeadCommitHash($owner: String!, $repo: String!, $pr_num: Int!) {
44+
repository(owner:$owner, name:$repo) {
45+
pullRequest(number: $pr_num) {
46+
headRef {
47+
target {
48+
... on Commit {
49+
oid
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
owner: ${{ github.event.repository.owner.login }}
57+
repo: ${{ github.event.repository.name }}
58+
pr_num: ${{ fromJSON(inputs.pull_request_number) }}
59+
env:
60+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61+
62+
- name: Update system packages
63+
run: sudo apt-get update -y
64+
65+
- name: Install system deps
66+
run: sudo apt-get install -y build-essential
67+
68+
- name: Setup Python
69+
uses: actions/setup-python@v4
70+
with:
71+
python-version: '3.x'
72+
73+
- name: Install Python deps
74+
run: pip install -r requirements.txt -r requirements-dev.txt wheel boto3
75+
76+
- name: Install Python SDK
77+
run: make install
78+
env:
79+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
80+
81+
- run: make INTEGRATION_TEST_PATH="${{ inputs.test_path }}" testint
82+
if: ${{ steps.validate-tests.outputs.match == '' }}
83+
env:
84+
LINODE_CLI_TOKEN: ${{ secrets.LINODE_TOKEN }}
85+
86+
- uses: actions/github-script@v6
87+
id: update-check-run
88+
if: ${{ inputs.pull_request_number != '' && fromJson(steps.commit-hash.outputs.data).repository.pullRequest.headRef.target.oid == inputs.sha }}
89+
env:
90+
number: ${{ inputs.pull_request_number }}
91+
job: ${{ github.job }}
92+
conclusion: ${{ job.status }}
93+
with:
94+
github-token: ${{ secrets.GITHUB_TOKEN }}
95+
script: |
96+
const { data: pull } = await github.rest.pulls.get({
97+
...context.repo,
98+
pull_number: process.env.number
99+
});
100+
const ref = pull.head.sha;
101+
const { data: checks } = await github.rest.checks.listForRef({
102+
...context.repo,
103+
ref
104+
});
105+
const check = checks.check_runs.filter(c => c.name === process.env.job);
106+
const { data: result } = await github.rest.checks.update({
107+
...context.repo,
108+
check_run_id: check[0].id,
109+
status: 'completed',
110+
conclusion: process.env.conclusion
111+
});
112+
return result;

.github/workflows/publish-pypi.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@ jobs:
3333
LINODE_SDK_VERSION: ${{ github.event.release.tag_name }}
3434

3535
- name: Publish the release artifacts to PyPI
36-
uses: pypa/gh-action-pypi-publish@37f50c210e3d2f9450da2cd423303d6a14a6e29f # pin@release/v1
36+
uses: pypa/gh-action-pypi-publish@a56da0b891b3dc519c7ee3284aff1fad93cc8598 # pin@release/v1.8.6
3737
with:
38-
password: ${{ secrets.PYPI_API_TOKEN }}
38+
password: ${{ secrets.PYPI_API_TOKEN }}

Makefile

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
PYTHON ?= python3
22

3+
INTEGRATION_TEST_PATH :=
4+
TEST_CASE_COMMAND :=
5+
MODEL_COMMAND :=
6+
7+
ifdef TEST_CASE
8+
TEST_CASE_COMMAND = -k $(TEST_CASE)
9+
endif
10+
11+
ifdef TEST_MODEL
12+
MODEL_COMMAND = models/$(TEST_MODEL)
13+
endif
14+
315
@PHONEY: clean
416
clean:
517
mkdir -p dist
@@ -8,8 +20,7 @@ clean:
820

921
@PHONEY: build
1022
build: clean
11-
$(PYTHON) setup.py sdist
12-
$(PYTHON) setup.py bdist_wheel
23+
$(PYTHON) -m build --wheel --sdist
1324

1425

1526
@PHONEY: release
@@ -18,7 +29,7 @@ release: build
1829

1930

2031
install: clean
21-
python3 setup.py install
32+
python3 -m pip install .
2233

2334

2435
requirements:
@@ -45,3 +56,7 @@ lint:
4556
autoflake --check linode_api4 test
4657
black --check --verbose linode_api4 test
4758
pylint linode_api4
59+
60+
@PHONEY: testint
61+
testint:
62+
python3 -m pytest test/integration/${INTEGRATION_TEST_PATH}${MODEL_COMMAND} ${TEST_CASE_COMMAND}

README.rst

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Building from Source
2727
To build and install this package:
2828

2929
- Clone this repository
30-
- ``./setup.py install``
30+
- ``python3 -m pip install .``
3131

3232
Usage
3333
=====
@@ -101,7 +101,7 @@ Contributing
101101
Tests
102102
-----
103103

104-
Tests live in the ``tests`` directory. When invoking tests, make sure you are
104+
Tests live in the ``test`` directory. When invoking tests, make sure you are
105105
in the root directory of this project. To run the full suite across all
106106
supported python versions, use tox_:
107107

@@ -133,6 +133,35 @@ from the api base url that should be returned, for example::
133133

134134
.. _tox: http://tox.readthedocs.io
135135

136+
137+
Integration Tests
138+
-----------
139+
Integration tests live in the ``test/integration`` directory.
140+
141+
Pre-requisite
142+
^^^^^^^^^^^^^^^^^
143+
Export Linode API token as `LINODE_CLI_TOKEN` before running integration tests::
144+
145+
export LINODE_TOKEN = $(your_token)
146+
147+
Running the tests
148+
^^^^^^^^^^^^^^^^^
149+
Run the tests locally using the make command. Run the entire test suite using command below::
150+
151+
make testint
152+
153+
To run a specific package, use environment variable `INTEGRATION_TEST_PATH` with `testint` command::
154+
155+
make INTEGRATION_TEST_PATH="linode_client" testint
156+
157+
To run a specific model test suite, set the environment variable `TEST_MODEL` using file name in `integration/models`::
158+
159+
make TEST_MODEL="test_account.py" testint
160+
161+
Lastly to run a specific test case use environment variable `TEST_CASE` with `testint` command::
162+
163+
make TEST_CASE=test_get_domain_record testint
164+
136165
Documentation
137166
-------------
138167

docs/guides/event_polling.rst

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
Polling for Events
2+
==================
3+
4+
There are often situations where an API request will trigger a
5+
long-running operation (e.g. Instance shutdown) that will run
6+
after the request has been made. These operations are tracked
7+
through `Linode Account Events`_ which reflect the target entity,
8+
progress, and status of these operations.
9+
10+
.. _Linode Account Events: https://www.linode.com/docs/api/account/#events-list
11+
12+
There are often cases where you would like for your application to
13+
halt until these operations have succeeded. The most reliable and
14+
efficient way to achieve this is by using the :py:class:`EventPoller`
15+
object.
16+
17+
Polling on Basic Operations
18+
---------------------------
19+
20+
In order to poll for an operation, we must create an :py:class:`EventPoller`
21+
object *before* the endpoint that triggers the operation has been called.
22+
23+
Assuming a :py:class:`LinodeClient` object has already been created with the name
24+
"client" and an :py:class:`Instance` object has already been created with the name "my_instance",
25+
an :py:class:`EventPoller` can be created using the
26+
:meth:`LinodeClient.polling.event_poller_create(...) <PollingGroup.event_poller_create>`
27+
method::
28+
29+
poller = client.polling.event_poller_create(
30+
"linode", # The type of the target entity
31+
"linode_shutdown", # The action to poll for
32+
entity_id=my_instance.id, # The ID of your Linode Instance
33+
)
34+
35+
Valid values for the `type` and `action` fields can be found in the `Events Response Documentation`_.
36+
37+
.. _Events Response Documentation: https://www.linode.com/docs/api/account/#events-list__responses
38+
39+
From here, we can send the request to trigger the long-running operation::
40+
41+
my_instance.shutdown()
42+
43+
To wait for this operation to finish, we can call the
44+
:meth:`poller.wait_for_next_event_finished(...) <EventPoller.wait_for_next_event_finished>`
45+
method::
46+
47+
poller.wait_for_next_event_finished()
48+
49+
The :py:class:`timeout` (default 240) and :py:class:`interval` (default 5) arguments can optionally be used to configure the timeout
50+
and poll frequency for this operation.
51+
52+
Bringing this together, we get the following::
53+
54+
from linode_api4 import LinodeClient, Instance
55+
56+
# Construct a client
57+
client = LinodeClient("MY_LINODE_TOKEN")
58+
59+
# Fetch an existing Linode Instance
60+
my_instance = client.load(Instance, 12345)
61+
62+
# Create the event poller
63+
poller = client.polling.event_poller_create(
64+
"linode", # The type of the target entity
65+
"linode_shutdown", # The action to poll for
66+
entity_id=my_instance.id, # The ID of your Linode Instance
67+
)
68+
69+
# Shutdown the Instance
70+
my_instance.shutdown()
71+
72+
# Wait until the event has finished
73+
poller.wait_for_next_event_finished()
74+
75+
print("Linode has been successfully shutdown!")
76+
77+
Polling for an Entity to be Free
78+
--------------------------------
79+
80+
In many cases, certain operations cannot be run until any other operations running on a resource have
81+
been completed. To ensure these operation are run reliably and do not encounter conflicts,
82+
you can use the
83+
:meth:`LinodeClient.polling.wait_for_entity_free(...) <PollingGroup.wait_for_entity_free>` method
84+
to wait until a resource has no running or queued operations.
85+
86+
For example::
87+
88+
# Construct a client
89+
client = LinodeClient("MY_LINODE_TOKEN")
90+
91+
# Load an existing instance
92+
my_instance = client.load(Instance, 12345)
93+
94+
# Wait until the Linode is not busy
95+
client.polling.wait_for_entity_free(
96+
"linode",
97+
my_instance.id
98+
)
99+
100+
# Boot the Instance
101+
my_instance.boot()
102+
103+
The :py:class:`timeout` (default 240) and :py:class:`interval` (default 5) arguments can optionally be used to configure the timeout
104+
and poll frequency for this operation.

docs/guides/getting_started.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ If you prefer, you can clone the package from github_ and install it from source
1818
1919
git clone git@github.com:Linode/linode_api4-python
2020
cd linode_api4
21-
python setup.py install
21+
python -m pip install .
2222
2323
Authentication
2424
--------------

docs/index.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ To install from source::
1919

2020
git clone https://github.com/linode/linode_api4-python
2121
cd linode_api4
22-
python setup.py install
22+
python -m pip install .
2323

2424
For more information, see our :doc:`Getting Started<guides/getting_started>`
2525
guide.
@@ -32,9 +32,11 @@ Table of Contents
3232

3333
guides/getting_started
3434
guides/core_concepts
35+
guides/event_polling
3536
guides/oauth
3637
linode_api4/linode_client
3738
linode_api4/login_client
3839
linode_api4/objects/models
40+
linode_api4/polling
3941
linode_api4/paginated_list
4042
linode_api4/objects/filtering

docs/linode_api4/linode_client.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,15 @@ with buckets and objects, use the s3 API directly with a library like `boto3`_.
146146

147147
.. _boto3: https://github.com/boto/boto3
148148

149+
PollingGroup
150+
^^^^^^^^^^^^
151+
152+
Includes methods related to account event polling.
153+
154+
.. autoclass:: linode_api4.linode_client.PollingGroup
155+
:members:
156+
:special-members:
157+
149158
ProfileGroup
150159
^^^^^^^^^^^^
151160

docs/linode_api4/objects/models.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,3 @@ Volume Models
139139
:exclude-members: api_endpoint, properties, derived_url_path, id_attribute, parent_id_name
140140
:undoc-members:
141141
:inherited-members:
142-

docs/linode_api4/polling.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Event Polling
2+
==========
3+
4+
This project exposes a framework for dynamically polling on long-running Linode Events.
5+
6+
See the :doc:`Event Polling Guide<../guides/event_polling>` for more details.
7+
8+
EventPoller class
9+
-------------------
10+
11+
.. autoclass:: linode_api4.EventPoller
12+
:members:

0 commit comments

Comments
 (0)