thingsboard-common 0.1.0__tar.gz
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.
- thingsboard_common-0.1.0/LICENSE +21 -0
- thingsboard_common-0.1.0/PKG-INFO +49 -0
- thingsboard_common-0.1.0/README.md +10 -0
- thingsboard_common-0.1.0/pyproject.toml +14 -0
- thingsboard_common-0.1.0/setup.cfg +4 -0
- thingsboard_common-0.1.0/setup.py +200 -0
- thingsboard_common-0.1.0/src/thingsboard/__init__.py +7 -0
- thingsboard_common-0.1.0/src/thingsboard/common/__init__.py +15 -0
- thingsboard_common-0.1.0/src/thingsboard/common/core/__init__.py +10 -0
- thingsboard_common-0.1.0/src/thingsboard/common/core/thread_monitor.py +55 -0
- thingsboard_common-0.1.0/src/thingsboard/common/core/threaded.py +160 -0
- thingsboard_common-0.1.0/src/thingsboard/common/utils/__init__.py +7 -0
- thingsboard_common-0.1.0/src/thingsboard/common/utils/attribute_helpers.py +48 -0
- thingsboard_common-0.1.0/src/thingsboard/common/utils/datetime_helpers.py +30 -0
- thingsboard_common-0.1.0/src/thingsboard/common/utils/path_helpers.py +48 -0
- thingsboard_common-0.1.0/src/thingsboard/common/utils/resource_loader.py +77 -0
- thingsboard_common-0.1.0/src/thingsboard/common/utils/string_helpers.py +31 -0
- thingsboard_common-0.1.0/src/thingsboard/common/utils/timestamp_helpers.py +17 -0
- thingsboard_common-0.1.0/src/thingsboard/common/version.py +15 -0
- thingsboard_common-0.1.0/src/thingsboard_common.egg-info/PKG-INFO +49 -0
- thingsboard_common-0.1.0/src/thingsboard_common.egg-info/SOURCES.txt +22 -0
- thingsboard_common-0.1.0/src/thingsboard_common.egg-info/dependency_links.txt +1 -0
- thingsboard_common-0.1.0/src/thingsboard_common.egg-info/requires.txt +3 -0
- thingsboard_common-0.1.0/src/thingsboard_common.egg-info/top_level.txt +1 -0
|
@@ -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,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thingsboard-common
|
|
3
|
+
Version: 0.1.0
|
|
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,10 @@
|
|
|
1
|
+
# ThingsBoard Common
|
|
2
|
+
|
|
3
|
+
> Note:
|
|
4
|
+
>
|
|
5
|
+
> This is not an official project of the [
|
|
6
|
+
ThingsBoard](https://thingsboard.io/) authors! Please refer to [ThingsBoard - Open-source IoT Platform](https://github.com/thingsboard) for official ThingsBoard projects!
|
|
7
|
+
|
|
8
|
+
This project serves as a toolkit allowing other projects to use the same packages/modules as codebase.
|
|
9
|
+
|
|
10
|
+
These projects are using the [ThingsBoard](https://thingsboard.io/) platform and extend it with additional functionality.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "thingsboard-common"
|
|
7
|
+
dynamic = ["version", "classifiers", "dependencies", "urls", "keywords", "authors", "description"]
|
|
8
|
+
requires-python = ">=3.9"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICEN[CS]E*"]
|
|
12
|
+
|
|
13
|
+
[tool.setuptools.dynamic]
|
|
14
|
+
version = {attr = "thingsboard.common.version"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Script to install project using pip
|
|
5
|
+
|
|
6
|
+
See:
|
|
7
|
+
https://packaging.python.org/en/latest/distributing.html
|
|
8
|
+
https://github.com/pypa/sampleproject
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
# To use a consistent encoding
|
|
13
|
+
from codecs import open
|
|
14
|
+
|
|
15
|
+
# Always prefer setuptools over distutils
|
|
16
|
+
from setuptools import setup, find_packages
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_version_info():
|
|
20
|
+
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
|
21
|
+
'src/thingsboard/common/version.py')) as vf:
|
|
22
|
+
content = vf.readlines()
|
|
23
|
+
for line in content:
|
|
24
|
+
if '__version__' in line:
|
|
25
|
+
values = line.split('=')
|
|
26
|
+
version = values[1]
|
|
27
|
+
version = version.strip('\n')
|
|
28
|
+
version = version.strip('\r')
|
|
29
|
+
version = version.replace('\'', '')
|
|
30
|
+
version = version.strip(' ')
|
|
31
|
+
return version
|
|
32
|
+
return '0.0.0'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__version__ = read_version_info()
|
|
36
|
+
|
|
37
|
+
# Get the long description from the README file
|
|
38
|
+
current_directory = os.path.abspath(os.path.dirname(__file__))
|
|
39
|
+
with open(os.path.join(current_directory, 'README.md'), encoding='utf-8') as f:
|
|
40
|
+
long_description = f.read()
|
|
41
|
+
|
|
42
|
+
# Arguments marked as "Required" below must be included for upload to PyPI.
|
|
43
|
+
# Fields marked as "Optional" may be commented out.
|
|
44
|
+
|
|
45
|
+
setup(
|
|
46
|
+
# This is the name of your project. The first time you publish this
|
|
47
|
+
# package, this name will be registered for you. It will determine how
|
|
48
|
+
# users can install this project, e.g.:
|
|
49
|
+
#
|
|
50
|
+
# $ pip install <sample project>
|
|
51
|
+
#
|
|
52
|
+
# And where it will live on PyPI: https://pypi.org/project/sampleproject/
|
|
53
|
+
#
|
|
54
|
+
# There are some restrictions on what makes a valid project name
|
|
55
|
+
# specification here:
|
|
56
|
+
# https://packaging.python.org/specifications/core-metadata/#name
|
|
57
|
+
name='thingsboard-common', # Required
|
|
58
|
+
|
|
59
|
+
# Versions should comply with PEP 440:
|
|
60
|
+
# https://www.python.org/dev/peps/pep-0440/
|
|
61
|
+
#
|
|
62
|
+
# For a discussion on single-sourcing the version across setup.py and the
|
|
63
|
+
# project code, see
|
|
64
|
+
# https://packaging.python.org/en/latest/single_source_version.html
|
|
65
|
+
version=__version__, # Required
|
|
66
|
+
|
|
67
|
+
# This is a one-line description or tagline of what your project does. This
|
|
68
|
+
# corresponds to the "Summary" metadata field:
|
|
69
|
+
# https://packaging.python.org/specifications/core-metadata/#summary
|
|
70
|
+
description='ThingsBoard common package', # Required
|
|
71
|
+
|
|
72
|
+
# This is an optional longer description of your project that represents
|
|
73
|
+
# the body of text which users will see when they visit PyPI.
|
|
74
|
+
#
|
|
75
|
+
# Often, this is the same as your README, so you can just read it in from
|
|
76
|
+
# that file directly (as we have already done above)
|
|
77
|
+
#
|
|
78
|
+
# This field corresponds to the "Description" metadata field:
|
|
79
|
+
# https://packaging.python.org/specifications/core-metadata/#description-optional
|
|
80
|
+
# long_description=long_description, # Optional
|
|
81
|
+
long_description=long_description,
|
|
82
|
+
|
|
83
|
+
long_description_content_type='text/markdown',
|
|
84
|
+
|
|
85
|
+
# This should be a valid link to your project's main homepage.
|
|
86
|
+
#
|
|
87
|
+
# This field corresponds to the "Home-Page" metadata field:
|
|
88
|
+
# https://packaging.python.org/specifications/core-metadata/#home-page-optional
|
|
89
|
+
url='https://gitlab.com/things-board', # Optional
|
|
90
|
+
|
|
91
|
+
project_urls={
|
|
92
|
+
"Bug Tracker": "https://gitlab.com/things-board/thingsboard-common/-/issues",
|
|
93
|
+
"Source Code": "https://gitlab.com/things-board/thingsboard-common",
|
|
94
|
+
"Documentation": "https://gitlab.com/things-board/thingsboard-common#thingsboard-common",
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
# This should be your name or the name of the organization which owns the
|
|
98
|
+
# project.
|
|
99
|
+
author='HES-SO Valais, School of Engineering, Sion', # Optional
|
|
100
|
+
|
|
101
|
+
# This should be a valid email address corresponding to the author listed
|
|
102
|
+
# above.
|
|
103
|
+
author_email='thomas.sterren@hes-so.ch', # Optional
|
|
104
|
+
|
|
105
|
+
# Classifiers help users find your project by categorizing it.
|
|
106
|
+
#
|
|
107
|
+
# For a list of valid classifiers, see
|
|
108
|
+
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
|
109
|
+
classifiers=[ # Optional
|
|
110
|
+
# How mature is this project? Common values are
|
|
111
|
+
# 3 - Alpha
|
|
112
|
+
# 4 - Beta
|
|
113
|
+
# 5 - Production/Stable
|
|
114
|
+
'Development Status :: 3 - Alpha',
|
|
115
|
+
|
|
116
|
+
# Indicate who your project is intended for
|
|
117
|
+
'Intended Audience :: Developers',
|
|
118
|
+
'Intended Audience :: Education',
|
|
119
|
+
'Intended Audience :: Science/Research',
|
|
120
|
+
'Intended Audience :: Information Technology',
|
|
121
|
+
'Topic :: Software Development :: Libraries :: Python Modules',
|
|
122
|
+
|
|
123
|
+
# Specify the Python versions you support here. In particular, ensure
|
|
124
|
+
# that you indicate whether you support Python 2, Python 3 or both.
|
|
125
|
+
'Programming Language :: Python :: 3.9',
|
|
126
|
+
'Programming Language :: Python :: 3.10',
|
|
127
|
+
'Programming Language :: Python :: 3.11',
|
|
128
|
+
'Programming Language :: Python :: 3.12',
|
|
129
|
+
|
|
130
|
+
'Operating System :: OS Independent',
|
|
131
|
+
],
|
|
132
|
+
|
|
133
|
+
# This field adds keywords for your project which will appear on the
|
|
134
|
+
# project page. What does your project relate to?
|
|
135
|
+
#
|
|
136
|
+
# Note that this is a string of words separated by whitespace, not a list.
|
|
137
|
+
keywords='ThingsBoard common library IoT cloud connector core utils', # Optional
|
|
138
|
+
|
|
139
|
+
# You can just specify package directories manually here if your project is
|
|
140
|
+
# simple. Or you can use find_packages().
|
|
141
|
+
#
|
|
142
|
+
# Alternatively, if you just want to distribute a single Python file, use
|
|
143
|
+
# the `py_modules` argument instead as follows, which will expect a file
|
|
144
|
+
# called `my_module.py` to exist:
|
|
145
|
+
#
|
|
146
|
+
# py_modules=["my_module"],
|
|
147
|
+
#
|
|
148
|
+
packages=find_packages('src'), # Required
|
|
149
|
+
package_dir={'': 'src'},
|
|
150
|
+
|
|
151
|
+
# This field lists other packages that your project depends on to run.
|
|
152
|
+
# Any package you put here will be installed by pip when your project is
|
|
153
|
+
# installed, so they must be valid existing projects.
|
|
154
|
+
#
|
|
155
|
+
# For an analysis of "install_requires" vs pip's requirements files see:
|
|
156
|
+
# https://packaging.python.org/en/latest/requirements.html
|
|
157
|
+
install_requires=['configobj', 'urllib3', 'tb-mqtt-client'], # Optional
|
|
158
|
+
|
|
159
|
+
# List additional groups of dependencies here (e.g. development
|
|
160
|
+
# dependencies). Users will be able to install these using the "extras"
|
|
161
|
+
# syntax, for example:
|
|
162
|
+
#
|
|
163
|
+
# $ pip install <sample project>[dev]
|
|
164
|
+
#
|
|
165
|
+
# Similar to `install_requires` above, these must be valid existing
|
|
166
|
+
# projects.
|
|
167
|
+
extras_require={ # Optional
|
|
168
|
+
# 'tests': ['coverage'],
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
# If there are data files included in your packages that need to be
|
|
172
|
+
# installed, specify them here.
|
|
173
|
+
#
|
|
174
|
+
# If using Python 2.6 or earlier, then these have to be included in
|
|
175
|
+
# MANIFEST.in as well.
|
|
176
|
+
package_data={ # Optional
|
|
177
|
+
# 'sample': ['package_data.dat'],
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
# Although 'package_data' is the preferred approach, in some case you may
|
|
181
|
+
# need to place data files outside of your packages. See:
|
|
182
|
+
# http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files
|
|
183
|
+
#
|
|
184
|
+
# In this case, 'data_file' will be installed into '<sys.prefix>/my_data'
|
|
185
|
+
# data_files=[('my_data', ['data/data_file'])], # Optional
|
|
186
|
+
data_files=[], # Optional
|
|
187
|
+
|
|
188
|
+
# To provide executable scripts, use entry points in preference to the
|
|
189
|
+
# "scripts" keyword. Entry points provide cross-platform support and allow
|
|
190
|
+
# `pip` to create the appropriate form of executable for the target
|
|
191
|
+
# platform.
|
|
192
|
+
#
|
|
193
|
+
# For example, the following would provide a command called `sample` which
|
|
194
|
+
# executes the function `main` from this package when invoked:
|
|
195
|
+
# entry_points={ # Optional
|
|
196
|
+
# 'console_scripts': [
|
|
197
|
+
# 'sample=sample:main',
|
|
198
|
+
# ],
|
|
199
|
+
# },
|
|
200
|
+
)
|
|
@@ -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,77 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import urllib.parse as urlparse # pip install urllib3
|
|
5
|
+
import urllib3
|
|
6
|
+
|
|
7
|
+
from configobj import ConfigObj # pip install configobj
|
|
8
|
+
from .path_helpers import prettify
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ResourceLoader(object):
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def load_from_locations(cls, filename: str, locations: str or list[str]) -> ConfigObj:
|
|
15
|
+
"""Searches for a resource file in the given locations and loads them.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
if isinstance(locations, str):
|
|
19
|
+
locations = [locations, ]
|
|
20
|
+
|
|
21
|
+
for location in locations:
|
|
22
|
+
url = urlparse.urlparse(location)
|
|
23
|
+
|
|
24
|
+
if url.scheme == 'home':
|
|
25
|
+
base_path = os.path.expanduser('~') + url.path
|
|
26
|
+
file_path = os.path.join(base_path, filename)
|
|
27
|
+
file_path = prettify(file_path)
|
|
28
|
+
|
|
29
|
+
if os.path.isfile(file_path):
|
|
30
|
+
properties = ConfigObj(file_path)
|
|
31
|
+
return properties
|
|
32
|
+
elif url.scheme in ('file', 'path'):
|
|
33
|
+
file_path = os.path.join(url.path, filename)
|
|
34
|
+
if os.path.isfile(file_path):
|
|
35
|
+
properties = ConfigObj(file_path)
|
|
36
|
+
return properties
|
|
37
|
+
elif url.scheme == 'http' or \
|
|
38
|
+
url.scheme == 'https':
|
|
39
|
+
if not location.endswith('/'):
|
|
40
|
+
location += '/'
|
|
41
|
+
file_path = location + filename
|
|
42
|
+
try:
|
|
43
|
+
http = urllib3.PoolManager()
|
|
44
|
+
r = http.request('GET', file_path)
|
|
45
|
+
file_content = r.data
|
|
46
|
+
properties = ConfigObj(file_content)
|
|
47
|
+
return properties
|
|
48
|
+
except NameError:
|
|
49
|
+
pass
|
|
50
|
+
return ConfigObj()
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def find_first_occurrence(cls, filename: str, locations: str or list[str]) -> tuple[str or None, int]:
|
|
54
|
+
""" Searches for a file in given locations.
|
|
55
|
+
|
|
56
|
+
Returns the first location where the file was found by returning the path and the location
|
|
57
|
+
index where the file was found.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
if isinstance(locations, str):
|
|
61
|
+
locations = [locations, ]
|
|
62
|
+
|
|
63
|
+
for index, location in enumerate(locations):
|
|
64
|
+
url = urlparse.urlparse(location)
|
|
65
|
+
|
|
66
|
+
if url.scheme == 'home':
|
|
67
|
+
base_path = os.path.expanduser('~') + url.path
|
|
68
|
+
file_path = os.path.join(base_path, filename)
|
|
69
|
+
file_path = prettify(file_path)
|
|
70
|
+
|
|
71
|
+
if os.path.isfile(file_path):
|
|
72
|
+
return prettify(base_path), index
|
|
73
|
+
elif url.scheme in ('file', 'path'):
|
|
74
|
+
file_path = os.path.join(url.path, filename)
|
|
75
|
+
if os.path.isfile(file_path):
|
|
76
|
+
return prettify(url.path), index
|
|
77
|
+
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,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thingsboard-common
|
|
3
|
+
Version: 0.1.0
|
|
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,22 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
src/thingsboard/__init__.py
|
|
6
|
+
src/thingsboard/common/__init__.py
|
|
7
|
+
src/thingsboard/common/version.py
|
|
8
|
+
src/thingsboard/common/core/__init__.py
|
|
9
|
+
src/thingsboard/common/core/thread_monitor.py
|
|
10
|
+
src/thingsboard/common/core/threaded.py
|
|
11
|
+
src/thingsboard/common/utils/__init__.py
|
|
12
|
+
src/thingsboard/common/utils/attribute_helpers.py
|
|
13
|
+
src/thingsboard/common/utils/datetime_helpers.py
|
|
14
|
+
src/thingsboard/common/utils/path_helpers.py
|
|
15
|
+
src/thingsboard/common/utils/resource_loader.py
|
|
16
|
+
src/thingsboard/common/utils/string_helpers.py
|
|
17
|
+
src/thingsboard/common/utils/timestamp_helpers.py
|
|
18
|
+
src/thingsboard_common.egg-info/PKG-INFO
|
|
19
|
+
src/thingsboard_common.egg-info/SOURCES.txt
|
|
20
|
+
src/thingsboard_common.egg-info/dependency_links.txt
|
|
21
|
+
src/thingsboard_common.egg-info/requires.txt
|
|
22
|
+
src/thingsboard_common.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
thingsboard
|