pyixapi 0.2.0__tar.gz → 0.2.2__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.
Files changed (43) hide show
  1. {pyixapi-0.2.0/pyixapi.egg-info → pyixapi-0.2.2}/PKG-INFO +9 -9
  2. {pyixapi-0.2.0 → pyixapi-0.2.2}/README.md +0 -2
  3. pyixapi-0.2.2/pyixapi/__init__.py +5 -0
  4. pyixapi-0.2.2/pyixapi/__main__.py +69 -0
  5. {pyixapi-0.2.0 → pyixapi-0.2.2}/pyixapi/core/api.py +23 -18
  6. {pyixapi-0.2.0 → pyixapi-0.2.2}/pyixapi/core/endpoint.py +15 -8
  7. {pyixapi-0.2.0 → pyixapi-0.2.2}/pyixapi/core/query.py +29 -16
  8. {pyixapi-0.2.0 → pyixapi-0.2.2}/pyixapi/core/response.py +6 -6
  9. {pyixapi-0.2.0 → pyixapi-0.2.2}/pyixapi/core/token.py +26 -18
  10. {pyixapi-0.2.0 → pyixapi-0.2.2}/pyixapi/core/util.py +1 -1
  11. {pyixapi-0.2.0 → pyixapi-0.2.2}/pyixapi/models.py +4 -12
  12. pyixapi-0.2.2/pyproject.toml +78 -0
  13. pyixapi-0.2.0/.github/FUNDING.yml +0 -4
  14. pyixapi-0.2.0/.github/dependabot.yml +0 -19
  15. pyixapi-0.2.0/.github/workflows/publish.yml +0 -29
  16. pyixapi-0.2.0/.github/workflows/tests.yml +0 -26
  17. pyixapi-0.2.0/.gitignore +0 -102
  18. pyixapi-0.2.0/PKG-INFO +0 -79
  19. pyixapi-0.2.0/docs/Makefile +0 -17
  20. pyixapi-0.2.0/docs/advanced.rst +0 -74
  21. pyixapi-0.2.0/docs/conf.py +0 -158
  22. pyixapi-0.2.0/docs/endpoint.rst +0 -5
  23. pyixapi-0.2.0/docs/index.rst +0 -29
  24. pyixapi-0.2.0/docs/request.rst +0 -8
  25. pyixapi-0.2.0/docs/response.rst +0 -8
  26. pyixapi-0.2.0/pyixapi/__init__.py +0 -11
  27. pyixapi-0.2.0/pyixapi.egg-info/SOURCES.txt +0 -38
  28. pyixapi-0.2.0/pyixapi.egg-info/dependency_links.txt +0 -1
  29. pyixapi-0.2.0/pyixapi.egg-info/not-zip-safe +0 -1
  30. pyixapi-0.2.0/pyixapi.egg-info/requires.txt +0 -2
  31. pyixapi-0.2.0/pyixapi.egg-info/top_level.txt +0 -1
  32. pyixapi-0.2.0/pyproject.toml +0 -8
  33. pyixapi-0.2.0/requirements-dev.txt +0 -2
  34. pyixapi-0.2.0/requirements.txt +0 -2
  35. pyixapi-0.2.0/setup.cfg +0 -4
  36. pyixapi-0.2.0/setup.py +0 -33
  37. pyixapi-0.2.0/tests/__init__.py +0 -0
  38. pyixapi-0.2.0/tests/fixtures/api/authenticate.json +0 -4
  39. pyixapi-0.2.0/tests/fixtures/api/refresh_authentication.json +0 -4
  40. pyixapi-0.2.0/tests/test_api.py +0 -86
  41. pyixapi-0.2.0/tests/util.py +0 -15
  42. {pyixapi-0.2.0 → pyixapi-0.2.2}/LICENSE +0 -0
  43. {pyixapi-0.2.0 → pyixapi-0.2.2}/pyixapi/core/__init__.py +0 -0
