Skip to content

Commit e2d401f

Browse files
authored
Merge branch 'dev' into monitor-api
2 parents ed02e83 + 40c1630 commit e2d401f

21 files changed

+330
-44
lines changed

.github/workflows/e2e-test.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ jobs:
232232
steps:
233233
- name: Notify Slack
234234
id: main_message
235-
uses: slackapi/slack-github-action@v2.0.0
235+
uses: slackapi/slack-github-action@v2.1.0
236236
with:
237237
method: chat.postMessage
238238
token: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -264,7 +264,7 @@ jobs:
264264
265265
- name: Test summary thread
266266
if: success()
267-
uses: slackapi/slack-github-action@v2.0.0
267+
uses: slackapi/slack-github-action@v2.1.0
268268
with:
269269
method: chat.postMessage
270270
token: ${{ secrets.SLACK_BOT_TOKEN }}

.github/workflows/nightly-smoke-tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545

4646
- name: Notify Slack
4747
if: always() && github.repository == 'linode/linode_api4-python'
48-
uses: slackapi/slack-github-action@v2.0.0
48+
uses: slackapi/slack-github-action@v2.1.0
4949
with:
5050
method: chat.postMessage
5151
token: ${{ secrets.SLACK_BOT_TOKEN }}

.github/workflows/publish-pypi.yaml

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ on:
55
types: [ published ]
66
jobs:
77
pypi-release:
8+
permissions:
9+
# IMPORTANT: this permission is mandatory for trusted publishing
10+
id-token: write
811
runs-on: ubuntu-latest
12+
environment: pypi-release
913
steps:
1014
- name: Checkout
1115
uses: actions/checkout@v4
@@ -25,5 +29,3 @@ jobs:
2529

2630
- name: Publish the release artifacts to PyPI
2731
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # pin@release/v1.12.4
28-
with:
29-
password: ${{ secrets.PYPI_API_TOKEN }}

.github/workflows/release-notify-slack.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
steps:
1212
- name: Notify Slack - Main Message
1313
id: main_message
14-
uses: slackapi/slack-github-action@v2.0.0
14+
uses: slackapi/slack-github-action@v2.1.0
1515
with:
1616
method: chat.postMessage
1717
token: ${{ secrets.SLACK_BOT_TOKEN }}

linode_api4/groups/object_storage.py

+16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
ObjectStorageCluster,
2222
ObjectStorageKeyPermission,
2323
ObjectStorageKeys,
24+
ObjectStorageQuota,
2425
)
2526
from linode_api4.util import drop_null_keys
2627

@@ -517,3 +518,18 @@ def object_url_create(
517518
)
518519

519520
return MappedObject(**result)
521+
522+
def quotas(self, *filters):
523+
"""
524+
Lists the active ObjectStorage-related quotas applied to your account.
525+
526+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-object-storage-quotas
527+
528+
:param filters: Any number of filters to apply to this query.
529+
See :doc:`Filtering Collections</linode_api4/objects/filtering>`
530+
for more details on filtering.
531+
532+
:returns: A list of Object Storage Quotas that matched the query.
533+
:rtype: PaginatedList of ObjectStorageQuota
534+
"""
535+
return self.client._get_and_filter(ObjectStorageQuota, *filters)

linode_api4/objects/account.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ def entity(self):
601601
)
602602
return self.cls(self._client, self.id)
603603

604-
def _serialize(self):
604+
def _serialize(self, *args, **kwargs):
605605
"""
606606
Returns this grant in as JSON the api will accept. This is only relevant
607607
in the context of UserGrants.save
@@ -668,7 +668,7 @@ def _grants_dict(self):
668668

669669
return grants
670670

671-
def _serialize(self):
671+
def _serialize(self, *args, **kwargs):
672672
"""
673673
Returns the user grants in as JSON the api will accept.
674674
This is only relevant in the context of UserGrants.save

linode_api4/objects/base.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ def _flatten_base_subclass(obj: "Base") -> Optional[Dict[str, Any]]:
114114

115115
@property
116116
def dict(self):
117+
return self._serialize()
118+
119+
def _serialize(self, is_put: bool = False) -> Dict[str, Any]:
117120
result = vars(self).copy()
118121
cls = type(self)
119122

