Skip to content

Commit e80f644

Browse files
chisouchsou
andauthored
Feature/processing mode support (#60)
* Added support for processing mode header in requests. * Fixed test_tfa_settings_formatting test case. * Added processing mode to singe and multi tenant application wrappers. * Introducing processing modes; added cookie-based authentication support (OAI) for user scopes. * Added latest API extensions. * Switch to Python 3.10. * Improved cookie handling and error reporting. --------- Co-authored-by: Christoph Souris <christoph.souris@softwareag.com>
1 parent 43326c2 commit e80f644

File tree

11 files changed

+519
-61
lines changed

11 files changed

+519
-61
lines changed

c8y_api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from pkg_resources import get_distribution, DistributionNotFound
88

9-
from c8y_api._base_api import CumulocityRestApi
9+
from c8y_api._base_api import ProcessingMode, CumulocityRestApi
1010
from c8y_api._main_api import CumulocityApi
1111
from c8y_api._registry_api import CumulocityDeviceRegistry
1212
from c8y_api._auth import HTTPBasicAuth, HTTPBearerAuth

c8y_api/_base_api.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,56 @@
1010
from typing import Union, Dict, BinaryIO
1111

1212
import collections
13+
1314
import requests
1415
from requests.auth import AuthBase, HTTPBasicAuth
1516

1617
from c8y_api._auth import HTTPBearerAuth
1718
from c8y_api._jwt import JWT
1819

1920

21+
class ProcessingMode:
22+
"""Cumulocity REST API processing modes."""
23+
PERSISTENT = 'PERSISTENT'
24+
TRANSIENT = 'TRANSIENT'
25+
QUIESCENT = 'QUIESCENT'
26+
27+
28+
class HttpError(Exception):
29+
"""Base class for technical HTTP errors."""
30+
def __init__(self, method: str, url: str, code: int, message: str):
31+
self.method = method
32+
self.url = url
33+
self.code = code
34+
self. message = message
35+
36+
37+
class UnauthorizedError(HttpError):
38+
"""Error raised for unauthorized access."""
39+
def __init__(self, method: str, url: str = None, message: str = "Unauthorized."):
40+
super().__init__(method, url, 401, message)
41+
42+
43+
class AccessDeniedError(HttpError):
44+
"""Error raised for denied access."""
45+
def __init__(self, method: str, url: str = None, message: str = "Access denied."):
46+
super().__init__(method, url, 403, message)
47+
48+
2049
class CumulocityRestApi:
2150
"""Cumulocity base REST API.
2251
2352
Provides REST access to a Cumulocity instance.
2453
"""
2554

55+
METHOD_GET = 'GET'
56+
METHOD_POST = 'POST'
57+
METHOD_PUT = 'PUT'
58+
METHOD_DELETE = 'DELETE'
59+
2660
MIMETYPE_JSON = 'application/json'
2761
HEADER_APPLICATION_KEY = 'X-Cumulocity-Application-Key'
62+
HEADER_PROCESSING_MODE = 'X-Cumulocity-Processing-Mode'
2863

2964
ACCEPT_MANAGED_OBJECT = 'application/vnd.com.nsn.cumulocity.managedobject+json'
3065
ACCEPT_USER = 'application/vnd.com.nsn.cumulocity.user+json'
@@ -35,7 +70,7 @@ class CumulocityRestApi:
3570
CONTENT_MEASUREMENT_COLLECTION = 'application/vnd.com.nsn.cumulocity.measurementcollection+json'
3671

3772
def __init__(self, base_url: str, tenant_id: str, username: str = None, password: str = None, tfa_token: str = None,
38-
auth: AuthBase = None, application_key: str = None):
73+
auth: AuthBase = None, application_key: str = None, processing_mode: str = None):
3974
"""Build a CumulocityRestApi instance.
4075
4176
One of `auth` or `username/password` must be provided. The TFA token
@@ -50,10 +85,13 @@ def __init__(self, base_url: str, tenant_id: str, username: str = None, password
5085
auth (AuthBase): Authentication details
5186
application_key (str): Application ID to include in requests
5287
(for billing/metering purposes).
88+
processing_mode (str); Connection processing mode (see
89+
also https://cumulocity.com/api/core/#processing-mode)
5390
"""
5491
self.base_url = base_url.rstrip('/')
5592
self.tenant_id = tenant_id
5693
self.application_key = application_key
94+
self.processing_mode = processing_mode
5795
self.is_tls = self.base_url.startswith('https')
5896

