pyixapi 0.2.7__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyixapi-0.2.7 → pyixapi-0.3.0}/PKG-INFO +6 -11
- {pyixapi-0.2.7 → pyixapi-0.3.0}/pyixapi/core/api.py +80 -16
- {pyixapi-0.2.7 → pyixapi-0.3.0}/pyixapi/core/endpoint.py +14 -7
- {pyixapi-0.2.7 → pyixapi-0.3.0}/pyixapi/core/query.py +40 -43
- {pyixapi-0.2.7 → pyixapi-0.3.0}/pyixapi/core/response.py +70 -54
- {pyixapi-0.2.7 → pyixapi-0.3.0}/pyixapi/core/token.py +1 -4
- {pyixapi-0.2.7 → pyixapi-0.3.0}/pyixapi/core/util.py +10 -10
- {pyixapi-0.2.7 → pyixapi-0.3.0}/pyixapi/models.py +113 -24
- pyixapi-0.3.0/pyixapi/py.typed +0 -0
- {pyixapi-0.2.7 → pyixapi-0.3.0}/pyproject.toml +24 -10
- pyixapi-0.2.7/LICENSE +0 -178
- {pyixapi-0.2.7 → pyixapi-0.3.0}/README.md +0 -0
- {pyixapi-0.2.7 → pyixapi-0.3.0}/pyixapi/__init__.py +0 -0
- {pyixapi-0.2.7 → pyixapi-0.3.0}/pyixapi/core/__init__.py +0 -0
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyixapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python API client library for IX-API
|
|
5
|
-
License-Expression: Apache-2.0
|
|
6
|
-
License-File: LICENSE
|
|
7
5
|
Author: Guillaume Mazoyer
|
|
8
|
-
Author-email: oss@mazoyer.eu
|
|
9
|
-
|
|
6
|
+
Author-email: Guillaume Mazoyer <oss@mazoyer.eu>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
10
8
|
Classifier: Intended Audience :: Developers
|
|
11
9
|
Classifier: Programming Language :: Python :: 3
|
|
12
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -14,11 +12,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
14
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
-
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-
|
|
20
|
-
Requires-Dist: requests (>=2.32.4,<3.0)
|
|
21
|
-
Requires-Dist: ruff (>=0.14,<0.15) ; extra == "dev"
|
|
15
|
+
Requires-Dist: pyjwt>=2.4.0,<2.14
|
|
16
|
+
Requires-Dist: requests>=2.32.4,<3.0
|
|
17
|
+
Requires-Python: >=3.10
|
|
22
18
|
Description-Content-Type: text/markdown
|
|
23
19
|
|
|
24
20
|
# pyixapi
|
|
@@ -79,4 +75,3 @@ DXDB:PAS:00008
|
|
|
79
75
|
```
|
|
80
76
|
|
|
81
77
|
Write queries are not implemented yet.
|
|
82
|
-
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import warnings
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
4
|
import requests
|
|
4
5
|
|
|
@@ -11,6 +12,7 @@ from pyixapi.models import (
|
|
|
11
12
|
IP,
|
|
12
13
|
MAC,
|
|
13
14
|
Account,
|
|
15
|
+
AvailabilityZone,
|
|
14
16
|
Connection,
|
|
15
17
|
Contact,
|
|
16
18
|
Demarc,
|
|
@@ -29,9 +31,10 @@ from pyixapi.models import (
|
|
|
29
31
|
ProductOffering,
|
|
30
32
|
Role,
|
|
31
33
|
RoleAssignment,
|
|
34
|
+
RoutingFunction,
|
|
32
35
|
)
|
|
33
36
|
|
|
34
|
-
__version__ = "0.
|
|
37
|
+
__version__ = "0.3.0"
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
class API(object):
|
|
@@ -44,14 +47,14 @@ class API(object):
|
|
|
44
47
|
|
|
45
48
|
def __init__(
|
|
46
49
|
self,
|
|
47
|
-
url,
|
|
48
|
-
key,
|
|
49
|
-
secret,
|
|
50
|
-
access_token="",
|
|
51
|
-
refresh_token="",
|
|
52
|
-
user_agent=f"pyixapi/{__version__}",
|
|
53
|
-
proxies=None,
|
|
54
|
-
):
|
|
50
|
+
url: str,
|
|
51
|
+
key: str,
|
|
52
|
+
secret: str,
|
|
53
|
+
access_token: str = "",
|
|
54
|
+
refresh_token: str = "",
|
|
55
|
+
user_agent: str = f"pyixapi/{__version__}",
|
|
56
|
+
proxies: dict[str, str] | None = None,
|
|
57
|
+
) -> None:
|
|
55
58
|
self.url = url.rstrip("/")
|
|
56
59
|
self.key = key
|
|
57
60
|
self.secret = secret
|
|
@@ -60,11 +63,11 @@ class API(object):
|
|
|
60
63
|
self.http_session = requests.Session()
|
|
61
64
|
self.user_agent = user_agent
|
|
62
65
|
self.proxies = proxies
|
|
66
|
+
self._version: int | None = None
|
|
63
67
|
|
|
64
68
|
self.auth = Endpoint(self, "auth")
|
|
65
69
|
self.connections = Endpoint(self, "connections", model=Connection)
|
|
66
70
|
self.contacts = Endpoint(self, "contacts", model=Contact)
|
|
67
|
-
self.demarcs = Endpoint(self, "demarcs", model=Demarc)
|
|
68
71
|
self.devices = Endpoint(self, "devices", model=Device)
|
|
69
72
|
self.facilities = Endpoint(self, "facilities", model=Facility)
|
|
70
73
|
self.ips = Endpoint(self, "ips", model=IP)
|
|
@@ -75,6 +78,7 @@ class API(object):
|
|
|
75
78
|
self.network_services = Endpoint(self, "network-services", model=NetworkService)
|
|
76
79
|
self.pops = Endpoint(self, "pops", model=PoP)
|
|
77
80
|
# Version 2+
|
|
81
|
+
self.availability_zones = Endpoint(self, "availability-zones", model=AvailabilityZone)
|
|
78
82
|
self.member_joining_rules = Endpoint(self, "member-joining-rules", model=MemberJoiningRule)
|
|
79
83
|
self.metro_areas = Endpoint(self, "metro-areas", model=MetroArea)
|
|
80
84
|
self.metro_area_networks = Endpoint(self, "metro-area-networks", model=MetroAreaNetwork)
|
|
@@ -82,12 +86,19 @@ class API(object):
|
|
|
82
86
|
self.port_reservations = Endpoint(self, "port-reservations", model=PortReservation)
|
|
83
87
|
self.roles = Endpoint(self, "roles", model=Role)
|
|
84
88
|
self.role_assignments = Endpoint(self, "role-assignments", model=RoleAssignment)
|
|
89
|
+
self.routing_functions = Endpoint(self, "routing-functions", model=RoutingFunction)
|
|
85
90
|
|
|
86
91
|
@property
|
|
87
92
|
def version(self) -> int:
|
|
88
93
|
"""
|
|
89
94
|
Get the API version of IX-API.
|
|
95
|
+
|
|
96
|
+
The version is resolved once on first access and cached for the lifetime
|
|
97
|
+
of the API instance, as it does not change between requests.
|
|
90
98
|
"""
|
|
99
|
+
if self._version is not None:
|
|
100
|
+
return self._version
|
|
101
|
+
|
|
91
102
|
version = Request(
|
|
92
103
|
base=self.url,
|
|
93
104
|
token=self.access_token,
|
|
@@ -103,21 +114,43 @@ class API(object):
|
|
|
103
114
|
stacklevel=2,
|
|
104
115
|
)
|
|
105
116
|
|
|
117
|
+
self._version = version
|
|
106
118
|
return version
|
|
107
119
|
|
|
108
120
|
@property
|
|
109
|
-
def accounts(self):
|
|
121
|
+
def accounts(self) -> Endpoint:
|
|
110
122
|
return Endpoint(self, "customers" if self.version == 1 else "accounts", model=Account)
|
|
111
123
|
|
|
112
124
|
@property
|
|
113
|
-
def
|
|
125
|
+
def demarcs(self) -> Endpoint:
|
|
126
|
+
if self.version != 1:
|
|
127
|
+
raise AttributeError("demarcs endpoint is only available in IX-API v1")
|
|
128
|
+
return Endpoint(self, "demarcs", model=Demarc)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def product_offerings(self) -> Endpoint:
|
|
114
132
|
return Endpoint(
|
|
115
133
|
self,
|
|
116
134
|
"products" if self.version == 1 else "product-offerings",
|
|
117
135
|
model=ProductOffering,
|
|
118
136
|
)
|
|
119
137
|
|
|
120
|
-
def
|
|
138
|
+
def account(self) -> Account:
|
|
139
|
+
"""
|
|
140
|
+
Get the authenticated user's own account.
|
|
141
|
+
|
|
142
|
+
Available in IX-API 2 or newer.
|
|
143
|
+
"""
|
|
144
|
+
r = Request(
|
|
145
|
+
base=cat(self.url, "account"),
|
|
146
|
+
token=self.access_token,
|
|
147
|
+
http_session=self.http_session,
|
|
148
|
+
user_agent=self.user_agent,
|
|
149
|
+
proxies=self.proxies,
|
|
150
|
+
)
|
|
151
|
+
return Account(r._make_call(), self, self.accounts)
|
|
152
|
+
|
|
153
|
+
def authenticate(self) -> Record | None:
|
|
121
154
|
"""
|
|
122
155
|
Authenticate and generate a pair of tokens.
|
|
123
156
|
|
|
@@ -147,13 +180,16 @@ class API(object):
|
|
|
147
180
|
|
|
148
181
|
return Record(r, self, self.auth)
|
|
149
182
|
|
|
150
|
-
def refresh_authentication(self):
|
|
183
|
+
def refresh_authentication(self) -> Record:
|
|
151
184
|
"""
|
|
152
185
|
Prolong authentication by refreshing the tokens pair.
|
|
153
186
|
"""
|
|
187
|
+
if not self.refresh_token:
|
|
188
|
+
raise ValueError("No refresh token available to refresh authentication")
|
|
189
|
+
|
|
154
190
|
r = Request(
|
|
155
191
|
cat(self.url, "auth", "refresh"),
|
|
156
|
-
token=self.refresh_token
|
|
192
|
+
token=self.refresh_token,
|
|
157
193
|
http_session=self.http_session,
|
|
158
194
|
user_agent=self.user_agent,
|
|
159
195
|
proxies=self.proxies,
|
|
@@ -164,7 +200,21 @@ class API(object):
|
|
|
164
200
|
|
|
165
201
|
return Record(r, self, self.auth)
|
|
166
202
|
|
|
167
|
-
def
|
|
203
|
+
def extensions(self) -> list[dict[str, Any]]:
|
|
204
|
+
"""
|
|
205
|
+
Get the list of extensions supported by the IX-API implementation.
|
|
206
|
+
|
|
207
|
+
Available in IX-API 2 or newer.
|
|
208
|
+
"""
|
|
209
|
+
return Request(
|
|
210
|
+
base=cat(self.url, "extensions"),
|
|
211
|
+
token=self.access_token,
|
|
212
|
+
http_session=self.http_session,
|
|
213
|
+
user_agent=self.user_agent,
|
|
214
|
+
proxies=self.proxies,
|
|
215
|
+
)._make_call()
|
|
216
|
+
|
|
217
|
+
def health(self) -> dict[str, Any]:
|
|
168
218
|
"""
|
|
169
219
|
Get the health information from IX-API.
|
|
170
220
|
|
|
@@ -180,3 +230,17 @@ class API(object):
|
|
|
180
230
|
user_agent=self.user_agent,
|
|
181
231
|
proxies=self.proxies,
|
|
182
232
|
).get_health()
|
|
233
|
+
|
|
234
|
+
def implementation(self) -> dict[str, Any]:
|
|
235
|
+
"""
|
|
236
|
+
Get implementation details of the IX-API server.
|
|
237
|
+
|
|
238
|
+
Available in IX-API 2 or newer.
|
|
239
|
+
"""
|
|
240
|
+
return Request(
|
|
241
|
+
base=cat(self.url, "implementation"),
|
|
242
|
+
token=self.access_token,
|
|
243
|
+
http_session=self.http_session,
|
|
244
|
+
user_agent=self.user_agent,
|
|
245
|
+
proxies=self.proxies,
|
|
246
|
+
)._make_call()
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
1
5
|
from pyixapi.core.query import Request, RequestError
|
|
2
6
|
from pyixapi.core.response import Record, RecordSet
|
|
3
7
|
from pyixapi.core.util import cat
|
|
4
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pyixapi.core.api import API
|
|
11
|
+
|
|
5
12
|
|
|
6
13
|
class Endpoint(object):
|
|
7
14
|
"""
|
|
@@ -11,16 +18,16 @@ class Endpoint(object):
|
|
|
11
18
|
object.
|
|
12
19
|
"""
|
|
13
20
|
|
|
14
|
-
def __init__(self, api, name, model=None):
|
|
15
|
-
self.return_obj = model if model else Record
|
|
21
|
+
def __init__(self, api: API, name: str, model: type[Record] | None = None) -> None:
|
|
22
|
+
self.return_obj: type[Record] = model if model else Record
|
|
16
23
|
self.api = api
|
|
17
24
|
self.url = cat(api.url, name)
|
|
18
25
|
self.name = name
|
|
19
26
|
|
|
20
|
-
def __str__(self):
|
|
27
|
+
def __str__(self) -> str:
|
|
21
28
|
return self.url
|
|
22
29
|
|
|
23
|
-
def all(self):
|
|
30
|
+
def all(self) -> RecordSet:
|
|
24
31
|
"""
|
|
25
32
|
Return all objects from an endpoint.
|
|
26
33
|
"""
|
|
@@ -33,7 +40,7 @@ class Endpoint(object):
|
|
|
33
40
|
)
|
|
34
41
|
return RecordSet(self, r)
|
|
35
42
|
|
|
36
|
-
def filter(self, *args, **kwargs):
|
|
43
|
+
def filter(self, *args: Any, **kwargs: Any) -> RecordSet:
|
|
37
44
|
"""
|
|
38
45
|
Query the list of a given endpoint. Also take named arguments that match the
|
|
39
46
|
usable filters on a given endpoint.
|
|
@@ -48,7 +55,7 @@ class Endpoint(object):
|
|
|
48
55
|
)
|
|
49
56
|
return RecordSet(self, r)
|
|
50
57
|
|
|
51
|
-
def get(self, *args, **kwargs):
|
|
58
|
+
def get(self, *args: Any, **kwargs: Any) -> Record | None:
|
|
52
59
|
"""
|
|
53
60
|
Return a single object from an endpoint.
|
|
54
61
|
"""
|
|
@@ -89,7 +96,7 @@ class Endpoint(object):
|
|
|
89
96
|
else:
|
|
90
97
|
raise e
|
|
91
98
|
|
|
92
|
-
def create(self, *args, **kwargs):
|
|
99
|
+
def create(self, *args: Any, **kwargs: Any) -> Record:
|
|
93
100
|
"""
|
|
94
101
|
Creates an object on an endpoint.
|
|
95
102
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from typing import Any, Generator
|
|
2
3
|
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from pyixapi.core.token import Token
|
|
3
7
|
from pyixapi.core.util import cat
|
|
4
8
|
|
|
5
9
|
|
|
@@ -10,7 +14,7 @@ class RequestError(Exception):
|
|
|
10
14
|
return is JSON we decode and add it to the message.
|
|
11
15
|
"""
|
|
12
16
|
|
|
13
|
-
def __init__(self, r):
|
|
17
|
+
def __init__(self, r: requests.Response) -> None:
|
|
14
18
|
if r.status_code == 404:
|
|
15
19
|
self.message = f"The requested url: {r.url} could not be found."
|
|
16
20
|
elif r.status_code == 401:
|
|
@@ -27,7 +31,7 @@ class RequestError(Exception):
|
|
|
27
31
|
self.req = r
|
|
28
32
|
self.error = r.text
|
|
29
33
|
|
|
30
|
-
def __str__(self):
|
|
34
|
+
def __str__(self) -> str:
|
|
31
35
|
return self.message
|
|
32
36
|
|
|
33
37
|
|
|
@@ -38,13 +42,13 @@ class ContentError(Exception):
|
|
|
38
42
|
those cases.
|
|
39
43
|
"""
|
|
40
44
|
|
|
41
|
-
def __init__(self, req):
|
|
45
|
+
def __init__(self, req: requests.Response) -> None:
|
|
42
46
|
super(ContentError, self).__init__(req)
|
|
43
47
|
|
|
44
48
|
self.req = req
|
|
45
49
|
self.error = "The server returned invalid (non-JSON) data. Maybe not an IX-API server?"
|
|
46
50
|
|
|
47
|
-
def __str__(self):
|
|
51
|
+
def __str__(self) -> str:
|
|
48
52
|
return self.error
|
|
49
53
|
|
|
50
54
|
|
|
@@ -55,22 +59,20 @@ class Request(object):
|
|
|
55
59
|
Responsible for building the URL and making the HTTP(S) requests to the API.
|
|
56
60
|
|
|
57
61
|
:param base: (str) Base URL passed in api() instantiation.
|
|
58
|
-
:param filters: (dict, optional)
|
|
59
|
-
|
|
60
|
-
In (e.g. /api/v1/devices?name='test') 'name': 'test' would be in the filters
|
|
61
|
-
dict.
|
|
62
|
+
:param filters: (dict, optional) key/value pairs matching the filters an
|
|
63
|
+
endpoint accepts, e.g. {"name": "test"} for /devices?name=test.
|
|
62
64
|
"""
|
|
63
65
|
|
|
64
66
|
def __init__(
|
|
65
67
|
self,
|
|
66
|
-
base,
|
|
67
|
-
http_session,
|
|
68
|
-
filters=None,
|
|
69
|
-
key=None,
|
|
70
|
-
token=None,
|
|
71
|
-
user_agent=None,
|
|
72
|
-
proxies=None,
|
|
73
|
-
):
|
|
68
|
+
base: str,
|
|
69
|
+
http_session: requests.Session,
|
|
70
|
+
filters: dict[str, Any] | None = None,
|
|
71
|
+
key: str | None = None,
|
|
72
|
+
token: Token | None = None,
|
|
73
|
+
user_agent: str | None = None,
|
|
74
|
+
proxies: dict[str, str] | None = None,
|
|
75
|
+
) -> None:
|
|
74
76
|
self.base = base
|
|
75
77
|
self.filters = filters or None
|
|
76
78
|
self.key = key
|
|
@@ -80,18 +82,7 @@ class Request(object):
|
|
|
80
82
|
self.user_agent = user_agent
|
|
81
83
|
self.proxies = proxies
|
|
82
84
|
|
|
83
|
-
def
|
|
84
|
-
"""
|
|
85
|
-
Get the OpenAPI Spec.
|
|
86
|
-
"""
|
|
87
|
-
headers = {"Content-Type": "application/json;"}
|
|
88
|
-
req = self.http_session.get(cat(self.base, "docs/?format=openapi"), headers=headers)
|
|
89
|
-
if req.ok:
|
|
90
|
-
return req.json()
|
|
91
|
-
else:
|
|
92
|
-
raise RequestError(req)
|
|
93
|
-
|
|
94
|
-
def get_version(self):
|
|
85
|
+
def get_version(self) -> int:
|
|
95
86
|
"""
|
|
96
87
|
Get the API version of IX-API.
|
|
97
88
|
|
|
@@ -105,32 +96,38 @@ class Request(object):
|
|
|
105
96
|
except RequestError:
|
|
106
97
|
return 1
|
|
107
98
|
|
|
108
|
-
def get_health(self):
|
|
99
|
+
def get_health(self) -> dict[str, Any]:
|
|
109
100
|
"""
|
|
110
101
|
Get the health from /api/health endpoint in IX-API.
|
|
111
102
|
"""
|
|
112
|
-
headers = {"Content-Type": "application/json;"}
|
|
103
|
+
headers: dict[str, str] = {"Content-Type": "application/json;"}
|
|
113
104
|
if self.token:
|
|
114
|
-
headers["Authorization"] = f"Bearer {self.token}"
|
|
105
|
+
headers["Authorization"] = f"Bearer {self.token.encoded}"
|
|
115
106
|
r = self.http_session.get(cat(self.base, "health"), headers=headers)
|
|
116
107
|
if r.ok:
|
|
117
108
|
return r.json()
|
|
118
109
|
else:
|
|
119
110
|
raise RequestError(r)
|
|
120
111
|
|
|
121
|
-
def _make_call(
|
|
122
|
-
|
|
123
|
-
|
|
112
|
+
def _make_call(
|
|
113
|
+
self,
|
|
114
|
+
verb: str = "get",
|
|
115
|
+
url_override: str | None = None,
|
|
116
|
+
add_params: dict[str, Any] | None = None,
|
|
117
|
+
data: dict[str, Any] | None = None,
|
|
118
|
+
) -> Any:
|
|
119
|
+
if verb in ("post", "put") or (verb == "delete" and data):
|
|
120
|
+
headers: dict[str, str] = {"Content-Type": "application/json;"}
|
|
124
121
|
else:
|
|
125
122
|
headers = {"accept": "application/json;"}
|
|
126
123
|
|
|
127
124
|
if self.token:
|
|
128
|
-
headers["Authorization"] = f"Bearer {self.token}"
|
|
125
|
+
headers["Authorization"] = f"Bearer {self.token.encoded}"
|
|
129
126
|
|
|
130
127
|
if self.user_agent:
|
|
131
128
|
headers["User-Agent"] = self.user_agent
|
|
132
129
|
|
|
133
|
-
params = {}
|
|
130
|
+
params: dict[str, Any] = {}
|
|
134
131
|
if not url_override:
|
|
135
132
|
if self.filters:
|
|
136
133
|
params.update(self.filters)
|
|
@@ -158,7 +155,7 @@ class Request(object):
|
|
|
158
155
|
else:
|
|
159
156
|
raise RequestError(r)
|
|
160
157
|
|
|
161
|
-
def get(self, add_params=None):
|
|
158
|
+
def get(self, add_params: dict[str, Any] | None = None) -> Generator[dict[str, Any], None, None]:
|
|
162
159
|
"""
|
|
163
160
|
Make a GET request to IX-API.
|
|
164
161
|
|
|
@@ -174,10 +171,10 @@ class Request(object):
|
|
|
174
171
|
for i in req:
|
|
175
172
|
yield i
|
|
176
173
|
else:
|
|
177
|
-
self.count =
|
|
174
|
+
self.count = 1
|
|
178
175
|
yield req
|
|
179
176
|
|
|
180
|
-
def put(self, data):
|
|
177
|
+
def put(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
181
178
|
"""
|
|
182
179
|
Make a PUT request to IX-API.
|
|
183
180
|
|
|
@@ -189,7 +186,7 @@ class Request(object):
|
|
|
189
186
|
"""
|
|
190
187
|
return self._make_call(verb="put", data=data)
|
|
191
188
|
|
|
192
|
-
def post(self, data):
|
|
189
|
+
def post(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
193
190
|
"""
|
|
194
191
|
Make a POST request to IX-API.
|
|
195
192
|
|
|
@@ -201,7 +198,7 @@ class Request(object):
|
|
|
201
198
|
"""
|
|
202
199
|
return self._make_call(verb="post", data=data)
|
|
203
200
|
|
|
204
|
-
def delete(self, data=None):
|
|
201
|
+
def delete(self, data: dict[str, Any] | None = None) -> bool:
|
|
205
202
|
"""
|
|
206
203
|
Make a DELETE request to IX-API.
|
|
207
204
|
|
|
@@ -215,7 +212,7 @@ class Request(object):
|
|
|
215
212
|
"""
|
|
216
213
|
return self._make_call(verb="delete", data=data)
|
|
217
214
|
|
|
218
|
-
def patch(self, data):
|
|
215
|
+
def patch(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
219
216
|
"""
|
|
220
217
|
Make a PATCH request to IX-API.
|
|
221
218
|
|
|
@@ -227,7 +224,7 @@ class Request(object):
|
|
|
227
224
|
"""
|
|
228
225
|
return self._make_call(verb="patch", data=data)
|
|
229
226
|
|
|
230
|
-
def options(self):
|
|
227
|
+
def options(self) -> dict[str, Any]:
|
|
231
228
|
"""
|
|
232
229
|
Make an OPTIONS request to IX-API.
|
|
233
230
|
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Iterator
|
|
4
|
+
|
|
1
5
|
from pyixapi.core.query import Request
|
|
2
|
-
from pyixapi.core.util import Hashabledict
|
|
6
|
+
from pyixapi.core.util import Hashabledict, cat
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pyixapi.core.api import API
|
|
10
|
+
from pyixapi.core.endpoint import Endpoint
|
|
3
11
|
|
|
4
12
|
|
|
5
|
-
def get_return(lookup):
|
|
13
|
+
def get_return(lookup: Any) -> Any:
|
|
6
14
|
"""
|
|
7
15
|
Return simple representations for items passed to lookup.
|
|
8
16
|
|
|
@@ -52,21 +60,21 @@ class RecordSet(object):
|
|
|
52
60
|
>>>
|
|
53
61
|
"""
|
|
54
62
|
|
|
55
|
-
def __init__(self, endpoint, request, **kwargs):
|
|
63
|
+
def __init__(self, endpoint: Endpoint, request: Request, **kwargs: Any) -> None:
|
|
56
64
|
self.endpoint = endpoint
|
|
57
65
|
self.request = request
|
|
58
66
|
self.response = self.request.get()
|
|
59
|
-
self._response_cache = []
|
|
67
|
+
self._response_cache: list[dict[str, Any]] = []
|
|
60
68
|
|
|
61
|
-
def __iter__(self):
|
|
69
|
+
def __iter__(self) -> Iterator[Record]:
|
|
62
70
|
return self
|
|
63
71
|
|
|
64
|
-
def __next__(self):
|
|
72
|
+
def __next__(self) -> Record:
|
|
65
73
|
if self._response_cache:
|
|
66
74
|
return self.endpoint.return_obj(self._response_cache.pop(), self.endpoint.api, self.endpoint)
|
|
67
75
|
return self.endpoint.return_obj(next(self.response), self.endpoint.api, self.endpoint)
|
|
68
76
|
|
|
69
|
-
def __len__(self):
|
|
77
|
+
def __len__(self) -> int:
|
|
70
78
|
try:
|
|
71
79
|
return self.request.count
|
|
72
80
|
except AttributeError:
|
|
@@ -82,11 +90,11 @@ class Record(object):
|
|
|
82
90
|
Create Python objects from IX-API responses.
|
|
83
91
|
|
|
84
92
|
Nested dicts that represent other endpoints are also turned into
|
|
85
|
-
:py:class:`.Record` objects
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
:py:class:`.Record` objects (when the corresponding model declares them as a
|
|
94
|
+
class attribute). All fields are then assigned to the object's attributes.
|
|
95
|
+
|
|
96
|
+
Only the fields present in the response are set as attributes; accessing a
|
|
97
|
+
field that was not returned raises :py:exc:`AttributeError`.
|
|
90
98
|
|
|
91
99
|
:examples:
|
|
92
100
|
Default representation of the object is usually its ID and/or name:
|
|
@@ -138,18 +146,17 @@ class Record(object):
|
|
|
138
146
|
>>>
|
|
139
147
|
"""
|
|
140
148
|
|
|
141
|
-
url = None
|
|
149
|
+
url: str | None = None
|
|
142
150
|
|
|
143
|
-
def __init__(self, values, api, endpoint):
|
|
144
|
-
self.
|
|
145
|
-
self._init_cache = []
|
|
151
|
+
def __init__(self, values: dict[str, Any], api: API, endpoint: Endpoint) -> None:
|
|
152
|
+
self._init_cache: list[tuple[str, Any]] = []
|
|
146
153
|
self.api = api
|
|
147
|
-
self.default_ret = Record
|
|
154
|
+
self.default_ret: type[Record] = Record
|
|
148
155
|
self.endpoint = endpoint
|
|
149
156
|
if values:
|
|
150
157
|
self._parse_values(values)
|
|
151
158
|
|
|
152
|
-
def __iter__(self):
|
|
159
|
+
def __iter__(self) -> Iterator[tuple[str, Any]]:
|
|
153
160
|
for i in dict(self._init_cache):
|
|
154
161
|
a = getattr(self, i)
|
|
155
162
|
if isinstance(a, Record):
|
|
@@ -159,53 +166,46 @@ class Record(object):
|
|
|
159
166
|
else:
|
|
160
167
|
yield i, a
|
|
161
168
|
|
|
162
|
-
def __getitem__(self, k):
|
|
169
|
+
def __getitem__(self, k: str) -> Any:
|
|
163
170
|
return dict(self)[k]
|
|
164
171
|
|
|
165
|
-
def __str__(self):
|
|
172
|
+
def __str__(self) -> str:
|
|
166
173
|
if hasattr(self, "id"):
|
|
167
174
|
return self.id
|
|
168
175
|
else:
|
|
169
176
|
return str(self.endpoint)
|
|
170
177
|
|
|
171
|
-
def __repr__(self):
|
|
178
|
+
def __repr__(self) -> str:
|
|
172
179
|
return str(dict(self))
|
|
173
180
|
|
|
174
|
-
def
|
|
175
|
-
return self.__dict__
|
|
176
|
-
|
|
177
|
-
def __setstate__(self, d):
|
|
178
|
-
self.__dict__.update(d)
|
|
179
|
-
|
|
180
|
-
def __key__(self):
|
|
181
|
+
def __key__(self) -> tuple[str, ...] | tuple[str]:
|
|
181
182
|
if hasattr(self, "id"):
|
|
182
183
|
return (self.endpoint.name, self.id)
|
|
183
184
|
else:
|
|
184
|
-
return self.endpoint.name
|
|
185
|
+
return (self.endpoint.name,)
|
|
185
186
|
|
|
186
|
-
def __hash__(self):
|
|
187
|
+
def __hash__(self) -> int:
|
|
187
188
|
return hash(self.__key__())
|
|
188
189
|
|
|
189
|
-
def __eq__(self, other):
|
|
190
|
+
def __eq__(self, other: object) -> bool:
|
|
190
191
|
if isinstance(other, Record):
|
|
191
192
|
return self.__key__() == other.__key__()
|
|
192
193
|
return NotImplemented
|
|
193
194
|
|
|
194
|
-
def _add_cache(self, item):
|
|
195
|
+
def _add_cache(self, item: tuple[str, Any]) -> None:
|
|
195
196
|
key, value = item
|
|
196
197
|
self._init_cache.append((key, get_return(value)))
|
|
197
198
|
|
|
198
|
-
def _parse_values(self, values):
|
|
199
|
+
def _parse_values(self, values: dict[str, Any]) -> None:
|
|
199
200
|
"""
|
|
200
201
|
Parse values dict at init and sets object attributes with the values within.
|
|
201
202
|
"""
|
|
202
203
|
|
|
203
|
-
def list_parser(key_name, list_item):
|
|
204
|
+
def list_parser(key_name: str, list_item: Any) -> Any:
|
|
204
205
|
if isinstance(list_item, dict):
|
|
205
206
|
lookup = getattr(self.__class__, key_name, None)
|
|
206
207
|
if not isinstance(lookup, list):
|
|
207
|
-
#
|
|
208
|
-
# a list (or is not defined), just return the default model
|
|
208
|
+
# Field not declared as a list model: use the default Record.
|
|
209
209
|
return self.default_ret(list_item, self.api, self.endpoint)
|
|
210
210
|
else:
|
|
211
211
|
model = lookup[0]
|
|
@@ -226,7 +226,7 @@ class Record(object):
|
|
|
226
226
|
self._add_cache((k, v))
|
|
227
227
|
setattr(self, k, v)
|
|
228
228
|
|
|
229
|
-
def serialize(self, nested=False, init=False):
|
|
229
|
+
def serialize(self, nested: bool = False, init: bool = False) -> Any:
|
|
230
230
|
"""
|
|
231
231
|
Pull all the attributes in an object and create a dict that can be turned into
|
|
232
232
|
the JSON that IX-API is expecting.
|
|
@@ -240,7 +240,7 @@ class Record(object):
|
|
|
240
240
|
if init:
|
|
241
241
|
init_vals = dict(self._init_cache)
|
|
242
242
|
|
|
243
|
-
r = {}
|
|
243
|
+
r: dict[str, Any] = {}
|
|
244
244
|
for i in dict(self):
|
|
245
245
|
current_val = getattr(self, i) if not init else init_vals.get(i)
|
|
246
246
|
if isinstance(current_val, Record):
|
|
@@ -250,19 +250,21 @@ class Record(object):
|
|
|
250
250
|
r[i] = current_val
|
|
251
251
|
return r
|
|
252
252
|
|
|
253
|
-
def _diff(self):
|
|
254
|
-
def
|
|
253
|
+
def _diff(self) -> set[str]:
|
|
254
|
+
def make_hashable(v: Any) -> Any:
|
|
255
|
+
# Hashable, structure-preserving form for set comparison. Lists become
|
|
256
|
+
# tuples, not a joined string (which would collide).
|
|
255
257
|
if isinstance(v, dict):
|
|
256
|
-
return k,
|
|
258
|
+
return Hashabledict({k: make_hashable(val) for k, val in v.items()})
|
|
257
259
|
if isinstance(v, list):
|
|
258
|
-
return
|
|
259
|
-
return
|
|
260
|
+
return tuple(make_hashable(i) for i in v)
|
|
261
|
+
return v
|
|
260
262
|
|
|
261
|
-
current =
|
|
262
|
-
init =
|
|
263
|
-
return
|
|
263
|
+
current = {k: make_hashable(v) for k, v in self.serialize().items()}
|
|
264
|
+
init = {k: make_hashable(v) for k, v in self.serialize(init=True).items()}
|
|
265
|
+
return {key for key, _ in set(current.items()) ^ set(init.items())}
|
|
264
266
|
|
|
265
|
-
def updates(self):
|
|
267
|
+
def updates(self) -> dict[str, Any]:
|
|
266
268
|
"""
|
|
267
269
|
Compile changes for an existing object into a dict.
|
|
268
270
|
|
|
@@ -276,7 +278,16 @@ class Record(object):
|
|
|
276
278
|
return {i: serialized[i] for i in diff}
|
|
277
279
|
return {}
|
|
278
280
|
|
|
279
|
-
def
|
|
281
|
+
def _make_request(self, sub_path: str) -> Request:
|
|
282
|
+
return Request(
|
|
283
|
+
base=cat(self.endpoint.url, self.id, sub_path),
|
|
284
|
+
token=self.api.access_token,
|
|
285
|
+
http_session=self.api.http_session,
|
|
286
|
+
user_agent=self.api.user_agent,
|
|
287
|
+
proxies=self.api.proxies,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def save(self) -> bool:
|
|
280
291
|
"""
|
|
281
292
|
Save changes to an existing object.
|
|
282
293
|
|
|
@@ -290,14 +301,19 @@ class Record(object):
|
|
|
290
301
|
base=self.endpoint.url,
|
|
291
302
|
token=self.api.access_token,
|
|
292
303
|
http_session=self.api.http_session,
|
|
293
|
-
user_agent=self.user_agent,
|
|
304
|
+
user_agent=self.api.user_agent,
|
|
294
305
|
proxies=self.api.proxies,
|
|
295
306
|
)
|
|
296
|
-
|
|
307
|
+
result = r.patch(updates)
|
|
308
|
+
if result:
|
|
309
|
+
# Refresh the record from the server response
|
|
310
|
+
if isinstance(result, dict):
|
|
311
|
+
self._init_cache = []
|
|
312
|
+
self._parse_values(result)
|
|
297
313
|
return True
|
|
298
314
|
return False
|
|
299
315
|
|
|
300
|
-
def update(self, data):
|
|
316
|
+
def update(self, data: dict[str, Any]) -> bool:
|
|
301
317
|
"""
|
|
302
318
|
Update an object with a dictionary.
|
|
303
319
|
|
|
@@ -309,7 +325,7 @@ class Record(object):
|
|
|
309
325
|
setattr(self, k, v)
|
|
310
326
|
return self.save()
|
|
311
327
|
|
|
312
|
-
def delete(self):
|
|
328
|
+
def delete(self) -> bool:
|
|
313
329
|
"""
|
|
314
330
|
Delete an existing object.
|
|
315
331
|
"""
|
|
@@ -318,7 +334,7 @@ class Record(object):
|
|
|
318
334
|
base=self.endpoint.url,
|
|
319
335
|
token=self.api.access_token,
|
|
320
336
|
http_session=self.api.http_session,
|
|
321
|
-
user_agent=self.user_agent,
|
|
337
|
+
user_agent=self.api.user_agent,
|
|
322
338
|
proxies=self.api.proxies,
|
|
323
339
|
)
|
|
324
|
-
return
|
|
340
|
+
return r.delete()
|
|
@@ -29,9 +29,6 @@ class Token:
|
|
|
29
29
|
self.encoded: str = token # Cache signed token data
|
|
30
30
|
self.expires_at: datetime = expires_at
|
|
31
31
|
|
|
32
|
-
def __str__(self) -> str:
|
|
33
|
-
return self.encoded
|
|
34
|
-
|
|
35
32
|
def __repr__(self) -> str:
|
|
36
33
|
return f"<Token ttl={self.ttl}s>"
|
|
37
34
|
|
|
@@ -49,7 +46,7 @@ class Token:
|
|
|
49
46
|
TTL is the number of seconds, from now, before the token expires.
|
|
50
47
|
"""
|
|
51
48
|
seconds = (self.expires_at - datetime.now(timezone.utc)).total_seconds()
|
|
52
|
-
return max(0, seconds)
|
|
49
|
+
return max(0, int(seconds))
|
|
53
50
|
|
|
54
51
|
@property
|
|
55
52
|
def is_expired(self) -> bool:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class Hashabledict(dict):
|
|
5
|
-
def __hash__(self):
|
|
4
|
+
class Hashabledict(dict): # type: ignore[type-arg]
|
|
5
|
+
def __hash__(self) -> int:
|
|
6
6
|
return hash(frozenset(self))
|
|
7
7
|
|
|
8
8
|
|
|
@@ -15,16 +15,16 @@ def cat(*args: Any, separator: str = "/", trailing: str = "") -> str:
|
|
|
15
15
|
|
|
16
16
|
If an item cannot be parsed as a string, an AttributeError will be raised.
|
|
17
17
|
|
|
18
|
-
>>>
|
|
19
|
-
'a/b/c/'
|
|
20
|
-
>>> concatenate("a", "b", "/c/", separator="")
|
|
18
|
+
>>> cat("a", "/b/", "c/")
|
|
21
19
|
'a/b/c'
|
|
22
|
-
>>>
|
|
23
|
-
'a/
|
|
24
|
-
>>>
|
|
25
|
-
'
|
|
20
|
+
>>> cat("a", 1, "b")
|
|
21
|
+
'a/1/b'
|
|
22
|
+
>>> cat("a", "b", "c", separator="_")
|
|
23
|
+
'a_b_c'
|
|
24
|
+
>>> cat("a", "b", "c", trailing="/")
|
|
25
|
+
'a/b/c/'
|
|
26
26
|
"""
|
|
27
|
-
s = separator.join([str(i).
|
|
27
|
+
s = separator.join([str(i).strip(separator) for i in args if str(i)])
|
|
28
28
|
if trailing:
|
|
29
29
|
s += trailing
|
|
30
30
|
return s
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ipaddress
|
|
2
|
+
from typing import Any
|
|
2
3
|
|
|
3
4
|
from pyixapi.core.response import Record
|
|
4
5
|
|
|
@@ -12,7 +13,7 @@ class Account(Record):
|
|
|
12
13
|
It is also referenced as "customer" in IX-API v1.
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
|
-
def __str__(self):
|
|
16
|
+
def __str__(self) -> str:
|
|
16
17
|
return f"{self.id}: {self.name}"
|
|
17
18
|
|
|
18
19
|
|
|
@@ -21,16 +22,31 @@ class Connection(Record):
|
|
|
21
22
|
A Connection is a group of physical ports collected together into a LAG.
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
|
-
def __str__(self):
|
|
25
|
+
def __str__(self) -> str:
|
|
25
26
|
return f"{self.id}: {self.name}"
|
|
26
27
|
|
|
28
|
+
def cancellation_policy(self) -> dict[str, Any]:
|
|
29
|
+
return self._make_request("cancellation-policy")._make_call()
|
|
30
|
+
|
|
31
|
+
def get_loa(self) -> dict[str, Any]:
|
|
32
|
+
return self._make_request("loa")._make_call()
|
|
33
|
+
|
|
34
|
+
def upload_loa(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
35
|
+
return self._make_request("loa")._make_call(verb="post", data=data)
|
|
36
|
+
|
|
37
|
+
def statistics(self, **kwargs: Any) -> dict[str, Any]:
|
|
38
|
+
return self._make_request("statistics")._make_call(add_params=kwargs or None)
|
|
39
|
+
|
|
40
|
+
def statistics_timeseries(self, aggregate: str, **kwargs: Any) -> dict[str, Any]:
|
|
41
|
+
return self._make_request(f"statistics/{aggregate}/timeseries")._make_call(add_params=kwargs or None)
|
|
42
|
+
|
|
27
43
|
|
|
28
44
|
class Contact(Record):
|
|
29
45
|
"""
|
|
30
46
|
A Contact is a role undertaking a specific responsibility within an account.
|
|
31
47
|
"""
|
|
32
48
|
|
|
33
|
-
def __str__(self):
|
|
49
|
+
def __str__(self) -> str:
|
|
34
50
|
r = []
|
|
35
51
|
|
|
36
52
|
legal_company_name = getattr(self, "legal_company_name", None)
|
|
@@ -51,7 +67,7 @@ class Demarc(Record):
|
|
|
51
67
|
eg a physical port / socket, generally with a specified bandwidth.
|
|
52
68
|
"""
|
|
53
69
|
|
|
54
|
-
def __str__(self):
|
|
70
|
+
def __str__(self) -> str:
|
|
55
71
|
return self.id
|
|
56
72
|
|
|
57
73
|
|
|
@@ -61,7 +77,7 @@ class Device(Record):
|
|
|
61
77
|
specified facility and inside a PoP.
|
|
62
78
|
"""
|
|
63
79
|
|
|
64
|
-
def __str__(self):
|
|
80
|
+
def __str__(self) -> str:
|
|
65
81
|
return self.name
|
|
66
82
|
|
|
67
83
|
|
|
@@ -71,7 +87,7 @@ class Facility(Record):
|
|
|
71
87
|
defined set of PoPs can be accessed.
|
|
72
88
|
"""
|
|
73
89
|
|
|
74
|
-
def __str__(self):
|
|
90
|
+
def __str__(self) -> str:
|
|
75
91
|
return self.name
|
|
76
92
|
|
|
77
93
|
|
|
@@ -81,18 +97,18 @@ class IP(Record):
|
|
|
81
97
|
"""
|
|
82
98
|
|
|
83
99
|
@property
|
|
84
|
-
def cidr(self):
|
|
100
|
+
def cidr(self) -> ipaddress.IPv4Interface | ipaddress.IPv6Interface:
|
|
85
101
|
return ipaddress.ip_interface(f"{self.address}/{self.prefix_length}")
|
|
86
102
|
|
|
87
103
|
@property
|
|
88
|
-
def ip(self):
|
|
104
|
+
def ip(self) -> ipaddress.IPv4Address | ipaddress.IPv6Address:
|
|
89
105
|
return self.cidr.ip
|
|
90
106
|
|
|
91
107
|
@property
|
|
92
|
-
def network(self):
|
|
108
|
+
def network(self) -> ipaddress.IPv4Network | ipaddress.IPv6Network:
|
|
93
109
|
return self.cidr.network
|
|
94
110
|
|
|
95
|
-
def __str__(self):
|
|
111
|
+
def __str__(self) -> str:
|
|
96
112
|
return str(self.cidr)
|
|
97
113
|
|
|
98
114
|
|
|
@@ -101,7 +117,7 @@ class MAC(Record):
|
|
|
101
117
|
A MAC is a MAC address with a given validity period.
|
|
102
118
|
"""
|
|
103
119
|
|
|
104
|
-
def __str__(self):
|
|
120
|
+
def __str__(self) -> str:
|
|
105
121
|
return self.address.lower()
|
|
106
122
|
|
|
107
123
|
|
|
@@ -110,7 +126,7 @@ class NetworkFeatureConfig(Record):
|
|
|
110
126
|
A NetworkFeatureConfig is a customer's configuration to use a NetworkFeature.
|
|
111
127
|
"""
|
|
112
128
|
|
|
113
|
-
def __str__(self):
|
|
129
|
+
def __str__(self) -> str:
|
|
114
130
|
return self.id
|
|
115
131
|
|
|
116
132
|
|
|
@@ -120,7 +136,7 @@ class NetworkFeature(Record):
|
|
|
120
136
|
NetworkService.
|
|
121
137
|
"""
|
|
122
138
|
|
|
123
|
-
def __str__(self):
|
|
139
|
+
def __str__(self) -> str:
|
|
124
140
|
return self.id
|
|
125
141
|
|
|
126
142
|
|
|
@@ -130,9 +146,24 @@ class NetworkServiceConfig(Record):
|
|
|
130
146
|
the configuration of a (subset of a) connection for that customer's traffic.
|
|
131
147
|
"""
|
|
132
148
|
|
|
133
|
-
def __str__(self):
|
|
149
|
+
def __str__(self) -> str:
|
|
134
150
|
return self.id
|
|
135
151
|
|
|
152
|
+
def cancellation_policy(self) -> dict[str, Any]:
|
|
153
|
+
return self._make_request("cancellation-policy")._make_call()
|
|
154
|
+
|
|
155
|
+
def peer_statistics(self, **kwargs: Any) -> dict[str, Any]:
|
|
156
|
+
return self._make_request("peer-statistics")._make_call(add_params=kwargs or None)
|
|
157
|
+
|
|
158
|
+
def peer_statistics_timeseries(self, aggregate: str, **kwargs: Any) -> dict[str, Any]:
|
|
159
|
+
return self._make_request(f"peer-statistics/{aggregate}/timeseries")._make_call(add_params=kwargs or None)
|
|
160
|
+
|
|
161
|
+
def statistics(self, **kwargs: Any) -> dict[str, Any]:
|
|
162
|
+
return self._make_request("statistics")._make_call(add_params=kwargs or None)
|
|
163
|
+
|
|
164
|
+
def statistics_timeseries(self, aggregate: str, **kwargs: Any) -> dict[str, Any]:
|
|
165
|
+
return self._make_request(f"statistics/{aggregate}/timeseries")._make_call(add_params=kwargs or None)
|
|
166
|
+
|
|
136
167
|
|
|
137
168
|
class NetworkService(Record):
|
|
138
169
|
"""
|
|
@@ -140,9 +171,30 @@ class NetworkService(Record):
|
|
|
140
171
|
depending on the type of product.
|
|
141
172
|
"""
|
|
142
173
|
|
|
143
|
-
def __str__(self):
|
|
174
|
+
def __str__(self) -> str:
|
|
144
175
|
return self.id
|
|
145
176
|
|
|
177
|
+
def cancellation_policy(self) -> dict[str, Any]:
|
|
178
|
+
return self._make_request("cancellation-policy")._make_call()
|
|
179
|
+
|
|
180
|
+
def change_request(self) -> dict[str, Any]:
|
|
181
|
+
return self._make_request("change-request")._make_call()
|
|
182
|
+
|
|
183
|
+
def create_change_request(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
184
|
+
return self._make_request("change-request")._make_call(verb="post", data=data)
|
|
185
|
+
|
|
186
|
+
def delete_change_request(self) -> bool:
|
|
187
|
+
return self._make_request("change-request")._make_call(verb="delete")
|
|
188
|
+
|
|
189
|
+
def rtt_statistics(self, **kwargs: Any) -> dict[str, Any]:
|
|
190
|
+
return self._make_request("rtt-statistics")._make_call(add_params=kwargs or None)
|
|
191
|
+
|
|
192
|
+
def statistics(self, **kwargs: Any) -> dict[str, Any]:
|
|
193
|
+
return self._make_request("statistics")._make_call(add_params=kwargs or None)
|
|
194
|
+
|
|
195
|
+
def statistics_timeseries(self, aggregate: str, **kwargs: Any) -> dict[str, Any]:
|
|
196
|
+
return self._make_request(f"statistics/{aggregate}/timeseries")._make_call(add_params=kwargs or None)
|
|
197
|
+
|
|
146
198
|
|
|
147
199
|
class PoP(Record):
|
|
148
200
|
"""
|
|
@@ -150,7 +202,7 @@ class PoP(Record):
|
|
|
150
202
|
infrastructure and has defined reachability of other facilities.
|
|
151
203
|
"""
|
|
152
204
|
|
|
153
|
-
def __str__(self):
|
|
205
|
+
def __str__(self) -> str:
|
|
154
206
|
return self.name
|
|
155
207
|
|
|
156
208
|
|
|
@@ -161,13 +213,22 @@ class ProductOffering(Record):
|
|
|
161
213
|
It is also referenced as "product" in IX-API v1.
|
|
162
214
|
"""
|
|
163
215
|
|
|
164
|
-
def __str__(self):
|
|
216
|
+
def __str__(self) -> str:
|
|
165
217
|
return self.name
|
|
166
218
|
|
|
167
219
|
|
|
168
220
|
# Version 2 and up
|
|
169
221
|
|
|
170
222
|
|
|
223
|
+
class AvailabilityZone(Record):
|
|
224
|
+
"""
|
|
225
|
+
An AvailabilityZone describes the physical location or zone of a resource.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
def __str__(self) -> str:
|
|
229
|
+
return self.name
|
|
230
|
+
|
|
231
|
+
|
|
171
232
|
class MemberJoiningRule(Record):
|
|
172
233
|
"""
|
|
173
234
|
A MemberJoiningRule defines a rule to allow or deny access for an Account to a
|
|
@@ -177,7 +238,7 @@ class MemberJoiningRule(Record):
|
|
|
177
238
|
listed in an allow rule.
|
|
178
239
|
"""
|
|
179
240
|
|
|
180
|
-
def __str__(self):
|
|
241
|
+
def __str__(self) -> str:
|
|
181
242
|
return self.id
|
|
182
243
|
|
|
183
244
|
|
|
@@ -186,7 +247,7 @@ class MetroArea(Record):
|
|
|
186
247
|
A MetroArea exists if a MetroAreaNetwork or Facility is present in it.
|
|
187
248
|
"""
|
|
188
249
|
|
|
189
|
-
def __str__(self):
|
|
250
|
+
def __str__(self) -> str:
|
|
190
251
|
return self.display_name
|
|
191
252
|
|
|
192
253
|
|
|
@@ -199,7 +260,7 @@ class MetroAreaNetwork(Record):
|
|
|
199
260
|
list of IDs.
|
|
200
261
|
"""
|
|
201
262
|
|
|
202
|
-
def __str__(self):
|
|
263
|
+
def __str__(self) -> str:
|
|
203
264
|
return self.name
|
|
204
265
|
|
|
205
266
|
|
|
@@ -209,9 +270,15 @@ class Port(Record):
|
|
|
209
270
|
associated with a device and pop, has a speed and a media_type.
|
|
210
271
|
"""
|
|
211
272
|
|
|
212
|
-
def __str__(self):
|
|
273
|
+
def __str__(self) -> str:
|
|
213
274
|
return self.name
|
|
214
275
|
|
|
276
|
+
def statistics(self, **kwargs: Any) -> dict[str, Any]:
|
|
277
|
+
return self._make_request("statistics")._make_call(add_params=kwargs or None)
|
|
278
|
+
|
|
279
|
+
def statistics_timeseries(self, aggregate: str, **kwargs: Any) -> dict[str, Any]:
|
|
280
|
+
return self._make_request(f"statistics/{aggregate}/timeseries")._make_call(add_params=kwargs or None)
|
|
281
|
+
|
|
215
282
|
|
|
216
283
|
class PortReservation(Record):
|
|
217
284
|
"""
|
|
@@ -223,16 +290,38 @@ class PortReservation(Record):
|
|
|
223
290
|
Please note that individual cancellation policies might apply.
|
|
224
291
|
"""
|
|
225
292
|
|
|
226
|
-
def __str__(self):
|
|
293
|
+
def __str__(self) -> str:
|
|
227
294
|
return self.id
|
|
228
295
|
|
|
296
|
+
def cancellation_policy(self) -> dict[str, Any]:
|
|
297
|
+
return self._make_request("cancellation-policy")._make_call()
|
|
298
|
+
|
|
299
|
+
def get_loa(self) -> dict[str, Any]:
|
|
300
|
+
return self._make_request("loa")._make_call()
|
|
301
|
+
|
|
302
|
+
def upload_loa(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
303
|
+
return self._make_request("loa")._make_call(verb="post", data=data)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class RoutingFunction(Record):
|
|
307
|
+
"""
|
|
308
|
+
A RoutingFunction is a function that provides routing capabilities within a
|
|
309
|
+
network service.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
def __str__(self) -> str:
|
|
313
|
+
return self.id
|
|
314
|
+
|
|
315
|
+
def cancellation_policy(self) -> dict[str, Any]:
|
|
316
|
+
return self._make_request("cancellation-policy")._make_call()
|
|
317
|
+
|
|
229
318
|
|
|
230
319
|
class Role(Record):
|
|
231
320
|
"""
|
|
232
321
|
A Role enables a Contact to act for a specific purpose.
|
|
233
322
|
"""
|
|
234
323
|
|
|
235
|
-
def __str__(self):
|
|
324
|
+
def __str__(self) -> str:
|
|
236
325
|
return self.name
|
|
237
326
|
|
|
238
327
|
|
|
@@ -241,5 +330,5 @@ class RoleAssignment(Record):
|
|
|
241
330
|
A Contact can be assigned to many Roles.
|
|
242
331
|
"""
|
|
243
332
|
|
|
244
|
-
def __str__(self):
|
|
333
|
+
def __str__(self) -> str:
|
|
245
334
|
return self.id
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyixapi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Python API client library for IX-API"
|
|
5
5
|
authors = [{ name = "Guillaume Mazoyer", email = "oss@mazoyer.eu" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -15,14 +15,10 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3.13",
|
|
16
16
|
"Programming Language :: Python :: 3.14",
|
|
17
17
|
]
|
|
18
|
-
dependencies = ["PyJWT>=2.4.0,<2.
|
|
18
|
+
dependencies = ["PyJWT>=2.4.0,<2.14", "requests>=2.32.4,<3.0"]
|
|
19
19
|
|
|
20
|
-
[
|
|
21
|
-
dev = ["pytest", "ruff
|
|
22
|
-
|
|
23
|
-
[tool.poetry]
|
|
24
|
-
requires-poetry = ">=2.0"
|
|
25
|
-
packages = [{ include = "pyixapi" }]
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = ["pytest", "pytest-cov", "ruff", "ty"]
|
|
26
22
|
|
|
27
23
|
[tool.ruff]
|
|
28
24
|
line-length = 120
|
|
@@ -57,6 +53,24 @@ known-first-party = ["pyixapi"]
|
|
|
57
53
|
[tool.ruff.lint.mccabe]
|
|
58
54
|
max-complexity = 31
|
|
59
55
|
|
|
56
|
+
[tool.ty.rules]
|
|
57
|
+
unresolved-attribute = "ignore"
|
|
58
|
+
invalid-return-type = "ignore"
|
|
59
|
+
unresolved-import = "ignore"
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
addopts = "--cov=pyixapi --cov-branch --cov-report=term-missing --cov-report=xml"
|
|
63
|
+
|
|
64
|
+
[tool.coverage.run]
|
|
65
|
+
source = ["pyixapi"]
|
|
66
|
+
omit = ["pyixapi/__main__.py"]
|
|
67
|
+
|
|
68
|
+
[tool.coverage.report]
|
|
69
|
+
exclude_lines = ["if TYPE_CHECKING:"]
|
|
70
|
+
|
|
60
71
|
[build-system]
|
|
61
|
-
requires = ["
|
|
62
|
-
build-backend = "
|
|
72
|
+
requires = ["uv_build>=0.9.21,<0.10.0"]
|
|
73
|
+
build-backend = "uv_build"
|
|
74
|
+
|
|
75
|
+
[tool.uv.build-backend]
|
|
76
|
+
module-root = "."
|
pyixapi-0.2.7/LICENSE
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
Apache License
|
|
3
|
-
Version 2.0, January 2004
|
|
4
|
-
http://www.apache.org/licenses/
|
|
5
|
-
|
|
6
|
-
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
-
|
|
8
|
-
1. Definitions.
|
|
9
|
-
|
|
10
|
-
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
-
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
-
|
|
13
|
-
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
-
the copyright owner that is granting the License.
|
|
15
|
-
|
|
16
|
-
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
-
other entities that control, are controlled by, or are under common
|
|
18
|
-
control with that entity. For the purposes of this definition,
|
|
19
|
-
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
-
direction or management of such entity, whether by contract or
|
|
21
|
-
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
-
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
-
|
|
24
|
-
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
-
exercising permissions granted by this License.
|
|
26
|
-
|
|
27
|
-
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
-
including but not limited to software source code, documentation
|
|
29
|
-
source, and configuration files.
|
|
30
|
-
|
|
31
|
-
"Object" form shall mean any form resulting from mechanical
|
|
32
|
-
transformation or translation of a Source form, including but
|
|
33
|
-
not limited to compiled object code, generated documentation,
|
|
34
|
-
and conversions to other media types.
|
|
35
|
-
|
|
36
|
-
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
-
Object form, made available under the License, as indicated by a
|
|
38
|
-
copyright notice that is included in or attached to the work
|
|
39
|
-
(an example is provided in the Appendix below).
|
|
40
|
-
|
|
41
|
-
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
-
form, that is based on (or derived from) the Work and for which the
|
|
43
|
-
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
-
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
-
of this License, Derivative Works shall not include works that remain
|
|
46
|
-
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
-
the Work and Derivative Works thereof.
|
|
48
|
-
|
|
49
|
-
"Contribution" shall mean any work of authorship, including
|
|
50
|
-
the original version of the Work and any modifications or additions
|
|
51
|
-
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
-
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
-
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
-
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
-
means any form of electronic, verbal, or written communication sent
|
|
56
|
-
to the Licensor or its representatives, including but not limited to
|
|
57
|
-
communication on electronic mailing lists, source code control systems,
|
|
58
|
-
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
-
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
-
excluding communication that is conspicuously marked or otherwise
|
|
61
|
-
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
-
|
|
63
|
-
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
-
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
-
subsequently incorporated within the Work.
|
|
66
|
-
|
|
67
|
-
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
-
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
-
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
-
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
-
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
-
Work and such Derivative Works in Source or Object form.
|
|
73
|
-
|
|
74
|
-
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
-
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
-
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
-
(except as stated in this section) patent license to make, have made,
|
|
78
|
-
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
-
where such license applies only to those patent claims licensable
|
|
80
|
-
by such Contributor that are necessarily infringed by their
|
|
81
|
-
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
-
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
-
institute patent litigation against any entity (including a
|
|
84
|
-
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
-
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
-
or contributory patent infringement, then any patent licenses
|
|
87
|
-
granted to You under this License for that Work shall terminate
|
|
88
|
-
as of the date such litigation is filed.
|
|
89
|
-
|
|
90
|
-
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
-
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
-
modifications, and in Source or Object form, provided that You
|
|
93
|
-
meet the following conditions:
|
|
94
|
-
|
|
95
|
-
(a) You must give any other recipients of the Work or
|
|
96
|
-
Derivative Works a copy of this License; and
|
|
97
|
-
|
|
98
|
-
(b) You must cause any modified files to carry prominent notices
|
|
99
|
-
stating that You changed the files; and
|
|
100
|
-
|
|
101
|
-
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
-
that You distribute, all copyright, patent, trademark, and
|
|
103
|
-
attribution notices from the Source form of the Work,
|
|
104
|
-
excluding those notices that do not pertain to any part of
|
|
105
|
-
the Derivative Works; and
|
|
106
|
-
|
|
107
|
-
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
-
distribution, then any Derivative Works that You distribute must
|
|
109
|
-
include a readable copy of the attribution notices contained
|
|
110
|
-
within such NOTICE file, excluding those notices that do not
|
|
111
|
-
pertain to any part of the Derivative Works, in at least one
|
|
112
|
-
of the following places: within a NOTICE text file distributed
|
|
113
|
-
as part of the Derivative Works; within the Source form or
|
|
114
|
-
documentation, if provided along with the Derivative Works; or,
|
|
115
|
-
within a display generated by the Derivative Works, if and
|
|
116
|
-
wherever such third-party notices normally appear. The contents
|
|
117
|
-
of the NOTICE file are for informational purposes only and
|
|
118
|
-
do not modify the License. You may add Your own attribution
|
|
119
|
-
notices within Derivative Works that You distribute, alongside
|
|
120
|
-
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
-
that such additional attribution notices cannot be construed
|
|
122
|
-
as modifying the License.
|
|
123
|
-
|
|
124
|
-
You may add Your own copyright statement to Your modifications and
|
|
125
|
-
may provide additional or different license terms and conditions
|
|
126
|
-
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
-
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
-
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
-
the conditions stated in this License.
|
|
130
|
-
|
|
131
|
-
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
-
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
-
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
-
this License, without any additional terms or conditions.
|
|
135
|
-
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
-
the terms of any separate license agreement you may have executed
|
|
137
|
-
with Licensor regarding such Contributions.
|
|
138
|
-
|
|
139
|
-
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
-
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
-
except as required for reasonable and customary use in describing the
|
|
142
|
-
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
-
|
|
144
|
-
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
-
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
-
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
-
implied, including, without limitation, any warranties or conditions
|
|
149
|
-
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
-
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
-
appropriateness of using or redistributing the Work and assume any
|
|
152
|
-
risks associated with Your exercise of permissions under this License.
|
|
153
|
-
|
|
154
|
-
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
-
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
-
unless required by applicable law (such as deliberate and grossly
|
|
157
|
-
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
-
liable to You for damages, including any direct, indirect, special,
|
|
159
|
-
incidental, or consequential damages of any character arising as a
|
|
160
|
-
result of this License or out of the use or inability to use the
|
|
161
|
-
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
-
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
-
other commercial damages or losses), even if such Contributor
|
|
164
|
-
has been advised of the possibility of such damages.
|
|
165
|
-
|
|
166
|
-
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
-
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
-
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
-
or other liability obligations and/or rights consistent with this
|
|
170
|
-
License. However, in accepting such obligations, You may act only
|
|
171
|
-
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
-
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
-
defend, and hold each Contributor harmless for any liability
|
|
174
|
-
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
-
of your accepting any such warranty or additional liability.
|
|
176
|
-
|
|
177
|
-
END OF TERMS AND CONDITIONS
|
|
178
|
-
Copyright 2017 DigitalOcean
|
|
File without changes
|
|
File without changes
|
|
File without changes
|