@@ -1,21 +1,22 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyixapi
3
- Version: 0.2.0
4
- Summary: IX-API client library
5
- Home-page: https://github.com/peering-manager/pyixapi
3
+ Version: 0.2.2
4
+ Summary: Python API client library for IX-API
5
+ License: Apache-2.0
6
6
  Author: Guillaume Mazoyer
7
7
  Author-email: oss@mazoyer.eu
8
- License: Apache2
9
- Keywords: ix-api,internet-exchange,peering
8
+ Requires-Python: >=3.8,<4.0
10
9
  Classifier: Intended Audience :: Developers
11
- Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: License :: OSI Approved :: Apache Software License
12
11
  Classifier: Programming Language :: Python :: 3
13
12
  Classifier: Programming Language :: Python :: 3.8
14
13
  Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Dist: PyJWT (>=2.4.0,<2.9)
18
+ Requires-Dist: requests (>=2.20.0,<3.0)
17
19
  Description-Content-Type: text/markdown
18
- License-File: LICENSE
19
20
 
20
21
  # pyixapi
21
22
 
@@ -30,8 +31,6 @@ Supported versions are:
30
31
 
31
32
  To install run `pip install pyixapi`.
32
33
 
33
- Alternatively, you can clone the repo and run `python setup.py install`.
34
-
35
34
  ## Quick Start
36
35
 
37
36
  To begin, import pyixapi and instantiate the API.
