1010from typing import Union , Dict , BinaryIO
1111
1212import collections
13+
1314import requests
1415from requests .auth import AuthBase , HTTPBasicAuth
1516
1617from c8y_api ._auth import HTTPBearerAuth
1718from 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+
2049class 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 :
0 commit comments