Skip to content

Commit 37ccb27

Browse files
authored
Feature/tenant options (#32)
* Added tenant options API support. * Switch to latest python 3 slim version. * Adding tenant_options sample microservice. * Making sure that all non-source directories are ignored.
1 parent bec6cc0 commit 37ccb27

File tree

10 files changed

+627
-12
lines changed

10 files changed

+627
-12
lines changed

.gitignore

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
.idea
2-
.vscode
1+
.idea*
2+
.vscode*
33
__pycache__
4-
dist
5-
build
6-
.venv
7-
venv
8-
venv-linux
9-
venv-win
4+
dist*
5+
build*
6+
.venv*
7+
venv*
108
*.egg-info
119
*.env*
1210
_version.py

CHANGELOG.md

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

33
## Work in progress
44

5-
_put your new comments here_
5+
* Added Tenant Options API support.
66

77
## Version 1.4
88

c8y_api/_main_api.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from c8y_api.model.inventory import Inventory, DeviceInventory, DeviceGroupInventory
1818
from c8y_api.model.measurements import Measurements
1919
from c8y_api.model.operations import Operations
20+
from c8y_api.model.tenant_options import TenantOptions
2021

2122

2223
class CumulocityApi(CumulocityRestApi):
@@ -42,6 +43,7 @@ def __init__(self, base_url: str, tenant_id: str, username: str = None, password
4243
self.__events = Events(self)
4344
self.__alarms = Alarms(self)
4445
self.__operations = Operations(self)
46+
self.__tenant_options = TenantOptions(self)
4547

4648
@property
4749
def measurements(self) -> Measurements:
@@ -112,3 +114,8 @@ def alarms(self) -> Alarms:
112114
def operations(self) -> Operations:
113115
"""Provide access to the Operation API."""
114116
return self.__operations
117+
118+
@property
119+
def tenant_options(self) -> TenantOptions:
120+
"""Provide access to the Tenant Options API."""
121+
return self.__tenant_options

c8y_api/model/tenant_options.py

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
# Copyright (c) 2020 Software AG,
2+
# Darmstadt, Germany and/or Software AG USA Inc., Reston, VA, USA,
3+
# and/or its subsidiaries and/or its affiliates and/or their licensors.
4+
# Use, reproduction, transfer, publication or disclosure is prohibited except
5+
# as specifically provided for in your License Agreement with Software AG.
6+
7+
from __future__ import annotations
8+
9+
from typing import Generator, List
10+
11+
from c8y_api._base_api import CumulocityRestApi
12+
from c8y_api.model._base import SimpleObject, CumulocityResource
13+
from c8y_api.model._parser import SimpleObjectParser
14+
15+
16+
class TenantOption(SimpleObject):
17+
""" Represent a tenant option within the database.
18+
19+
Instances of this class are returned by functions of the corresponding
20+
Tenant Option API. Use this class to create new or update options.
21+
22+
See also https://cumulocity.com/guides/latest/reference/tenants/#option
23+
"""
24+
# these need to be defined like this for the abstract super functions
25+
_resource = '/tenant/options'
26+
_parser = SimpleObjectParser({
27+
'category': 'category',
28+
'key': 'key',
29+
'_u_value': 'value'})
30+
_accept = 'application/vnd.com.nsn.cumulocity.option+json'
31+
32+
def __init__(self, c8y: CumulocityRestApi = None, category: str = None, key: str = None, value: str = None):
33+
""" Create a new TenantOption instance.
34+
35+
Args:
36+
c8y (CumulocityRestApi): Cumulocity connection reference; needs
37+
to be set for direct manipulation (create, delete)
38+
Args:
39+
category (str): Option category
40+
key (str): Option key (name)
41+
value (str): Option value
42+
43+
Returns:
44+
TenantObject instance
45+
"""
46+
super().__init__(c8y)
47+
self.category = category
48+
self.key = key
49+
self._u_value = value
50+
51+
value = SimpleObject.UpdatableProperty('_u_value')
52+
53+
def _assert_id(self):
54+
if self.key is None or self.category is None:
55+
raise ValueError("Both option category abd key must be set to allow direct object access.")
56+
57+
def _build_object_path(self):
58+
# no need to assert category/key this function is only used when a
59+
# corresponding assertion was run beforehand
60+
return f'{self._build_resource_path()}/{self.category}/{self.key}'
61+
62+
@classmethod
63+
def from_json(cls, json: dict) -> TenantOption:
64+
""" Build a new TenantOption instance from JSON.
65+
66+
The JSON is assumed to be in the format as it is used by the
67+
Cumulocity REST API.
68+
69+
Args:
70+
json (dict): JSON object (nested dictionary)
71+
representing a tenant option within Cumulocity
72+
73+
Returns:
74+
TenantOption object
75+
"""
76+
return cls._from_json(json, TenantOption())
77+
78+
def create(self) -> TenantOption:
79+
""" Create a new representation of this option within the database.
80+
81+
Returns:
82+
A fresh TenantOption instance representing the created
83+
option within the database.
84+
85+
See also function TenantOptions.create which doesn't parse the result.
86+
"""
87+
return super()._create()
88+
89+
def update(self) -> TenantOption:
90+
""" Write changes to the database.
91+
92+
Returns:
93+
A fresh TenantOption instance representing the updated
94+
object within the database.
95+
96+
See also function TenantOptions.update which doesn't parse the result.
97+
"""
98+
return super()._update()
99+
100+
def delete(self) -> None:
101+
"""Delete the option within the database.
102+
103+
See also function TenantOptions.delete to delete multiple objects.
104+
"""
105+
super()._delete()
106+
107+
108+
class TenantOptions(CumulocityResource):
109+
"""Provides access to the Tenant Options API.
110+
111+
This class can be used for get, search for, create, update and
112+
delete tenant options within the Cumulocity database.
113+
114+
See also: https://cumulocity.com/api/latest/#tag/Options
115+
"""
116+
117+
def __init__(self, c8y: CumulocityRestApi):
118+
super().__init__(c8y, '/tenant/options')
119+
120+
def build_object_path(self, category: str, key: str) -> str: # noqa
121+
# pylint: disable=arguments-differ
122+
"""Build the path to a specific object of this resource.
123+
124+
Note: this function overrides with different arguments because
125+
tenant options, unlike other objects, are not identified by ID
126+
but category/key.
127+
128+
Args:
129+
category (str): Option category
130+
key (str): Option key (name)
131+
132+
Returns:
133+
The relative path to the object within Cumulocity.
134+
"""
135+
return f'{self.resource}/{category}/{key}'
136+
137+
def select(self, category: str = None, limit: int = None, page_size: int = 1000) -> Generator[TenantOption]:
138+
""" Query the database for tenant options and iterate over the
139+
results.
140+
141+
This function is implemented in a lazy fashion - results will only be
142+
fetched from the database as long there is a consumer for them.
143+
144+
All parameters are considered to be filters, limiting the result set
145+
to objects which meet the filters specification. Filters can be
146+
combined (within reason).
147+
148+
Args:
149+
category (str): Option category
150+
limit (int): Limit the number of results to this number.
151+
page_size (int): Define the number of objects which are read (and
152+
parsed in one chunk). This is a performance related setting.
153+
154+
Returns:
155+
Generator for TenantObject instances
156+
"""
157+
base_query = self._build_base_query(category=category, page_size=page_size)
158+
return super()._iterate(base_query, limit, TenantOption.from_json)
159+
160+
def get_all(self, category: str = None, limit: int = None, page_size: int = 1000) -> List[TenantOption]:
161+
""" Query the database for tenant options and return the results
162+
as list.
163+
164+
This function is a greedy version of the `select` function. All
165+
available results are read immediately and returned as list.
166+
167+
Returns:
168+
List of TenantObject instances
169+
"""
170+
return list(self.select(category=category, limit=limit, page_size=page_size))
171+
172+
def get_all_mapped(self, category: str = None) -> dict[str, str]:
173+
""" Query the database for tenant options and return the results
174+
as a dictionary.
175+
176+
This result dictionary does not specify option categories hence
177+
it is best used with the category filter unless the keys are
178+
unique by themselves.
179+
180+
Args:
181+
category (str): Option category
182+
183+
Returns:
184+
Dictionary of option keys to values.
185+
"""
186+
return {o.key: o.value for o in self.get_all(category=category)}
187+
188+
def get(self, category: str, key: str) -> TenantOption:
189+
""" Retrieve a specific option from the database.
190+
191+
Args:
192+
category (str): Option category
193+
key (str): Option key (name)
194+
195+
Returns:
196+
A TenantOption instance
197+
198+
Raises:
199+
KeyError if the given combination of category and key
200+
is not defined within the database
201+
"""
202+
option = TenantOption.from_json(self.c8y.get(resource=self.build_object_path(category, key)))
203+
option.c8y = self.c8y # inject c8y connection into instance
204+
return option
205+
206+
def get_value(self, category: str, key: str) -> str:
207+
""" Retrieve the value of a specific option from the database.
208+
209+
Args:
210+
category (str): Option category
211+
key (str): Option key (name)
212+
213+
Returns:
214+
The value of the specified option
215+
216+
Raises:
217+
KeyError if the given combination of category and key
218+
is not defined within the database
219+
"""
220+
# this is a very simple payload, we extract it directly
221+
return self.c8y.get(resource=self.build_object_path(category, key))['value']
222+
223+
def set_value(self, category: str, key: str, value: str):
224+
""" Create a option within the database.
225+
226+
This is a shortcut function to avoid unnecessary instantiation of
227+
the TenantOption class.
228+
229+
Args:
230+
category (str): Option category
231+
key (str): Option key (name)
232+
value (str): Option value
233+
"""
234+
self.create(TenantOption(category=category, key=key, value=value))
235+
236+
def create(self, *options: TenantOption) -> None:
237+
""" Create options within the database.
238+
239+
Args:
240+
options (*TenantOption): Collection of TenantObject instances
241+
"""
242+
super()._create(TenantOption.to_json, *options)
243+
244+
def update(self, *options: TenantOption) -> None:
245+
""" Update options within the database.
246+
247+
Args:
248+
options (*TenantOption): Collection of TenantObject instances
249+
"""
250+
for o in options:
251+
self.c8y.put(self.build_object_path(o.category, o.key), json=o.to_diff_json(), accept=None)
252+
253+
def update_by(self, category: str, options: dict[str, str]) -> None:
254+
""" Update options within the database.
255+
256+
Args:
257+
category (str): Option category
258+
options (dict): A dictionary of option keys and values
259+
"""
260+
self.c8y.put(resource=self.resource + '/' + category, json=options, accept=None)
261+
262+
def delete(self, *options: TenantOption) -> None:
263+
""" Delete options within the database.
264+
265+
Args:
266+
options (*TenantOption): Collection of TenantObject instances
267+
"""
268+
for o in options:
269+
self.delete_by(o.category, o.key)
270+
271+
def delete_by(self, category: str, key: str) -> None:
272+
""" Delete specific option within the database.
273+
274+
Args:
275+
category (str): Option category
276+
key (str): Option key (name)
277+
"""
278+
self.c8y.delete(self.build_object_path(category, key))

0 commit comments

Comments
 (0)