Skip to content

Commit 0bcfa70

Browse files
Merge pull request #348 from linode/dev
Release v5.10.0
2 parents 1db527a + 0e46856 commit 0bcfa70

Some content is hidden

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

47 files changed

+2183
-453
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-latest
1515
strategy:
1616
matrix:
17-
python-version: ['3.7','3.8','3.9','3.10','3.11']
17+
python-version: ['3.8','3.9','3.10','3.11']
1818
steps:
1919
- uses: actions/checkout@v3
2020
- uses: actions/setup-python@v4

Makefile

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,47 +25,47 @@ build: clean
2525

2626
@PHONEY: release
2727
release: build
28-
twine upload dist/*
28+
$(PYTHON) -m twine upload dist/*
2929

3030
@PHONEY: install
3131
install: clean requirements
32-
python3 -m pip install .
32+
$(PYTHON) -m pip install .
3333

3434
@PHONEY: requirements
3535
requirements:
36-
pip install -r requirements.txt -r requirements-dev.txt
36+
$(PYTHON) -m pip install -r requirements.txt -r requirements-dev.txt
3737

3838
@PHONEY: black
3939
black:
40-
black linode_api4 test
40+
$(PYTHON) -m black linode_api4 test
4141

4242
@PHONEY: isort
4343
isort:
44-
isort linode_api4 test
44+
$(PYTHON) -m isort linode_api4 test
4545

4646
@PHONEY: autoflake
4747
autoflake:
48-
autoflake linode_api4 test
48+
$(PYTHON) -m autoflake linode_api4 test
4949

5050
@PHONEY: format
5151
format: black isort autoflake
5252

5353
@PHONEY: lint
5454
lint: build
55-
isort --check-only linode_api4 test
56-
autoflake --check linode_api4 test
57-
black --check --verbose linode_api4 test
58-
pylint linode_api4
59-
twine check dist/*
55+
$(PYTHON) -m isort --check-only linode_api4 test
56+
$(PYTHON) -m autoflake --check linode_api4 test
57+
$(PYTHON) -m black --check --verbose linode_api4 test
58+
$(PYTHON) -m pylint linode_api4
59+
$(PYTHON) -m twine check dist/*
6060

6161
@PHONEY: testint
6262
testint:
63-
python3 -m pytest test/integration/${INTEGRATION_TEST_PATH}${MODEL_COMMAND} ${TEST_CASE_COMMAND}
63+
$(PYTHON) -m pytest test/integration/${INTEGRATION_TEST_PATH}${MODEL_COMMAND} ${TEST_CASE_COMMAND}
6464

6565
@PHONEY: testunit
6666
testunit:
67-
python3 -m python test/unit
67+
$(PYTHON) -m pytest test/unit
6868

6969
@PHONEY: smoketest
7070
smoketest:
71-
pytest -m smoke test/integration --disable-warnings
71+
$(PYTHON) -m pytest -m smoke test/integration --disable-warnings

linode_api4/groups/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818
from .support import *
1919
from .tag import *
2020
from .volume import *
21+
from .vpc import *

linode_api4/groups/linode.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from linode_api4.objects import (
99
AuthorizedApp,
1010
Base,
11+
ConfigInterface,
12+
Firewall,
1113
Image,
1214
Instance,
1315
Kernel,
@@ -250,6 +252,8 @@ def instance_create(
250252
The contents of this field can be built using the
251253
:any:`build_instance_metadata` method.
252254
:type metadata: dict
255+
:param firewall: The firewall to attach this Linode to.
256+
:type firewall: int or Firewall
253257
254258
:returns: A new Instance object, or a tuple containing the new Instance and
255259
the generated password.
@@ -284,6 +288,10 @@ def instance_create(
284288
)
285289
del kwargs["backup"]
286290

291+
if "firewall" in kwargs:
292+
fw = kwargs.pop("firewall")
293+
kwargs["firewall_id"] = fw.id if isinstance(fw, Firewall) else fw
294+
287295
params = {
288296
"type": ltype.id if issubclass(type(ltype), Base) else ltype,
289297
"region": region.id if issubclass(type(region), Base) else region,
@@ -292,6 +300,7 @@ def instance_create(
292300
else None,
293301
"authorized_keys": authorized_keys,
294302
}
303+
295304
params.update(kwargs)
296305

297306
result = self.client.post("/linode/instances", data=params)

linode_api4/groups/vpc.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from typing import Any, Dict, List, Optional, Union
2+
3+
from linode_api4 import VPCSubnet
4+
from linode_api4.errors import UnexpectedResponseError
5+
from linode_api4.groups import Group
6+
from linode_api4.objects import VPC, Base, Region
7+
from linode_api4.paginated_list import PaginatedList
8+
9+
10+
class VPCGroup(Group):
11+
def __call__(self, *filters) -> PaginatedList:
12+
"""
13+
Retrieves all of the VPCs the acting user has access to.
14+
15+
This is intended to be called off of the :any:`LinodeClient`
16+
class, like this::
17+
18+
vpcs = client.vpcs()
19+
20+
API Documentation: TODO
21+
22+
:param filters: Any number of filters to apply to this query.
23+
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
24+
for more details on filtering.
25+
26+
:returns: A list of VPC the acting user can access.
27+
:rtype: PaginatedList of VPC
28+
"""
29+
return self.client._get_and_filter(VPC, *filters)
30+
31+
def create(
32+
self,
33+
label: str,
34+
region: Union[Region, str],
35+
description: Optional[str] = None,
36+
subnets: Optional[List[Dict[str, Any]]] = None,
37+
**kwargs,
38+
) -> VPC:
39+
"""
40+
Creates a new VPC under your Linode account.
41+
42+
API Documentation: TODO
43+
44+
:param label: The label of the newly created VPC.
45+
:type label: str
46+
:param region: The region of the newly created VPC.
47+
:type region: Union[Region, str]
48+
:param description: The user-defined description of this VPC.
49+
:type description: Optional[str]
50+
:param subnets: A list of subnets to create under this VPC.
51+
:type subnets: List[Dict[str, Any]]
52+
53+
:returns: The new VPC object.
54+
:rtype: VPC
55+
"""
56+
params = {
57+
"label": label,
58+
"region": region.id if isinstance(region, Region) else region,
59+
}
60+
61+
if description is not None:
62+
params["description"] = description
63+
64+
if subnets is not None and len(subnets) > 0:
65+
for subnet in subnets:
66+
if not isinstance(subnet, dict):
67+
raise ValueError(
68+
f"Unsupported type for subnet: {type(subnet)}"
69+
)
70+
71+
params["subnets"] = subnets
72+
73+
params.update(kwargs)
74+
75+
result = self.client.post("/vpcs", data=params)
76+
77+
if not "id" in result:
78+
raise UnexpectedResponseError(
79+
"Unexpected response when creating VPC", json=result
80+
)
81+
82+
d = VPC(self.client, result["id"], result)
83+
return d

linode_api4/linode_client.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import json
44
import logging
5+
from importlib.metadata import version
56
from typing import BinaryIO, Tuple
67
from urllib import parse
78

8-
import pkg_resources
99
import requests
1010
from requests.adapters import HTTPAdapter, Retry
1111

@@ -28,6 +28,7 @@
2828
SupportGroup,
2929
TagGroup,
3030
VolumeGroup,
31+
VPCGroup,
3132
)
3233
from linode_api4.objects import Image, and_
3334
from linode_api4.objects.filtering import Filter
@@ -36,7 +37,7 @@
3637
from .paginated_list import PaginatedList
3738
from .util import drop_null_keys
3839

39-
package_version = pkg_resources.require("linode_api4")[0].version
40+
package_version = version("linode_api4")
4041

4142
logger = logging.getLogger(__name__)
4243

@@ -190,6 +191,9 @@ def __init__(
190191
#: Access methods related to Images - See :any:`ImageGroup` for more information.
191192
self.images = ImageGroup(self)
192193

194+
#: Access methods related to VPCs - See :any:`VPCGroup` for more information.
195+
self.vpcs = VPCGroup(self)
196+
193197
#: Access methods related to Event polling - See :any:`PollingGroup` for more information.
194198
self.polling = PollingGroup(self)
195199

linode_api4/objects/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# isort: skip_file
22
from .base import Base, Property, MappedObject, DATE_FORMAT, ExplicitNullValue
33
from .dbase import DerivedBase
4+
from .serializable import JSONObject
45
from .filtering import and_, or_
56
from .region import Region
67
from .image import Image
@@ -17,4 +18,5 @@
1718
from .object_storage import *
1819
from .lke import *
1920
from .database import *
21+
from .vpc import *
2022
from .beta import *

linode_api4/objects/base.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import time
22
from datetime import datetime, timedelta
33

4+
from linode_api4.objects.serializable import JSONObject
5+
46
from .filtering import FilterableMetaclass
57

68
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
@@ -30,6 +32,7 @@ def __init__(
3032
id_relationship=False,
3133
slug_relationship=False,
3234
nullable=False,
35+
json_object=None,
3336
):
3437
"""
3538
A Property is an attribute returned from the API, and defines metadata
@@ -56,6 +59,8 @@ def __init__(
5659
self.is_datetime = is_datetime
5760
self.id_relationship = id_relationship
5861
self.slug_relationship = slug_relationship
62+
self.nullable = nullable
63+
self.json_class = json_object
5964

6065

6166
class MappedObject:
@@ -111,7 +116,7 @@ class Base(object, metaclass=FilterableMetaclass):
111116

112117
properties = {}
113118

114-
def __init__(self, client, id, json={}):
119+
def __init__(self, client: object, id: object, json: object = {}) -> object:
115120
self._set("_populated", False)
116121
self._set("_last_updated", datetime.min)
117122
self._set("_client", client)
@@ -123,8 +128,8 @@ def __init__(self, client, id, json={}):
123128
#: be updated on access.
124129
self._set("_raw_json", None)
125130

126-
for prop in type(self).properties:
127-
self._set(prop, None)
131+
for k in type(self).properties:
132+
self._set(k, None)
128133

129134
self._set("id", id)
130135
if hasattr(type(self), "id_attribute"):
@@ -289,7 +294,7 @@ def _serialize(self):
289294

290295
value = getattr(self, k)
291296

292-
if not value:
297+
if not v.nullable and (value is None or value == ""):
293298
continue
294299

295300
# Let's allow explicit null values as both classes and instances
@@ -305,7 +310,7 @@ def _serialize(self):
305310
for k, v in result.items():
306311
if isinstance(v, Base):
307312
result[k] = v.id
308-
elif isinstance(v, MappedObject):
313+
elif isinstance(v, MappedObject) or issubclass(type(v), JSONObject):
309314
result[k] = v.dict
310315

311316
return result
@@ -376,6 +381,18 @@ def _populate(self, json):
376381
.properties[key]
377382
.slug_relationship(self._client, json[key]),
378383
)
384+
elif type(self).properties[key].json_class:
385+
json_class = type(self).properties[key].json_class
386+
json_value = json[key]
387+
388+
# build JSON object
389+
if isinstance(json_value, list):
390+
# We need special handling for list responses
391+
value = [json_class.from_json(v) for v in json_value]
392+
else:
393+
value = json_class.from_json(json_value)
394+
395+
self._set(key, value)
379396
elif type(json[key]) is dict:
380397
self._set(key, MappedObject(**json[key]))
381398
elif type(json[key]) is list:

0 commit comments

Comments
 (0)