thingsboard-common 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Tell python that there are more sub-packages present, physically located elsewhere.
4
+ # See: https://stackoverflow.com/questions/8936884/python-import-path-packages-with-the-same-name-in-different-folders
5
+ import pkgutil
6
+
7
+ __path__ = pkgutil.extend_path(__path__, __name__)
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Tell python that there are more sub-packages present, physically located elsewhere.
4
+ # See: https://stackoverflow.com/questions/8936884/python-import-path-packages-with-the-same-name-in-different-folders
5
+ import pkgutil
6
+
7
+ __path__ = pkgutil.extend_path(__path__, __name__)
8
+
9
+ import logging
10
+ from .version import __version__ as version
11
+
12
+ # Do not output logs if logging module is not configured
13
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
14
+
15
+ logging.getLogger(__name__).info(f'thingsboard-common version: {version}')
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Tell python that there are more sub-packages present, physically located elsewhere.
4
+ # See: https://stackoverflow.com/questions/8936884/python-import-path-packages-with-the-same-name-in-different-folders
5
+ import pkgutil
6
+
7
+ __path__ = pkgutil.extend_path(__path__, __name__)
8
+
9
+ from .threaded import Threaded
10
+ from .thread_monitor import ThreadMonitor
@@ -0,0 +1,55 @@
1
+ import logging
2
+
3
+ from thingsboard.common.core.threaded import Threaded
4
+
5
+
6
+ class ThreadMonitor(object):
7
+ """Monitors registered threads.
8
+
9
+ It is a passive class. To check if threads are still alive, call method check_threads() regularly.
10
+ """
11
+ log = logging.getLogger(__name__)
12
+ threadList = []
13
+
14
+ # References single instance of this class.
15
+ _instance = None # type: ThreadMonitor or None
16
+
17
+ def __init__(self, log_name_prefix:str=''):
18
+ super(ThreadMonitor, self).__init__()
19
+ if self._instance:
20
+ assert False, 'Only one instance of this class is allowed'
21
+ else:
22
+ self._set_instance(self)
23
+ if log_name_prefix:
24
+ type(self).log = logging.getLogger(log_name_prefix + '.' + __name__)
25
+
26
+ @classmethod
27
+ def instance(cls):
28
+ """Returns the single instance of this class.
29
+ """
30
+ assert cls._instance, 'Create an instance of this class first'
31
+ return cls._instance
32
+
33
+ @classmethod
34
+ def _set_instance(cls, instance):
35
+ assert cls._instance is None, 'Only one instance of this class allowed'
36
+ cls._instance = instance
37
+
38
+ @classmethod
39
+ def register(cls, thread: Threaded):
40
+ cls.threadList.append(thread)
41
+ return True
42
+
43
+ @classmethod
44
+ def deregister(cls, thread: Threaded):
45
+ if thread in cls.threadList:
46
+ cls.threadList.remove(thread)
47
+ return True
48
+
49
+ @classmethod
50
+ def check_threads(cls):
51
+ for thread in cls.threadList:
52
+ if not thread.is_alive():
53
+ cls.log.error(f'Thread \'{thread.name}\' no more alive!')
54
+ return False
55
+ return True
@@ -0,0 +1,160 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import logging
4
+ import time
5
+ from threading import Condition
6
+ from threading import Thread
7
+
8
+
9
+ class Threaded(object):
10
+ """Provides a _thread which executes the _run() method at regular intervals.
11
+
12
+ Features:
13
+ - Thread supervision by ThreadMonitor
14
+ - Correct logging when exception occurs
15
+ - Thread reacts immediately on exit loop request (_thread_should_run)
16
+ """
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+ def __init__(self, control_interval_in_seconds=4.0, thread_monitor=None, **kwargs):
21
+ super(Threaded, self).__init__(**kwargs)
22
+ self._thread_monitor = thread_monitor # type: ThreadMonitor
23
+ self._thread = None # type: Thread or None # Thread executing the needed behavior
24
+ self._thread_should_run = True # Controls the loop of the _thread
25
+ self._thread_left_run_loop = False # Set to true when _thread is leaving run loop
26
+ self._control_interval_in_seconds = control_interval_in_seconds # Time to wait until next processing
27
+ self._sleep_condition = Condition()
28
+
29
+ def setup_thread(self, name=None, thread_monitor=None):
30
+
31
+ if thread_monitor:
32
+ assert not self._thread_monitor, 'Thread monitor should be initialized only once!'
33
+ self._thread_monitor = thread_monitor
34
+
35
+ if not name:
36
+ name = self.__class__.__name__
37
+
38
+ # Create a _thread that regularly polls the actual parameters of the real battery
39
+ self._thread = Thread(target=self._run_with_exception_logging, name=name,
40
+ daemon=True # Close _thread as soon as main _thread exits
41
+ )
42
+
43
+ if self._thread_monitor:
44
+ # Register _thread for later monitor of itself. Thread monitor allows to take action
45
+ # in case the _thread crashes.
46
+ self._thread_monitor.register(self._thread)
47
+
48
+ def start_thread(self):
49
+ if self._thread is None:
50
+ self.log.warning('Thread not created. Calling setThread() for you!')
51
+ self.setup_thread()
52
+
53
+ # Reset run attributes
54
+ self._thread_should_run = True
55
+ self._thread_left_run_loop = False
56
+
57
+ self._thread.start()
58
+
59
+ def stop_thread(self):
60
+ # Remove _thread from monitor
61
+ if self._thread_monitor:
62
+ self._thread_monitor.deregister(self._thread)
63
+
64
+ # Tell _thread it should leave
65
+ self._thread_should_run = False
66
+ # Wait until it is gone
67
+ if self._thread.is_alive():
68
+ self.wait_on_thread_to_leave()
69
+ # Delete instance
70
+ del self._thread
71
+ self._thread = None
72
+
73
+ def wakeup_thread(self):
74
+ """Wakes up _thread in case it is sleeping.
75
+ """
76
+ # Release _thread waiting on condition
77
+ with self._sleep_condition:
78
+ self._sleep_condition.notify()
79
+
80
+ def join(self):
81
+ """Wait for the internal _thread until it leaves.
82
+
83
+ Call stop_thread() to properly and quickly stop the internal _thread.
84
+ """
85
+ if self._thread:
86
+ self._thread.join()
87
+
88
+ def _run_with_exception_logging(self):
89
+ """Same as _run but logs exceptions to the console or log file.
90
+
91
+ This is necessary when running in testing/production environment.
92
+ In case of an exception thrown, the stack trace can be seen in the
93
+ log file. Otherwise, there is no info why the _thread did stop.
94
+ """
95
+ try:
96
+ self._run()
97
+ except Exception as e:
98
+ logging.error(e, exc_info=True)
99
+ finally:
100
+ # Wait here for a while. If leaving the method directly, the _thread
101
+ # gets deleted and the is_alive() method won't work anymore!
102
+ time.sleep(5)
103
+ return
104
+
105
+ def _thread_sleep_interval(self, sleep_interval_in_seconds=None):
106
+ """Tells the executing _thread how long to sleep while being still reactive on _thread_should_run attribute.
107
+ """
108
+ if sleep_interval_in_seconds is not None:
109
+ wait_time = sleep_interval_in_seconds
110
+ else:
111
+ wait_time = self._control_interval_in_seconds
112
+
113
+ if self._sleep_condition.acquire(blocking=False):
114
+ # Sleep the time given. Thread can be wakened up with self._sleep_condition.notify()
115
+ # see wakeup_thread()
116
+ try:
117
+ self._sleep_condition.wait(timeout=wait_time)
118
+ except RuntimeError as e: # pragma: no cover
119
+ self.log.exception(e)
120
+ finally:
121
+ self._sleep_condition.release()
122
+ return True
123
+ else:
124
+ self.log.error('Could not acquire sleep condition!') # pragma: no cover
125
+ return False # pragma: no cover
126
+
127
+ def _run(self):
128
+ assert False, 'Method needs to be implemented in derived class!'
129
+
130
+ """ Example loop:
131
+
132
+ while self._thread_should_run:
133
+
134
+ # Add your stuff here
135
+ print('Executes in a regular manner')
136
+
137
+ # Wait until next interval begins
138
+ if self._thread_should_run:
139
+ self._thread_sleep_interval()
140
+
141
+ self._thread_left_run_loop = True
142
+ """
143
+
144
+ def wait_on_thread_to_leave(self, timeout=3):
145
+ """Can be called to wait for the _thread until it left the run loop.
146
+
147
+ Replacement for self._thread.join() self._thread.join() is
148
+ reacting slowly! Replaced it with this method.
149
+ """
150
+ wait_time = timeout
151
+ decr_value = 0.2
152
+
153
+ if self._thread_left_run_loop:
154
+ return
155
+
156
+ while wait_time > 0:
157
+ time.sleep(decr_value)
158
+ wait_time -= decr_value
159
+ if self._thread_left_run_loop:
160
+ break
@@ -0,0 +1,7 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Tell python that there are more sub-packages present, physically located elsewhere.
4
+ # See: https://stackoverflow.com/questions/8936884/python-import-path-packages-with-the-same-name-in-different-folders
5
+ import pkgutil
6
+
7
+ __path__ = pkgutil.extend_path(__path__, __name__)
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from thingsboard.common.utils import string_helpers
4
+
5
+
6
+ def generate_attribute_names_by_name(name: str) -> tuple[str, str]:
7
+ """Generates attribute names according to our coding standards.
8
+ """
9
+ if len(name) == 0:
10
+ return tuple()
11
+
12
+ if name[0] == '_':
13
+ name = name[1:]
14
+
15
+ if '_' in name:
16
+ words = list(filter(None, name.split('_')))
17
+ elif '-' in name:
18
+ words = list(filter(None, name.split('-')))
19
+ else:
20
+ words = string_helpers.camel_case_split(name, lower_camel_case=True)
21
+
22
+ public_attribute_name = '_'.join((map(lambda x: x.lower(), words)))
23
+ private_attribute_name = '_' + public_attribute_name
24
+
25
+ return private_attribute_name, public_attribute_name
26
+
27
+
28
+ def generate_setters_from_attribute_name(attribute_name: str) -> tuple[str, str]:
29
+ """Generates method setter names according to our coding standards.
30
+ """
31
+ if len(attribute_name) == 0:
32
+ return tuple()
33
+
34
+ if attribute_name[0] == '_':
35
+ attribute_name = attribute_name[1:]
36
+
37
+ if '_' in attribute_name:
38
+ words = list(filter(None, attribute_name.split('_')))
39
+ else:
40
+ words = string_helpers.camel_case_split(attribute_name, lower_camel_case=True)
41
+
42
+ setter_name = '_'.join((map(lambda x: x.lower(), words)))
43
+ setter01 = 'set_' + setter_name
44
+
45
+ setter_name = ''.join((map(lambda x: x.capitalize(), words)))
46
+ setter02 = 'set' + setter_name
47
+
48
+ return setter01, setter02
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import datetime
4
+
5
+ EPOCH = datetime.datetime.fromtimestamp(0, datetime.UTC)
6
+
7
+
8
+ def get_current_timestamp() -> int:
9
+ """Creates a timestamp using the current date and time.
10
+
11
+ The timestamp returned is in milliseconds.
12
+ """
13
+ current_date_time = datetime.datetime.now(datetime.UTC)
14
+ return _unix_time_millis(current_date_time)
15
+
16
+
17
+ def get_timestamp(dt: datetime.datetime) -> int:
18
+ """Creates a timestamp using parameter dt.
19
+ :param dt The datetime object to convert to a timestamp
20
+ """
21
+ return _unix_time_millis(dt)
22
+
23
+
24
+ def _unix_time_millis(dt: datetime.datetime) -> int:
25
+ """Converts a datetime object into unix time including milliseconds.
26
+ :param dt The datetime object to convert.
27
+ """
28
+ ut = int((dt - EPOCH).total_seconds()) * 1000
29
+ ut += int(dt.microsecond / 1000)
30
+ return ut
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import os
4
+
5
+
6
+ def prettify(path_name: str) -> str:
7
+ """Makes all necessary stuff to get a usable path (specially under Windows)
8
+
9
+ :param path_name: Path to make pretty
10
+ """
11
+
12
+ if path_name:
13
+ # Remove backslashes from path
14
+ path_name = path_name.replace('\\', '/')
15
+
16
+ if path_name[0] == '~':
17
+ base_path = os.path.expanduser(path_name[0])
18
+ remaining_path = path_name[2:] if path_name[1] == '/' else path_name[1:]
19
+ path_name = os.path.join(base_path, remaining_path)
20
+
21
+ # and again
22
+ path_name = path_name.replace('\\', '/')
23
+
24
+ return path_name
25
+
26
+
27
+ def home_directory():
28
+ home_dir = os.path.expanduser('~')
29
+ return home_dir
30
+
31
+
32
+ def path_from_file(path_file_name: str) -> str:
33
+ """Returns the path of the given file location string.
34
+ """
35
+ path = os.path.abspath(os.path.dirname(path_file_name))
36
+ return path
37
+
38
+
39
+ def create_directory_structure(path: str):
40
+ """Creates one or more directories.
41
+ """
42
+ os.makedirs(path, exist_ok=True)
43
+
44
+
45
+ def remove_directory(path):
46
+ from shutil import rmtree
47
+
48
+ rmtree(path, ignore_errors=True)
@@ -0,0 +1,78 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import os
4
+ import urllib.parse as urlparse
5
+ import urllib3
6
+ from typing import Union
7
+
8
+ from configobj import ConfigObj
9
+ from .path_helpers import prettify
10
+
11
+
12
+ class ResourceLoader(object):
13
+
14
+ @classmethod
15
+ def load_from_locations(cls, filename: str, locations: Union[str, list[str]]) -> ConfigObj:
16
+ """Searches for a resource file in the given locations and loads them.
17
+ """
18
+
19
+ if isinstance(locations, str):
20
+ locations = [locations, ]
21
+
22
+ for location in locations:
23
+ url = urlparse.urlparse(location)
24
+
25
+ if url.scheme == 'home':
26
+ base_path = os.path.expanduser('~') + url.path
27
+ file_path = os.path.join(base_path, filename)
28
+ file_path = prettify(file_path)
29
+
30
+ if os.path.isfile(file_path):
31
+ properties = ConfigObj(file_path)
32
+ return properties
33
+ elif url.scheme in ('file', 'path'):
34
+ file_path = os.path.join(url.path, filename)
35
+ if os.path.isfile(file_path):
36
+ properties = ConfigObj(file_path)
37
+ return properties
38
+ elif url.scheme == 'http' or \
39
+ url.scheme == 'https':
40
+ if not location.endswith('/'):
41
+ location += '/'
42
+ file_path = location + filename
43
+ try:
44
+ http = urllib3.PoolManager()
45
+ r = http.request('GET', file_path)
46
+ file_content = r.data
47
+ properties = ConfigObj(file_content)
48
+ return properties
49
+ except NameError:
50
+ pass
51
+ return ConfigObj()
52
+
53
+ @classmethod
54
+ def find_first_occurrence(cls, filename: str, locations: Union[str, list[str]]) -> tuple[str | None, int]:
55
+ """ Searches for a file in given locations.
56
+
57
+ Returns the first location where the file was found by returning the path and the location
58
+ index where the file was found.
59
+ """
60
+
61
+ if isinstance(locations, str):
62
+ locations = [locations, ]
63
+
64
+ for index, location in enumerate(locations):
65
+ url = urlparse.urlparse(location)
66
+
67
+ if url.scheme == 'home':
68
+ base_path = os.path.expanduser('~') + url.path
69
+ file_path = os.path.join(base_path, filename)
70
+ file_path = prettify(file_path)
71
+
72
+ if os.path.isfile(file_path):
73
+ return prettify(base_path), index
74
+ elif url.scheme in ('file', 'path'):
75
+ file_path = os.path.join(url.path, filename)
76
+ if os.path.isfile(file_path):
77
+ return prettify(url.path), index
78
+ return None, -1
@@ -0,0 +1,31 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import re
4
+
5
+
6
+ def camel_case_split(string: str, lower_camel_case: bool = False) -> list:
7
+ """Splits a camel-case formatted string into its words.
8
+
9
+ Example: 'ANiceDay' gets converted to ['A', 'Nice', 'Day'].
10
+
11
+ If lower_camel_case is specified, also the first expression is put into the list.
12
+
13
+ Credits to:
14
+ - https://www.geeksforgeeks.org/python-split-camelcase-string-to-individual-strings/
15
+ - https://stackoverflow.com/questions/29916065/how-to-do-camelcase-split-in-python
16
+ """
17
+
18
+ if not lower_camel_case:
19
+ return re.findall(r'[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))', string)
20
+ else:
21
+ idx = list(map(str.isupper, string))
22
+ # mark change of case
23
+ lst = [0]
24
+ for (i, (x, y)) in enumerate(zip(idx, idx[1:])):
25
+ if x and not y: # "Ul"
26
+ lst.append(i)
27
+ elif not x and y: # "lU"
28
+ lst.append(i + 1)
29
+ lst.append(len(string))
30
+ # for "lUl", index of "U" will pop twice, have to filter it
31
+ return [string[x:y] for x, y in zip(lst, lst[1:]) if x < y]
@@ -0,0 +1,17 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import datetime
4
+
5
+ EPOCH = datetime.datetime.fromtimestamp(0, datetime.UTC)
6
+
7
+ def get_time_in_milliseconds(dt: datetime.datetime = None) -> int:
8
+ """Creates a timestamp using the current date and time.
9
+
10
+ If no parameters is given the actual UTC time is taken.
11
+ """
12
+ if not dt:
13
+ dt = datetime.datetime.now(datetime.UTC)
14
+
15
+ ut = int((dt - EPOCH).total_seconds()) * 1000
16
+ ut += int(dt.microsecond / 1000)
17
+ return ut
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ __author__ = 'Thomas Sterren'
5
+ __contributors__ = []
6
+ __version__ = '0.1.1'
7
+
8
+
9
+ def main():
10
+ print(__version__)
11
+
12
+
13
+ if __name__ == '__main__':
14
+ main()
15
+
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: thingsboard-common
3
+ Version: 0.1.1
4
+ Summary: ThingsBoard common package
5
+ Home-page: https://gitlab.com/things-board
6
+ Author: HES-SO Valais, School of Engineering, Sion
7
+ Author-email: thomas.sterren@hes-so.ch
8
+ License-Expression: MIT
9
+ Project-URL: Bug Tracker, https://gitlab.com/things-board/thingsboard-common/-/issues
10
+ Project-URL: Source Code, https://gitlab.com/things-board/thingsboard-common
11
+ Project-URL: Documentation, https://gitlab.com/things-board/thingsboard-common#thingsboard-common
12
+ Keywords: ThingsBoard common library IoT cloud connector core utils
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Education
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Intended Audience :: Information Technology
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Operating System :: OS Independent
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: configobj
28
+ Requires-Dist: urllib3
29
+ Requires-Dist: tb-mqtt-client
30
+ Dynamic: author
31
+ Dynamic: author-email
32
+ Dynamic: classifier
33
+ Dynamic: home-page
34
+ Dynamic: keywords
35
+ Dynamic: license-file
36
+ Dynamic: project-url
37
+ Dynamic: requires-dist
38
+ Dynamic: summary
39
+
40
+ # ThingsBoard Common
41
+
42
+ > Note:
43
+ >
44
+ > This is not an official project of the [
45
+ ThingsBoard](https://thingsboard.io/) authors! Please refer to [ThingsBoard - Open-source IoT Platform](https://github.com/thingsboard) for official ThingsBoard projects!
46
+
47
+ This project serves as a toolkit allowing other projects to use the same packages/modules as codebase.
48
+
49
+ These projects are using the [ThingsBoard](https://thingsboard.io/) platform and extend it with additional functionality.
@@ -0,0 +1,18 @@
1
+ thingsboard/__init__.py,sha256=76n4Ir0tUx-uCkvXK3u-jsWTx_SyyP-OAmv3Nf9OXp4,304
2
+ thingsboard/common/__init__.py,sha256=1nj8FJl6c4Wlq2zIWgjDZXtltHfHbj1vGCI0NZk1x3s,568
3
+ thingsboard/common/version.py,sha256=yzUT3AV_2JhkKU_bBGi7othAGr5HdYN-BBBkuh3l0GA,214
4
+ thingsboard/common/core/__init__.py,sha256=_O72SOav2A-q2gx9XuHDZM6ZOcLzu_5g7BGEuYjZztc,381
5
+ thingsboard/common/core/thread_monitor.py,sha256=G4EQ-TvzO2LowNaq93LKfz9yhQCDGHstO8iaMXakFGk,1732
6
+ thingsboard/common/core/threaded.py,sha256=PG5hItcADwjkyhcLvcsUsbcH6F4r-wWxxWK9FGHQ3SU,5859
7
+ thingsboard/common/utils/__init__.py,sha256=76n4Ir0tUx-uCkvXK3u-jsWTx_SyyP-OAmv3Nf9OXp4,304
8
+ thingsboard/common/utils/attribute_helpers.py,sha256=hdor2Fh2YEdLWuE2UkDIcwLalOSwsvHjvRBbTsJN4h0,1507
9
+ thingsboard/common/utils/datetime_helpers.py,sha256=4ZqMVFa_ZBJifT6jFkQzugigoWuS5A7N44MroEfmVgc,873
10
+ thingsboard/common/utils/path_helpers.py,sha256=BEbswIZyNdXMNm4lXwIUszbg9T7Fr3fAdxrgvh7TllA,1195
11
+ thingsboard/common/utils/resource_loader.py,sha256=M8HLQWndTjrk06P5i2wY1p-EinuozL-BiMzB_UYB9-g,2869
12
+ thingsboard/common/utils/string_helpers.py,sha256=tVRjZuijRhhd2G86Gbs6GtPNiVxO4FVZwPOPe0wSsbk,1124
13
+ thingsboard/common/utils/timestamp_helpers.py,sha256=zjZ3-xZvF-x2yjQm_DVljx8gLToTqyRzn3h0VNgzEtg,479
14
+ thingsboard_common-0.1.1.dist-info/licenses/LICENSE,sha256=kzvDVNE4q9rpOY-d0Fn61rXyTBtkgeRmZnRak2Nrvl4,1091
15
+ thingsboard_common-0.1.1.dist-info/METADATA,sha256=nGjuApSzV3jmmvm7h_4EJX7amcejBMLHhpVj6qsN3ys,2010
16
+ thingsboard_common-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ thingsboard_common-0.1.1.dist-info/top_level.txt,sha256=hIeFbajkoBwsAEzoI1q3cZpwD-MtNvckaJqYsKNggNA,12
18
+ thingsboard_common-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 HES-SO Valais
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ thingsboard