|
4 | 4 | # Use, reproduction, transfer, publication or disclosure is prohibited except |
5 | 5 | # as specifically provided for in your License Agreement with Software AG. |
6 | 6 |
|
7 | | -import sys |
8 | | -import requests |
9 | | -import collections |
10 | | -import time |
11 | | -import yaml |
12 | | -from dataclasses import dataclass |
13 | | - |
14 | | -from c8y_api._util import debug |
15 | | -from c8y_api.model.inventory import Inventory, Identity, Binary, DeviceGroupInventory, DeviceInventory |
16 | | -from c8y_api.model.administration import Users, GlobalRoles, InventoryRoles |
17 | | -from c8y_api.model.measurements import Measurements |
18 | | -from c8y_api.model.applications import Applications |
19 | | -from c8y_api.model.events import Events |
20 | | -from c8y_api.model.alarms import Alarms |
21 | | - |
22 | | - |
23 | | -class CumulocityRestApi: |
24 | | - |
25 | | - ACCEPT_MANAGED_OBJECT = 'application/vnd.com.nsn.cumulocity.managedobject+json' |
26 | | - |
27 | | - def __init__(self, base_url, tenant_id, username, password, tfa_token=None, application_key=None): |
28 | | - self.base_url = base_url |
29 | | - self.tenant_id = tenant_id |
30 | | - self.username = username |
31 | | - self.password = password |
32 | | - self.tfa_token = tfa_token |
33 | | - self.application_key = application_key |
34 | | - self.__auth = f'{tenant_id}/{username}', password |
35 | | - self.__default_headers = {} |
36 | | - if self.tfa_token: |
37 | | - self.__default_headers['tfatoken'] = self.tfa_token |
38 | | - if self.application_key: |
39 | | - self.__default_headers['X-Cumulocity-Application-Key'] = self.application_key |
40 | | - self.session = requests.Session() |
41 | | - |
42 | | - def prepare_request(self, method, resource, body=None, additional_headers=None): |
43 | | - hs = self.__default_headers |
44 | | - if additional_headers: |
45 | | - hs.update(additional_headers) |
46 | | - rq = requests.Request(method=method, url=self.base_url + resource, headers=hs, auth=self.__auth) |
47 | | - if body: |
48 | | - rq.json = body |
49 | | - return rq.prepare() |
50 | | - |
51 | | - def get(self, resource, ordered=False): |
52 | | - """Generic HTTP GET wrapper, dealing with standard error returning a JSON body object.""" |
53 | | - r = self.session.get(self.base_url + resource, auth=self.__auth, headers=self.__default_headers) |
54 | | - if r.status_code == 404: |
55 | | - raise KeyError(f"No such object: {resource}") |
56 | | - if 500 <= r.status_code <= 599: |
57 | | - raise SyntaxError(f"Invalid GET request. Status: {r.status_code} Response:\n" + r.text) |
58 | | - if r.status_code != 200: |
59 | | - raise ValueError(f"Unable to perform GET request. Status: {r.status_code} Response:\n" + r.text) |
60 | | - return r.json() if not ordered else r.json(object_pairs_hook=collections.OrderedDict) |
61 | | - |
62 | | - def post(self, resource, json, accept='application/json', content_type=None): |
63 | | - """Generic HTTP POST wrapper, dealing with standard error returning a JSON body object.""" |
64 | | - assert isinstance(json, dict) |
65 | | - headers = self.__default_headers.copy() |
66 | | - if accept: |
67 | | - headers['Accept'] = accept |
68 | | - if content_type: |
69 | | - headers['Content-Type'] = content_type |
70 | | - r = self.session.post(self.base_url + resource, json=json, auth=self.__auth, headers=headers) |
71 | | - if 500 <= r.status_code <= 599: |
72 | | - raise SyntaxError(f"Invalid POST request. Status: {r.status_code} Response:\n" + r.text) |
73 | | - if r.status_code != 201 and r.status_code != 200: |
74 | | - raise ValueError(f"Unable to perform POST request. Status: {r.status_code} Response:\n" + r.text) |
75 | | - if r.content: |
76 | | - return r.json() |
77 | | - |
78 | | - def post_file(self, resource, file, binary_meta_information): |
79 | | - assert isinstance(binary_meta_information, Binary) |
80 | | - assert file is not None |
81 | | - |
82 | | - headers = {'Accept': 'application/json', **self.__default_headers} |
83 | | - |
84 | | - payload = { |
85 | | - 'object': (None, str(binary_meta_information._to_full_json()).replace("'", '"')), |
86 | | - 'filesize': (None, sys.getsizeof(file)), |
87 | | - 'file': (None, file.read()) |
88 | | - } |
89 | | - |
90 | | - r = self.session.post(self.base_url + resource, files=payload, auth=self.__auth, headers=headers) |
91 | | - if 500 <= r.status_code <= 599: |
92 | | - raise SyntaxError(f"Invalid POST request. Status: {r.status_code} Response:\n" + r.text) |
93 | | - if r.status_code != 201: |
94 | | - raise ValueError("Unable to perform POST request.", ("Status", r.status_code), ("Response", r.text)) |
95 | | - return r.json() |
96 | | - |
97 | | - def put(self, resource, json, accept='application/json', content_type=None): |
98 | | - """Generic HTTP PUT wrapper, dealing with standard error returning a JSON body object.""" |
99 | | - assert isinstance(json, dict) |
100 | | - headers = self.__default_headers.copy() |
101 | | - if accept: |
102 | | - headers['Accept'] = accept |
103 | | - if content_type: |
104 | | - headers['Content-Type'] = content_type |
105 | | - r = self.session.put(self.base_url + resource, json=json, auth=self.__auth, headers=headers) |
106 | | - if r.status_code == 404: |
107 | | - raise KeyError(f"No such object: {resource}") |
108 | | - if 500 <= r.status_code <= 599: |
109 | | - raise SyntaxError(f"Invalid PUT request. Status: {r.status_code} Response:\n" + r.text) |
110 | | - if r.status_code != 200: |
111 | | - raise ValueError(f"Unable to perform PUT request. Status: {r.status_code} Response:\n" + r.text) |
112 | | - if r.content: |
113 | | - return r.json() |
114 | | - |
115 | | - def put_file(self, resource, file, media_type): |
116 | | - headers = {'Content-Type': media_type, **self.__default_headers} |
117 | | - r = self.session.put(self.base_url + resource, data=file.read(), auth=self.__auth, headers=headers) |
118 | | - if r.status_code == 404: |
119 | | - raise KeyError(f"No such object: {resource}") |
120 | | - if 500 <= r.status_code <= 599: |
121 | | - raise SyntaxError(f"Invalid PUT request. Status: {r.status_code} Response:\n" + r.text) |
122 | | - if r.status_code != 201: |
123 | | - raise ValueError(f"Unable to perform PUT request. Status: {r.status_code} Response:\n" + r.text) |
124 | | - |
125 | | - def delete(self, resource): |
126 | | - """Generic HTTP DELETE wrapper, dealing with standard error returning a JSON body object.""" |
127 | | - r = self.session.delete(self.base_url + resource, auth=self.__auth, headers=self.__default_headers) |
128 | | - if r.status_code == 404: |
129 | | - raise KeyError(f"No such object: {resource}") |
130 | | - if 500 <= r.status_code <= 599: |
131 | | - raise SyntaxError(f"Invalid DELETE request. Status: {r.status_code} Response:\n" + r.text) |
132 | | - if r.status_code != 204: |
133 | | - raise ValueError(f"Unable to perform DELETE request. Status: {r.status_code} Response:\n" + r.text) |
134 | | - |
135 | | - |
136 | | -class CumulocityApi(CumulocityRestApi): |
137 | | - |
138 | | - def __init__(self, base_url, tenant_id, username, password, tfa_token=None, application_key=None): |
139 | | - super().__init__(base_url, tenant_id, username, password, tfa_token, application_key) |
140 | | - self.__measurements = Measurements(self) |
141 | | - self.__inventory = Inventory(self) |
142 | | - self.__group_inventory = DeviceGroupInventory(self) |
143 | | - self.__device_inventory = DeviceInventory(self) |
144 | | - self.__identity = Identity(self) |
145 | | - self.__users = Users(self) |
146 | | - self.__global_roles = GlobalRoles(self) |
147 | | - self.__inventory_roles = InventoryRoles(self) |
148 | | - self.__applications = Applications(self) |
149 | | - self.__events = Events(self) |
150 | | - self.__alarms = Alarms(self) |
151 | | - |
152 | | - @property |
153 | | - def measurements(self): |
154 | | - return self.__measurements |
155 | | - |
156 | | - @property |
157 | | - def inventory(self): |
158 | | - return self.__inventory |
159 | | - |
160 | | - @property |
161 | | - def group_inventory(self): |
162 | | - return self.__group_inventory |
163 | | - |
164 | | - @property |
165 | | - def device_inventory(self): |
166 | | - return self.__device_inventory |
167 | | - |
168 | | - @property |
169 | | - def identity(self): |
170 | | - return self.__identity |
171 | | - |
172 | | - @property |
173 | | - def users(self): |
174 | | - return self.__users |
175 | | - |
176 | | - @property |
177 | | - def global_roles(self): |
178 | | - return self.__global_roles |
179 | | - |
180 | | - @property |
181 | | - def inventory_roles(self): |
182 | | - return self.__inventory_roles |
183 | | - |
184 | | - @property |
185 | | - def applications(self): |
186 | | - return self.__applications |
187 | | - |
188 | | - @property |
189 | | - def events(self): |
190 | | - return self.__events |
191 | | - |
192 | | - @property |
193 | | - def alarms(self): |
194 | | - return self.__alarms |
195 | | - |
196 | | - |
197 | | -class CumulocityDeviceRegistry(CumulocityRestApi): |
198 | | - |
199 | | - @dataclass |
200 | | - class Credentials: |
201 | | - tenant_id: str |
202 | | - username: str |
203 | | - password: str |
204 | | - |
205 | | - __default_instance = None |
206 | | - |
207 | | - def __init__(self, base_url, tenant_id, username, password): |
208 | | - super().__init__(base_url, tenant_id, username, password) |
209 | | - |
210 | | - @classmethod |
211 | | - def __build_default(cls): |
212 | | - with open('c8y_api.yaml') as config_file: |
213 | | - configuration = yaml.load(config_file, Loader=yaml.BaseLoader) |
214 | | - base = configuration['base'] |
215 | | - tenant_id = configuration['devicebootstrap']['tenant_id'] |
216 | | - username = configuration['devicebootstrap']['username'] |
217 | | - password = configuration['devicebootstrap']['password'] |
218 | | - return CumulocityDeviceRegistry(base, tenant_id, username, password) |
219 | | - |
220 | | - @classmethod |
221 | | - def default(cls): |
222 | | - if not cls.__default_instance: |
223 | | - cls.__default_instance = cls.__build_default() |
224 | | - return cls.__default_instance |
225 | | - |
226 | | - def await_credentials(self, device_id, timeout='60m', pause='1s'): |
227 | | - pause_s = CumulocityDeviceRegistry.__parse_timedelta_s(pause) |
228 | | - timeout_s = CumulocityDeviceRegistry.__parse_timedelta_s(timeout) |
229 | | - assert pause_s, f"Unable to parse pause string: {pause}" |
230 | | - assert timeout_s, f"Unable to parse timeout string: {timeout}" |
231 | | - request_json = {'id': device_id} |
232 | | - request = self.prepare_request(method='post', resource='/devicecontrol/deviceCredentials', body=request_json) |
233 | | - session = requests.Session() |
234 | | - timeout_time = time.time() + timeout_s |
235 | | - while True: |
236 | | - if timeout_time < time.time(): |
237 | | - raise TimeoutError |
238 | | - debug("Requesting device credentials for device id '%s'", device_id) |
239 | | - response: requests.Response = session.send(request) |
240 | | - if response.status_code == 404: |
241 | | - # This is the expected response until the device registration request got accepted |
242 | | - # from within Cumulocity. It will be recognized as an inbound request, though and |
243 | | - # trigger status 'pending' if it was 'awaiting connection'. |
244 | | - time.sleep(pause_s) |
245 | | - elif response.status_code == 201: |
246 | | - response_json = response.json() |
247 | | - return CumulocityDeviceRegistry.Credentials(response_json['tenantId'], |
248 | | - response_json['username'], |
249 | | - response_json['password']) |
250 | | - else: |
251 | | - raise RuntimeError(f"Unexpected response code: {response.status_code}") |
252 | | - |
253 | | - def await_connection(self, device_id, timeout='60m', pause='1s'): |
254 | | - credentials = self.await_credentials(device_id, timeout, pause) |
255 | | - return CumulocityApi(self.base_url, credentials.tenant_id, credentials.username, credentials.password) |
256 | | - |
257 | | - @staticmethod |
258 | | - def __parse_timedelta_s(string): |
259 | | - return float(string)/1000.0 if string.endswith('ms') \ |
260 | | - else int(string[:-1]) if string.endswith('s') \ |
261 | | - else int(string[:-1])*60 if string.endswith('m') \ |
262 | | - else int(string[:-1])*1440 if string.endswith('h') \ |
263 | | - else None |
| 7 | +from c8y_api._base_api import CumulocityRestApi |
| 8 | +from c8y_api._main_api import CumulocityApi |
| 9 | +from c8y_api._registry_api import CumulocityDeviceRegistry |
0 commit comments