@@ -77,3 +76,4 @@ DXDB:PAS:00008
77
76
  ```
78
77
 
79
78
  Write queries are not implemented yet.
79
+
@@ -11,8 +11,6 @@ Supported versions are:
11
11
 
12
12
  To install run `pip install pyixapi`.
13
13
 
14
- Alternatively, you can clone the repo and run `python setup.py install`.
15
-
16
14
  ## Quick Start
17
15
 
18
16
  To begin, import pyixapi and instantiate the API.
@@ -0,0 +1,5 @@
1
+ from .core.api import API as api
2
+ from .core.api import __version__ # noqa: F401
3
+ from .core.query import ContentError, RequestError
4
+
5
+ __all__ = ("api", "ContentError", "RequestError")
@@ -0,0 +1,69 @@
1
+ from pyixapi import api
2
+
3
+
4
+ def create_mac_address(ixapi, mac_address):
5
+ """
6
+ Create a new MAC address in IX-API. If the MAC already exists, return it
7
+ without creation.
8
+ """
9
+ # Make sure to have a consistent case
10
+ mac_address = str(mac_address).lower()
11
+
12
+ # Return existing object if MAC already exists
13
+ for mac in ixapi.macs.all():
14
+ if mac["address"] == mac_address:
15
+ return mac
16
+
17
+ return ixapi.macs.create(
18
+ address=mac_address, consuming_customer="2", managing_customer="2"
19
+ )
20
+
21
+
22
+ # ixapi = api(
23
+ # "https://api.de-cix.net/api/v1/",
24
+ # "-GnNlMD8hBuxSSUJmpbfUkss9dyOKfTV1SnZibNyyr4",
25
+ # "XKq8M6NVh5lCbPJ2Ml1h7V93QNIMsGVBfM6g2nRZF-E",
26
+ # )
27
+
28
+ ixapi = api(
29
+ "https://api.de-cix.net/api/v2/",
30
+ "lZJ_sV59gydj12RFATG4NaQFRI15iKRi9RC0oDqn2_A",
31
+ "Yt_RImR_ymuhx2r9YQXerh5q9EyE6TxaREjzmEQNr1o",
32
+ )
33
+
34
+ # ixapi = api(
35
+ # "http://localhost:8000/api/v1/",
36
+ # "c25f077c899694fc",
37
+ # "zZm-hzDIR5BZUhn5ULo4-_BQyx39ayRRn_0HFaSNDShqunrTiNY4a6n0Bnjqg62F-NsywRLvn3Dt3pRXWRgS1A",
38
+ # )
39
+
40
+ authentication = ixapi.authenticate()
41
+ print(authentication)
42
+
43
+ print(f"IX-API is at version {ixapi.version}")
44
+
45
+ authentication = ixapi.authenticate()
46
+ print(authentication)
47
+
48
+ print([i for i in ixapi.accounts.all()])
49
+
50
+ # for i in ixapi.ips.all():
51
+ # print(f"{i.ip} - {i.network}")
52
+ # print(ixapi.network_service_configs.get("DXDB:PAS:509177"))
53
+
54
+ # for i in ixapi.ports.all():
55
+ # print(i)
56
+
57
+ # print(len(ixapi.network_services.filter(consuming_customer="DXDB:CUST:1348")))
58
+ # print(len(ixapi.network_services.all()))
59
+
60
+ # mac = create_mac_address(ixapi, "e4:f7:ad:58:87:ff")
61
+
62
+ # for i in ixapi.macs.all():
63
+ # print(dict(i))
64
+
65
+ # for i in ixapi.network_service_configs.all():
66
+ # # i.macs = [mac]
67
+ # # print(i.save())
68
+ # i.update({"macs": [mac]})
69
+ # print(dict(i))
@@ -1,3 +1,5 @@
1
+ from typing import Any
2
+
1
3
  import requests
2
4
 
3
5
  from pyixapi.core.endpoint import Endpoint
@@ -24,13 +26,12 @@ from pyixapi.models import (
24
26
  PoP,
25
27
  Port,
26
28
  PortReservation,
27
- Product,
28
29
  ProductOffering,
29
30
  Role,
30
31
  RoleAssignment,
31
32
  )
32
33
 
33
- __version__ = "0.2.0"
34
+ __version__ = "0.2.2"
34
35
 
35
36
 
36
37
  class API(object):
@@ -43,13 +44,13 @@ class API(object):
43
44
 
44
45
  def __init__(
45
46
  self,
46
- url,
47
- key,
48
- secret,
49
- access_token="",
50
- refresh_token="",
51
- user_agent=f"pyixapi/{__version__}",
52
- ):
47
+ url: str,
48
+ key: str,
49
+ secret: str,
50
+ access_token: str = "",
51
+ refresh_token: str = "",
52
+ user_agent: str = f"pyixapi/{__version__}",
53
+ ) -> None:
53
54
  self.url = url.rstrip("/")
54
55
  self.key = key
55
56
  self.secret = secret
@@ -75,7 +76,6 @@ class API(object):
75
76
  )
76
77
  self.network_services = Endpoint(self, "network-services", model=NetworkService)
77
78
  self.pops = Endpoint(self, "pops", model=PoP)
78
- self.products = Endpoint(self, "products", model=Product)
79
79
  # Version 2+
80
80
  self.member_joining_rules = Endpoint(
81
81
  self, "member-joining-rules", model=MemberJoiningRule
@@ -88,14 +88,11 @@ class API(object):
88
88
  self.port_reservations = Endpoint(
89
89
  self, "port-reservations", model=PortReservation
90
90
  )
91
- self.product_offerings = Endpoint(
92
- self, "product-offerings", model=ProductOffering
93
- )
94
91
  self.roles = Endpoint(self, "roles", model=Role)
95
92
  self.role_assignments = Endpoint(self, "role-assignments", model=RoleAssignment)
96
93
 
97
94
  @property
98
- def version(self):
95
+ def version(self) -> int:
99
96
  """
100
97
  Get the API version of IX-API.
101
98
  """
@@ -104,12 +101,20 @@ class API(object):
104
101
  ).get_version()
105
102
 
106
103
  @property
107
- def accounts(self):
104
+ def accounts(self) -> Endpoint:
108
105
  return Endpoint(
109
106
  self, "customers" if self.version == 1 else "accounts", model=Account
110
107
  )
111
108
 
112
- def authenticate(self):
109
+ @property
110
+ def product_offerings(self) -> Endpoint:
111
+ return Endpoint(
112
+ self,
113
+ "products" if self.version == 1 else "product-offerings",
114
+ model=ProductOffering,
115
+ )
116
+
117
+ def authenticate(self) -> Record:
113
118
  """
114
119
  Authenticate and generate a pair of tokens.
115
120
 
@@ -136,7 +141,7 @@ class API(object):
136
141
 
137
142
  return Record(r, self, self.auth)
138
143
 
139
- def refresh_authentication(self):
144
+ def refresh_authentication(self) -> Record:
140
145
  """
141
146
  Prolong authentication by refreshing the tokens pair.
142
147
  """
@@ -151,7 +156,7 @@ class API(object):
151
156
 
152
157
  return Record(r, self, self.auth)
153
158
 
154
- def health(self):
159
+ def health(self) -> dict[str, Any]:
155
160
  """
156
161
  Get the health information from IX-API.
157
162
 
@@ -1,9 +1,14 @@
1
+ from typing import TYPE_CHECKING, Optional
2
+
1
3
  from pyixapi.core.query import Request, RequestError
2
4
  from pyixapi.core.response import Record, RecordSet
3
5
  from pyixapi.core.util import cat
4
6
 
7
+ if TYPE_CHECKING:
8
+ from .api import API
9
+
5
10
 
6
- class Endpoint(object):
11
+ class Endpoint:
7
12
  """
8
13
  Represent actions available on endpoints in the IX-API.
9
14
 
@@ -11,16 +16,16 @@ class Endpoint(object):
11
16
  object.
12
17
  """
13
18
 
14
- def __init__(self, api, name, model=None):
19
+ def __init__(self, api: "API", name: str, model: Optional[Record] = None) -> None:
15
20
  self.return_obj = model if model else Record
16
21
  self.api = api
17
22
  self.url = cat(api.url, name)
18
23
  self.name = name
19
24
 
20
- def __str__(self):
25
+ def __str__(self) -> str:
21
26
  return self.url
22
27
 
23
- def all(self):
28
+ def all(self) -> RecordSet:
24
29
  """
25
30
  Return all objects from an endpoint.
26
31
  """
@@ -31,7 +36,7 @@ class Endpoint(object):
31
36
  )
32
37
  return RecordSet(self, r)
33
38
 
34
- def filter(self, *args, **kwargs):
39
+ def filter(self, *args, **kwargs) -> RecordSet:
35
40
  """
36
41
  Query the list of a given endpoint. Also take named arguments that match the
37
42
  usable filters on a given endpoint.
@@ -44,7 +49,7 @@ class Endpoint(object):
44
49
  )
45
50
  return RecordSet(self, r)
46
51
 
47
- def get(self, *args, **kwargs):
52
+ def get(self, *args, **kwargs) -> Optional[Record]:
48
53
  """
49
54
  Return a single object from an endpoint.
50
55
  """
@@ -62,7 +67,9 @@ class Endpoint(object):
62
67
  try:
63
68
  next(response)
64
69
  raise ValueError(
65
- "get() returned more than one result. Check that the kwarg(s) passed are valid for this endpoint or use filter() or all() instead."
70
+ "get() returned more than one result. Check that the kwarg(s) "
71
+ "passed are valid for this endpoint or use filter() or all() "
72
+ "instead."
66
73
  )
67
74
  except StopIteration:
68
75
  return value
@@ -81,7 +88,7 @@ class Endpoint(object):
81
88
  else:
82
89
  raise e
83
90
 
84
- def create(self, *args, **kwargs):
91
+ def create(self, *args, **kwargs) -> Record:
85
92
  """
86
93
  Creates an object on an endpoint.
87
94
 
@@ -1,4 +1,5 @@
1
1
  import json
2
+ from typing import Any, Optional
2
3
 
3
4
  from pyixapi.core.util import cat
4
5
 
@@ -19,15 +20,21 @@ class RequestError(Exception):
19
20
  )
20
21
  else:
21
22
  try:
22
- self.message = f"The request failed with code {r.status_code} {r.reason}: {r.json()}"
23
+ self.message = (
24
+ f"The request failed with code {r.status_code} "
25
+ f"{r.reason}: {r.json()}"
26
+ )
23
27
  except ValueError:
24
- self.message = f"The request failed with code {r.status_code} {r.reason} but details were not found as JSON."
28
+ self.message = (
29
+ f"The request failed with code {r.status_code} "
30
+ f"{r.reason} but details were not found as JSON."
31
+ )
25
32
 
26
33
  super(RequestError, self).__init__(r)
27
34
  self.req = r
28
35
  self.error = r.text
29
36
 
30
- def __str__(self):
37
+ def __str__(self) -> str:
31
38
  return self.message
32
39
 
33
40
 
@@ -38,7 +45,7 @@ class ContentError(Exception):
38
45
  those cases.
39
46
  """
40
47
 
41
- def __init__(self, req):
48
+ def __init__(self, req) -> None:
42
49
  super(ContentError, self).__init__(req)
43
50
 
44
51
  self.req = req
@@ -46,11 +53,11 @@ class ContentError(Exception):
46
53
  "The server returned invalid (non-JSON) data. Maybe not an IX-API server?"
47
54
  )
48
55
 
49
- def __str__(self):
56
+ def __str__(self) -> str:
50
57
  return self.error
51
58
 
52
59
 
53
- class Request(object):
60
+ class Request:
54
61
  """
55
62
  Create requests to the IX-API.
56
63
 
@@ -74,7 +81,7 @@ class Request(object):
74
81
  self.url = self.base if not key else cat(self.base, key)
75
82
  self.user_agent = user_agent
76
83
 
77
- def get_openapi(self):
84
+ def get_openapi(self) -> Any:
78
85
  """
79
86
  Get the OpenAPI Spec.
80
87
  """
@@ -87,7 +94,7 @@ class Request(object):
87
94
  else:
88
95
  raise RequestError(req)
89
96
 
90
- def get_version(self):
97
+ def get_version(self) -> int:
91
98
  """
92
99
  Get the API version of IX-API.
93
100
 
@@ -101,7 +108,7 @@ class Request(object):
101
108
  except RequestError:
102
109
  return 1
103
110
 
104
- def get_health(self):
111
+ def get_health(self) -> dict[str, Any]:
105
112
  """
106
113
  Get the health from /api/health endpoint in IX-API.
107
114
  """
@@ -114,7 +121,13 @@ class Request(object):
114
121
  else:
115
122
  raise RequestError(r)
116
123
 
117
- def _make_call(self, verb="get", url_override=None, add_params=None, data=None):
124
+ def _make_call(
125
+ self,
126
+ verb: str = "get",
127
+ url_override: Optional[str] = None,
128
+ add_params: Optional[dict[str, Any]] = None,
129
+ data: Optional[Any] = None,
130
+ ) -> Any:
118
131
  if verb in ("post", "put") or verb == "delete" and data:
119
132
  headers = {"Content-Type": "application/json;"}
120
133
  else:
@@ -147,7 +160,7 @@ class Request(object):
147
160
  else:
148
161
  raise RequestError(r)
149
162
 
150
- def get(self, add_params=None):
163
+ def get(self, add_params: Optional[dict[str, Any]] = None) -> Any:
151
164
  """
152
165
  Make a GET request to IX-API.
153
166
 
@@ -166,7 +179,7 @@ class Request(object):
166
179
  self.count = len(req)
167
180
  yield req
168
181
 
169
- def put(self, data):
182
+ def put(self, data: Any) -> Any:
170
183
  """
171
184
  Make a PUT request to IX-API.
172
185
 
@@ -178,7 +191,7 @@ class Request(object):
178
191
  """
179
192
  return self._make_call(verb="put", data=data)
180
193
 
181
- def post(self, data):
194
+ def post(self, data: Any) -> Any:
182
195
  """
183
196
  Make a POST request to IX-API.
184
197
 
@@ -190,7 +203,7 @@ class Request(object):
190
203
  """
191
204
  return self._make_call(verb="post", data=data)
192
205
 
193
- def delete(self, data=None):
206
+ def delete(self, data: Optional[Any] = None) -> Any:
194
207
  """
195
208
  Make a DELETE request to IX-API.
196
209
 
@@ -204,7 +217,7 @@ class Request(object):
204
217
  """
205
218
  return self._make_call(verb="delete", data=data)
206
219
 
207
- def patch(self, data):
220
+ def patch(self, data: Any) -> Any:
208
221
  """
209
222
  Make a PATCH request to IX-API.
210
223
 
@@ -216,7 +229,7 @@ class Request(object):
216
229
  """
217
230
  return self._make_call(verb="patch", data=data)
218
231
 
219
- def options(self):
232
+ def options(self) -> Any:
220
233
  """
221
234
  Make an OPTIONS request to IX-API.
222
235
 
@@ -2,7 +2,7 @@ from pyixapi.core.query import Request
2
2
  from pyixapi.core.util import Hashabledict
3
3
 
4
4
 
5
- def get_return(lookup):
5
+ def get_return(lookup) -> int:
6
6
  """
7
7
  Return simple representations for items passed to lookup.
8
8
 
@@ -15,7 +15,7 @@ def get_return(lookup):
15
15
  return lookup
16
16
 
17
17
 
18
- class RecordSet(object):
18
+ class RecordSet:
19
19
  """
20
20
  Iterator containing :py:class:`.Record` objects.
21
21
 
@@ -52,13 +52,13 @@ class RecordSet(object):
52
52
  >>>
53
53
  """
54
54
 
55
- def __init__(self, endpoint, request, **kwargs):
55
+ def __init__(self, endpoint, request, **kwargs) -> None:
56
56
  self.endpoint = endpoint
57
57
  self.request = request
58
58
  self.response = self.request.get()
59
59
  self._response_cache = []
60
60
 
61
- def __iter__(self):
61
+ def __iter__(self) -> "RecordSet":
62
62
  return self
63
63
 
64
64
  def __next__(self):
@@ -81,7 +81,7 @@ class RecordSet(object):
81
81
  return self.request.count
82
82
 
83
83
 
84
- class Record(object):
84
+ class Record:
85
85
  """
86
86
  Create Python objects from IX-API responses.
87
87
 
@@ -144,7 +144,7 @@ class Record(object):
144
144
 
145
145
  url = None
146
146
 
147
- def __init__(self, values, api, endpoint):
147
+ def __init__(self, values, api, endpoint) -> None:
148
148
  self._full_cache = []
149
149
  self._init_cache = []
150
150
  self.api = api
@@ -1,4 +1,5 @@
1
- from datetime import datetime
1
+ import warnings
2
+ from datetime import datetime, timezone
2
3
 
3
4
  import jwt
4
5
 
@@ -11,7 +12,7 @@ class InvalidTokenException(Exception):
11
12
  pass
12
13
 
13
14
 
14
- class Token(object):
15
+ class Token:
15
16
  """
16
17
  A token is at the core of the authentication mechanism of IX-API.
17
18
 
@@ -24,37 +25,43 @@ class Token(object):
24
25
  continue using the API.
25
26
  """
26
27
 
27
- def __init__(self, token, issued_at, expires_at):
28
- self.encoded = token # Cache signed token data
29
- self.issued_at = issued_at
30
- self.expires_at = expires_at
28
+ def __init__(self, token: str, expires_at: datetime) -> None:
29
+ self.encoded: str = token # Cache signed token data
30
+ self.expires_at: datetime = expires_at
31
31
 
32
- def __str__(self):
32
+ def __str__(self) -> str:
33
33
  return self.encoded
34
34
 
35
- def __repr__(self):
36
- return f"<Token issued_at={self.issued_at} ttl={self.ttl}s>"
35
+ def __repr__(self) -> str:
36
+ return f"<Token ttl={self.ttl}s>"
37
37
 
38
38
  @property
39
- def ttl(self):
39
+ def issued_at(self) -> datetime:
40
+ warnings.warn(
41
+ "Property 'issued_at' is deprecated and value will always be set to now.",
42
+ DeprecationWarning,
43
+ )
44
+ return datetime.now(tz=timezone.utc)
45
+
46
+ @property
47
+ def ttl(self) -> int:
40
48
  """
41
49
  TTL is the number of seconds, from now, before the token expires.
42
50
  """
43
- seconds = (self.expires_at - datetime.now()).total_seconds()
51
+ seconds = (self.expires_at - datetime.now(timezone.utc)).total_seconds()
44
52
  return max(0, seconds)
45
53
 
46
54
  @property
47
- def is_expired(self):
55
+ def is_expired(self) -> bool:
48
56
  """
49
57
  Tell if a token is expired or not (TTL equals to 0).
50
58
  """
51
59
  return self.ttl == 0
52
60
 
53
61
  @classmethod
54
- def from_jwt(cls, token):
62
+ def from_jwt(cls, token: str) -> "Token":
55
63
  """
56
- Create a new token from a JWT, decoding it and caching it issued and
57
- expiration times.
64
+ Create a new token from a JWT, decoding it and caching its expiration time.
58
65
 
59
66
  :param token: (str) String containing the original encoded token which is the
60
67
  result of a POST request to the authentication endpoint.
@@ -65,8 +72,9 @@ class Token(object):
65
72
  raise TokenException(e)
66
73
 
67
74
  try:
68
- issued_at = datetime.fromtimestamp(payload["iat"])
69
- expires_at = datetime.fromtimestamp(payload["exp"])
70
- return cls(token, issued_at, expires_at)
75
+ return cls(
76
+ token=token,
77
+ expires_at=datetime.fromtimestamp(payload["exp"], tz=timezone.utc),
78
+ )
71
79
  except Exception as e:
72
80
  raise InvalidTokenException(e)
@@ -3,7 +3,7 @@ class Hashabledict(dict):
3
3
  return hash(frozenset(self))
4
4
 
5
5
 
6
- def cat(*args, separator="/", trailing=""):
6
+ def cat(*args, separator: str = "/", trailing: str = "") -> str:
7
7
  """
8
8
  Concatenate strings given a separator. All items will be parsed as string.
9
9
 
@@ -154,10 +154,11 @@ class PoP(Record):
154
154
  return self.name
155
155
 
156
156
 
157
- class Product(Record):
157
+ class ProductOffering(Record):
158
158
  """
159
- A Product is a network or peering-related product of a defined type sold by an IXP
160
- to its customers.
159
+ A ProductOffering is an offer made by an exchange to be consumed by a client.
160
+
161
+ It is also referenced as "product" in IX-API v1.
161
162
  """
162
163
 
163
164
  def __str__(self):
@@ -226,15 +227,6 @@ class PortReservation(Record):
226
227
  return self.id
227
228
 
228
229
 
229
- class ProductOffering(Record):
230
- """
231
- A ProductOffering is an offer made by an exchange to be consumed by a client.
232
- """
233
-
234
- def __str__(self):
235
- return self.name
236
-
237
-
238
230
  class Role(Record):
239
231
  """
240
232
  A Role enables a Contact to act for a specific purpose.