Skip to content

Commit e3ba11b

Browse files
authored
mgmt/github-pylint-workflow (#5)
* Create pylint.yml * Adding basic pylint configuraiton. * Using custom pylintrc for workflow. * Fixing linting issues. * Update pylint.yml * Major refactoring to fix circular dependencies.
1 parent e02e1f5 commit e3ba11b

File tree

20 files changed

+1474
-813
lines changed

20 files changed

+1474
-813
lines changed

.github/workflows/pylint.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Pylint
2+
3+
on: [push]
4+
5+
jobs:
6+
build:
7+
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- uses: actions/checkout@v2
12+
- name: Set up Python 3.8
13+
uses: actions/setup-python@v1
14+
with:
15+
python-version: 3.8
16+
- name: Install dependencies
17+
run: |
18+
python -m pip install --upgrade pip
19+
pip install pylint
20+
- name: Analysing the code with pylint
21+
run: |
22+
pylint --rcfile pylintrc --fail-under=9 c8y_api sample tests integration_tests

c8y_api/__init__.py

Lines changed: 3 additions & 257 deletions
Original file line numberDiff line numberDiff line change
@@ -4,260 +4,6 @@
44
# Use, reproduction, transfer, publication or disclosure is prohibited except
55
# as specifically provided for in your License Agreement with Software AG.
66

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

Comments
 (0)