@@ -123,7 +126,7 @@ def dict(self):
123126
elif isinstance(v, list):
124127
result[k] = [
125128
(
126-
item.dict
129+
item._serialize(is_put=is_put)
127130
if isinstance(item, (cls, JSONObject))
128131
else (
129132
self._flatten_base_subclass(item)
@@ -136,7 +139,7 @@ def dict(self):
136139
elif isinstance(v, Base):
137140
result[k] = self._flatten_base_subclass(v)
138141
elif isinstance(v, JSONObject):
139-
result[k] = v.dict
142+
result[k] = v._serialize(is_put=is_put)
140143

141144
return result
142145

@@ -278,9 +281,9 @@ def save(self, force=True) -> bool:
278281
data[key] = None
279282

280283
# Ensure we serialize any values that may not be already serialized
281-
data = _flatten_request_body_recursive(data)
284+
data = _flatten_request_body_recursive(data, is_put=True)
282285
else:
283-
data = self._serialize()
286+
data = self._serialize(is_put=True)
284287

285288
resp = self._client.put(type(self).api_endpoint, model=self, data=data)
286289

@@ -316,7 +319,7 @@ def invalidate(self):
316319

317320
self._set("_populated", False)
318321

319-
def _serialize(self):
322+
def _serialize(self, is_put: bool = False):
320323
"""
321324
A helper method to build a dict of all mutable Properties of
322325
this object
@@ -345,7 +348,7 @@ def _serialize(self):
345348

346349
# Resolve the underlying IDs of results
347350
for k, v in result.items():
348-
result[k] = _flatten_request_body_recursive(v)
351+
result[k] = _flatten_request_body_recursive(v, is_put=is_put)
349352

350353
return result
351354

@@ -503,7 +506,7 @@ def make_instance(cls, id, client, parent_id=None, json=None):
503506
return Base.make(id, client, cls, parent_id=parent_id, json=json)
504507

505508

506-
def _flatten_request_body_recursive(data: Any) -> Any:
509+
def _flatten_request_body_recursive(data: Any, is_put: bool = False) -> Any:
507510
"""
508511
This is a helper recursively flatten the given data for use in an API request body.
509512
@@ -515,15 +518,18 @@ def _flatten_request_body_recursive(data: Any) -> Any:
515518
"""
516519

517520
if isinstance(data, dict):
518-
return {k: _flatten_request_body_recursive(v) for k, v in data.items()}
521+
return {
522+
k: _flatten_request_body_recursive(v, is_put=is_put)
523+
for k, v in data.items()
524+
}
519525

520526
if isinstance(data, list):
521-
return [_flatten_request_body_recursive(v) for v in data]
527+
return [_flatten_request_body_recursive(v, is_put=is_put) for v in data]
522528

523529
if isinstance(data, Base):
524530
return data.id
525531

526532
if isinstance(data, MappedObject) or issubclass(type(data), JSONObject):
527-
return data.dict
533+
return data._serialize(is_put=is_put)
528534

529535
return data

linode_api4/objects/linode.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ class ConfigInterface(JSONObject):
400400
def __repr__(self):
401401
return f"Interface: {self.purpose}"
402402

403-
def _serialize(self):
403+
def _serialize(self, *args, **kwargs):
404404
purpose_formats = {
405405
"public": {"purpose": "public", "primary": self.primary},
406406
"vlan": {
@@ -510,16 +510,16 @@ def _populate(self, json):
510510

511511
self._set("devices", MappedObject(**devices))
512512

513-
def _serialize(self):
513+
def _serialize(self, is_put: bool = False):
514514
"""
515515
Overrides _serialize to transform interfaces into json
516516
"""
517-
partial = DerivedBase._serialize(self)
517+
partial = DerivedBase._serialize(self, is_put=is_put)
518518
interfaces = []
519519

520520
for c in self.interfaces:
521521
if isinstance(c, ConfigInterface):
522-
interfaces.append(c._serialize())
522+
interfaces.append(c._serialize(is_put=is_put))
523523
else:
524524
interfaces.append(c)
525525

@@ -1927,8 +1927,8 @@ def _populate(self, json):
19271927
ndist = [Image(self._client, d) for d in self.images]
19281928
self._set("images", ndist)
19291929

1930-
def _serialize(self):
1931-
dct = Base._serialize(self)
1930+
def _serialize(self, is_put: bool = False):
1931+
dct = Base._serialize(self, is_put=is_put)
19321932
dct["images"] = [d.id for d in self.images]
19331933
return dct
19341934

linode_api4/objects/object_storage.py

+49
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ class ObjectStorageEndpoint(JSONObject):
5151
s3_endpoint: Optional[str] = None
5252

5353

54+
@dataclass
55+
class ObjectStorageQuotaUsage(JSONObject):
56+
"""
57+
ObjectStorageQuotaUsage contains the fields of an object storage quota usage information.
58+
"""
59+
60+
quota_limit: int = 0
61+
usage: int = 0
62+
63+
5464
class ObjectStorageType(Base):
5565
"""
5666
An ObjectStorageType represents the structure of a valid Object Storage type.
@@ -566,3 +576,42 @@ class ObjectStorageKeys(Base):
566576
"limited": Property(),
567577
"regions": Property(unordered=True),
568578
}
579+
580+
581+
class ObjectStorageQuota(Base):
582+
"""
583+
An Object Storage related quota information on your account.
584+
Object Storage Quota related features are under v4beta and may not currently be available to all users.
585+
586+
API documentation: https://techdocs.akamai.com/linode-api/reference/get-object-storage-quota
587+
"""
588+
589+
api_endpoint = "/object-storage/quotas/{quota_id}"
590+
id_attribute = "quota_id"
591+
592+
properties = {
593+
"quota_id": Property(identifier=True),
594+
"quota_name": Property(),
595+
"endpoint_type": Property(),
596+
"s3_endpoint": Property(),
597+
"description": Property(),
598+
"quota_limit": Property(),
599+
"resource_metric": Property(),
600+
}
601+
602+
def usage(self):
603+
"""
604+
Gets usage data for a specific ObjectStorage Quota resource you can have on your account and the current usage for that resource.
605+
606+
API documentation: https://techdocs.akamai.com/linode-api/reference/get-object-storage-quota-usage
607+
608+
:returns: The Object Storage Quota usage.
609+
:rtype: ObjectStorageQuotaUsage
610+
"""
611+
612+
result = self._client.get(
613+
f"{type(self).api_endpoint}/usage",
614+
model=self,
615+
)
616+
617+
return ObjectStorageQuotaUsage.from_json(result)

linode_api4/objects/serializable.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import inspect
2-
from dataclasses import dataclass
2+
from dataclasses import dataclass, fields
33
from enum import Enum
44
from types import SimpleNamespace
55
from typing import (
@@ -9,6 +9,7 @@
99
List,
1010
Optional,
1111
Set,
12+
Type,
1213
Union,
1314
get_args,
1415
get_origin,
@@ -71,6 +72,13 @@ class JSONObject(metaclass=JSONFilterableMetaclass):
7172
are None.
7273
"""
7374

75+
put_class: ClassVar[Optional[Type["JSONObject"]]] = None
76+
"""
77+
An alternative JSONObject class to use as the schema for PUT requests.
78+
This prevents read-only fields from being included in PUT request bodies,
79+
which in theory will result in validation errors from the API.
80+
"""
81+
7482
def __init__(self):
7583
raise NotImplementedError(
7684
"JSONObject is not intended to be constructed directly"
@@ -154,19 +162,25 @@ def from_json(cls, json: Dict[str, Any]) -> Optional["JSONObject"]:
154162

155163
return obj
156164

157-
def _serialize(self) -> Dict[str, Any]:
165+
def _serialize(self, is_put: bool = False) -> Dict[str, Any]:
158166
"""
159167
Serializes this object into a JSON dict.
160168
"""
161169
cls = type(self)
170+
171+
if is_put and cls.put_class is not None:
172+
cls = cls.put_class
173+
174+
cls_field_keys = {field.name for field in fields(cls)}
175+
162176
type_hints = get_type_hints(cls)
163177

164178
def attempt_serialize(value: Any) -> Any:
165179
"""
166180
Attempts to serialize the given value, else returns the value unchanged.
167181
"""
168182
if issubclass(type(value), JSONObject):
169-
return value._serialize()
183+
return value._serialize(is_put=is_put)
170184

171185
return value
172186

@@ -175,6 +189,10 @@ def should_include(key: str, value: Any) -> bool:
175189
Returns whether the given key/value pair should be included in the resulting dict.
176190
"""
177191

192+
# During PUT operations, keys not present in the put_class should be excluded
193+
if key not in cls_field_keys:
194+
return False
195+
178196
if cls.include_none_values or key in cls.always_include:
179197
return True
180198

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"data": [
3+
{
4+
"quota_id": "obj-objects-us-ord-1",
5+
"quota_name": "Object Storage Maximum Objects",
6+
"description": "Maximum number of Objects this customer is allowed to have on this endpoint.",
7+
"endpoint_type": "E1",
8+
"s3_endpoint": "us-iad-1.linodeobjects.com",
9+
"quota_limit": 50,
10+
"resource_metric": "object"
11+
},
12+
{
13+
"quota_id": "obj-bucket-us-ord-1",
14+
"quota_name": "Object Storage Maximum Buckets",
15+
"description": "Maximum number of buckets this customer is allowed to have on this endpoint.",
16+
"endpoint_type": "E1",
17+
"s3_endpoint": "us-iad-1.linodeobjects.com",
18+
"quota_limit": 50,
19+
"resource_metric": "bucket"
20+
}
21+
],
22+
"page": 1,
23+
"pages": 1,
24+
"results": 2
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"quota_id": "obj-objects-us-ord-1",
3+
"quota_name": "Object Storage Maximum Objects",
4+
"description": "Maximum number of Objects this customer is allowed to have on this endpoint.",
5+
"endpoint_type": "E1",
6+
"s3_endpoint": "us-iad-1.linodeobjects.com",
7+
"quota_limit": 50,
8+
"resource_metric": "object"
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"quota_limit": 100,
3+
"usage": 10
4+
}

0 commit comments

Comments
 (0)