5997
if auth:
@@ -70,6 +108,8 @@ def __init__(self, base_url: str, tenant_id: str, username: str = None, password
70108
self.__default_headers['tfatoken'] = tfa_token
71109
if self.application_key:
72110
self.__default_headers[self.HEADER_APPLICATION_KEY] = self.application_key
111+
if self.processing_mode:
112+
self.__default_headers[self.HEADER_PROCESSING_MODE] = self.processing_mode
73113
self.session = self._create_session()
74114

75115
def _create_session(self) -> requests.Session:
@@ -78,6 +118,8 @@ def _create_session(self) -> requests.Session:
78118
s.headers = {'Accept': 'application/json'}
79119
if self.application_key:
80120
s.headers.update({self.HEADER_APPLICATION_KEY: self.application_key})
121+
if self.processing_mode:
122+
s.headers.update({self.HEADER_PROCESSING_MODE: self.processing_mode})
81123
return s
82124

83125
def prepare_request(self, method: str, resource: str,
@@ -125,6 +167,10 @@ def get(self, resource: str, params: dict = None, accept: str = None, ordered: b
125167
"""
126168
additional_headers = self._prepare_headers(accept=accept)
127169
r = self.session.get(self.base_url + resource, params=params, headers=additional_headers)
170+
if r.status_code == 401:
171+
raise UnauthorizedError(self.METHOD_GET, self.base_url + resource)
172+
if r.status_code == 403:
173+
raise AccessDeniedError(self.METHOD_GET, self.base_url + resource)
128174
if r.status_code == 404:
129175
raise KeyError(f"No such object: {resource}")
130176
if 500 <= r.status_code <= 599:
@@ -154,6 +200,10 @@ def get_file(self, resource: str, params: dict = None) -> bytes:
154200
(only 200 is accepted).
155201
"""
156202
r = self.session.get(self.base_url + resource, params=params)
203+
if r.status_code == 401:
204+
raise UnauthorizedError(self.METHOD_GET, self.base_url + resource)
205+
if r.status_code == 403:
206+
raise AccessDeniedError(self.METHOD_GET, self.base_url + resource)
157207
if r.status_code == 404:
158208
raise KeyError(f"No such object: {resource}")
159209
if 500 <= r.status_code <= 599:
@@ -186,6 +236,10 @@ def post(self, resource: str, json: dict, accept: str = None, content_type: str
186236
assert isinstance(json, dict)
187237
additional_headers = self._prepare_headers(accept=accept, content_type=content_type)
188238
r = self.session.post(self.base_url + resource, json=json, headers=additional_headers)
239+
if r.status_code == 401:
240+
raise UnauthorizedError(self.METHOD_POST, self.base_url + resource)
241+
if r.status_code == 403:
242+
raise AccessDeniedError(self.METHOD_POST, self.base_url + resource)
189243
if r.status_code == 404:
190244
raise KeyError(f"No such object: {resource}")
191245
if 500 <= r.status_code <= 599:
@@ -233,6 +287,10 @@ def perform_post(open_file):
233287
else:
234288
r = perform_post(file)
235289

290+
if r.status_code == 401:
291+
raise UnauthorizedError(self.METHOD_POST, self.base_url + resource)
292+
if r.status_code == 403:
293+
raise AccessDeniedError(self.METHOD_POST, self.base_url + resource)
236294
if 500 <= r.status_code <= 599:
237295
raise SyntaxError(f"Invalid POST request. Status: {r.status_code} Response:\n" + r.text)
238296
if r.status_code != 201:
@@ -267,6 +325,10 @@ def put(self, resource: str, json: dict, params: dict = None,
267325
assert isinstance(json, dict)
268326
additional_headers = self._prepare_headers(accept=accept, content_type=content_type)
269327
r = self.session.put(self.base_url + resource, json=json, params=params, headers=additional_headers)
328+
if r.status_code == 401:
329+
raise UnauthorizedError(self.METHOD_PUT, self.base_url + resource)
330+
if r.status_code == 403:
331+
raise AccessDeniedError(self.METHOD_PUT, self.base_url + resource)
270332
if r.status_code == 404:
271333
raise KeyError(f"No such object: {resource}")
272334
if 500 <= r.status_code <= 599:
@@ -314,6 +376,10 @@ def read_file_data(f):
314376
additional_headers = self._prepare_headers(accept=accept, content_type=content_type)
315377
data = read_file_data(file)
316378
r = self.session.put(self.base_url + resource, data=data, headers=additional_headers)
379+
if r.status_code == 401:
380+
raise UnauthorizedError(self.METHOD_PUT, self.base_url + resource)
381+
if r.status_code == 403:
382+
raise AccessDeniedError(self.METHOD_PUT, self.base_url + resource)
317383
if r.status_code == 404:
318384
raise KeyError(f"No such object: {resource}")
319385
if 500 <= r.status_code <= 599:
@@ -345,6 +411,10 @@ def delete(self, resource: str, json: dict = None, params: dict = None):
345411
if json:
346412
assert isinstance(json, dict)
347413
r = self.session.delete(self.base_url + resource, json=json, params=params, headers={'Accept': None})
414+
if r.status_code == 401:
415+
raise UnauthorizedError(self.METHOD_DELETE, self.base_url + resource)
416+
if r.status_code == 403:
417+
raise AccessDeniedError(self.METHOD_DELETE, self.base_url + resource)
348418
if r.status_code == 404:
349419
raise KeyError(f"No such object: {resource}")
350420
if 500 <= r.status_code <= 599:

c8y_api/_main_api.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,27 @@ class CumulocityApi(CumulocityRestApi):
2929
Provides usage centric access to a Cumulocity instance.
3030
"""
3131

32-
def __init__(self, base_url: str, tenant_id: str, username: str = None, password: str = None,
33-
tfa_token: str = None, auth: AuthBase = None, application_key: str = None):
34-
super().__init__(base_url, tenant_id, username=username, password=password, tfa_token=tfa_token,
35-
auth=auth, application_key=application_key)
32+
def __init__(
33+
self,
34+
base_url: str,
35+
tenant_id: str,
36+
username: str = None,
37+
password: str = None,
38+
tfa_token: str = None,
39+
auth: AuthBase = None,
40+
application_key: str = None,
41+
processing_mode: str = None,
42+
):
43+
super().__init__(
44+
base_url,
45+
tenant_id,
46+
username=username,
47+
password=password,
48+
tfa_token=tfa_token,
49+
auth=auth,
50+
application_key=application_key,
51+
processing_mode=processing_mode,
52+
)
3653
self.__measurements = Measurements(self)
3754
self.__inventory = Inventory(self)
3855
self.__binaries = Binaries(self)

0 commit comments

Comments
 (0)