cleepcli 1.43.4__tar.gz → 1.43.7__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.
- cleepcli-1.43.7/PKG-INFO +30 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/README.md +52 -29
- cleepcli-1.43.7/cleepcli/ci.py +397 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/cleepapi.py +48 -1
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/docs.py +210 -115
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/module.py +4 -3
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/package.py +158 -102
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/test.py +2 -2
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/version.py +1 -2
- cleepcli-1.43.7/cleepcli.egg-info/PKG-INFO +30 -0
- cleepcli-1.43.4/PKG-INFO +0 -15
- cleepcli-1.43.4/cleepcli/ci.py +0 -301
- cleepcli-1.43.4/cleepcli.egg-info/PKG-INFO +0 -15
- {cleepcli-1.43.4 → cleepcli-1.43.7}/LICENSE +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/MANIFEST.in +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/bin/ccli +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/bin/cleep-cli +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/__init__.py +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/check.py +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/config.py +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/console.py +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/file.py +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/git.py +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/tools.py +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli/watch.py +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli.egg-info/SOURCES.txt +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli.egg-info/dependency_links.txt +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli.egg-info/requires.txt +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/cleepcli.egg-info/top_level.txt +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/setup.cfg +0 -0
- {cleepcli-1.43.4 → cleepcli-1.43.7}/setup.py +0 -0
cleepcli-1.43.7/PKG-INFO
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cleepcli
|
|
3
|
+
Version: 1.43.7
|
|
4
|
+
Summary: Cleep-cli helps developers to build great Cleep applications from command line.
|
|
5
|
+
Home-page: https://github.com/CleepDevice/cleep-cli
|
|
6
|
+
Author: Tanguy Bonneau
|
|
7
|
+
Author-email: tanguy.bonneau@gmail.com
|
|
8
|
+
Maintainer: Tanguy Bonneau
|
|
9
|
+
Maintainer-email: tanguy.bonneau@gmail.com
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: Click<9.0,>=8.0
|
|
12
|
+
Requires-Dist: watchdog<4.0.0,>=3.0.0
|
|
13
|
+
Requires-Dist: Sphinx<6.0.0,>=5.0.0
|
|
14
|
+
Requires-Dist: sphinx-rtd-theme>=1.0.0
|
|
15
|
+
Requires-Dist: CProfileV>=1.0.7
|
|
16
|
+
Requires-Dist: PyGithub==1.54.1
|
|
17
|
+
Requires-Dist: pylint<3.0.0,>=2.11.1
|
|
18
|
+
Requires-Dist: semver==3.0.0
|
|
19
|
+
Requires-Dist: coverage==7.2.7
|
|
20
|
+
Requires-Dist: passlib==1.7.4
|
|
21
|
+
Requires-Dist: requests==2.31.0
|
|
22
|
+
Requires-Dist: psutil==5.9.4
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: author-email
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: maintainer
|
|
28
|
+
Dynamic: maintainer-email
|
|
29
|
+
Dynamic: requires-dist
|
|
30
|
+
Dynamic: summary
|
|
@@ -1,60 +1,70 @@
|
|
|
1
1
|
# Cleep-cli
|
|
2
2
|
|
|
3
3
|
This utility helps developers to build Cleep applications providing some useful commands:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
4
|
+
|
|
5
|
+
- Core commands:
|
|
6
|
+
- `coreget` to clone or pull official Cleep repository.
|
|
7
|
+
- `coredocs` to generate core documentation in HTML format.
|
|
8
|
+
- `coresync` to synchronize sources from repository to execution folders.
|
|
9
|
+
- `coretests` to perform core tests suite.
|
|
10
|
+
- `coretestscov` to display last core tests code coverage report.
|
|
11
|
+
- `cpuprof` to run CPU profiler on application execution instance.
|
|
12
|
+
- `memprof` to run memory profiler on application execution instance.
|
|
13
|
+
- `reset` clear existing installed Cleep files and install it again.
|
|
14
|
+
- Module commands:
|
|
15
|
+
- `modcreate` to create module (aka application) skeleton.
|
|
16
|
+
- `moddelete` to delete installed files of a module.
|
|
17
|
+
- `moddocs` to generate module documentation.
|
|
18
|
+
- `modsync` to synchronize sources from module folder to execution folders.
|
|
19
|
+
- `modtests` to execute module tests.
|
|
20
|
+
- `modtestscov` to display last module tests code coverage report.
|
|
21
|
+
- `modbuild` to create application package (.zip)
|
|
22
|
+
- `modcheckback` to check app backend (python files structure and content)
|
|
23
|
+
- `modcheckfront` to check app frontend (js files and desc.json)
|
|
24
|
+
- `modchecktests` to check app tests files
|
|
25
|
+
- `modcheckscripts` to check app scripts files
|
|
26
|
+
- `modcheckcode` to check backend code quality (run pylint)
|
|
27
|
+
- Watch commands:
|
|
28
|
+
- `watch` to watch for repository filesystem changes and automatically update files on execution folders. It also restart backend or/and frontend according to changes.
|
|
29
|
+
- Misc commands:
|
|
30
|
+
- `version` to get cleep-cli version
|
|
30
31
|
|
|
31
32
|
## Installation
|
|
33
|
+
|
|
32
34
|
Cleep-cli is automatically installed and managed by [Cleep developer](https://github.com/tangb/cleepmod-developer) application.
|
|
33
35
|
|
|
34
36
|
If you want to install it manually, execute following command:
|
|
37
|
+
|
|
35
38
|
> pip install cleepcli
|
|
36
39
|
|
|
37
40
|
## Compatibility
|
|
41
|
+
|
|
38
42
|
This cli is written in Python and is supposed to be used only on Raspbian platform for Cleep application development because it has harcoded paths.
|
|
39
43
|
|
|
40
44
|
## Help
|
|
45
|
+
|
|
41
46
|
Execute this command to get help on this cli:
|
|
47
|
+
|
|
42
48
|
> cleep-cli --help
|
|
43
49
|
|
|
44
50
|
## Watch usage
|
|
51
|
+
|
|
45
52
|
Launch `watch` cli command to monitor changes on "/root/cleep-dev" directory after getting official Cleep repository using `coreget` command.
|
|
46
53
|
|
|
47
54
|
Then open your favorite editor on your development computer, configure a remote sync plugin to push local changes on your raspberry pi then cleep-cli will synchronize sources on Cleep execution environment and restart application automatically.
|
|
48
55
|
|
|
49
56
|
### Paths
|
|
57
|
+
|
|
50
58
|
The cli clone repository on `/root/cleep-dev` folder. Modules are in `/root/cleep-dev/modules` folder.
|
|
51
59
|
So if you develop on Cleep core, your editor must sync to `/root/cleep-dev` remote directory while if you're developping only on a module your editor must sync to `/root/cleep-dev/modules/<your module name>` remote directory.
|
|
52
60
|
|
|
53
61
|
### Visual studio code
|
|
62
|
+
|
|
54
63
|
Install Visual studio code [sftp](https://marketplace.visualstudio.com/items?itemName=Natizyskunk.sftp) plugin and configure it to access your raspberry pi:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
|
|
65
|
+
- Open VSCode command palette pressing CTRL-SHIFT-P
|
|
66
|
+
- Type "sftp: config"
|
|
67
|
+
- Fill opened file with:
|
|
58
68
|
|
|
59
69
|
```
|
|
60
70
|
{
|
|
@@ -81,9 +91,11 @@ Install Visual studio code [sftp](https://marketplace.visualstudio.com/items?ite
|
|
|
81
91
|
```
|
|
82
92
|
|
|
83
93
|
### Local developments
|
|
94
|
+
|
|
84
95
|
If you want to develop directly on the raspberry using vim or nano, simply develop on `/root/cleep-dev/` directory. Cleep-cli watcher will synchronize all your modifications.
|
|
85
96
|
|
|
86
97
|
## How it works
|
|
98
|
+
|
|
87
99
|
This cli executes git commands to clone or update repository.
|
|
88
100
|
|
|
89
101
|
It uses rsync commands to synchronize files.
|
|
@@ -92,3 +104,14 @@ It uses pylint to check code quality (it returns a score)
|
|
|
92
104
|
|
|
93
105
|
The [watchdog](https://pypi.org/project/watchdog/) python library is used to detect changes on filesystem. According to changed files, it detects if change occurs on frontend or backend files and send commands to restart Cleep application.
|
|
94
106
|
|
|
107
|
+
## CI (GitHub Actions)
|
|
108
|
+
|
|
109
|
+
A tag `vX.Y.Z` (or a manual **workflow_dispatch**) builds a **universal** wheel + sdist on `ubuntu-latest` (Python 3.9) and uploads them to PyPI.
|
|
110
|
+
|
|
111
|
+
GitHub **environment** `actions` (Settings → Environments), secret:
|
|
112
|
+
|
|
113
|
+
| Secret | Description |
|
|
114
|
+
| ---------------- | ----------------------------------------------------------- |
|
|
115
|
+
| `PYPI_API_TOKEN` | PyPI API token (`pypi-...`). Username Twine is `__token__`. |
|
|
116
|
+
|
|
117
|
+
No Raspberry Pi / Woodpecker needed: the package is pure Python (`py2.py3-none-any`).
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import zipfile
|
|
6
|
+
import os
|
|
7
|
+
import glob
|
|
8
|
+
import re
|
|
9
|
+
import requests
|
|
10
|
+
import time
|
|
11
|
+
import urllib.parse
|
|
12
|
+
from . import config
|
|
13
|
+
from .console import Console
|
|
14
|
+
from .check import Check
|
|
15
|
+
from .cleepapi import resolve_rpc_url
|
|
16
|
+
import subprocess
|
|
17
|
+
|
|
18
|
+
requests.packages.urllib3.disable_warnings()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Ci:
|
|
22
|
+
"""
|
|
23
|
+
Continuous Integration helpers
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
TESTS_REQUIREMENTS_TXT = "tests/requirements.txt"
|
|
27
|
+
EXTRACT_DIR = "/tmp/extract"
|
|
28
|
+
APP_FILENAME_PATTERN = "APP_FILENAME=['\"](.*)['\"]"
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
"""
|
|
32
|
+
Constructor
|
|
33
|
+
"""
|
|
34
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
|
35
|
+
self.command_url = None
|
|
36
|
+
self.health_url = None
|
|
37
|
+
|
|
38
|
+
def mod_check_package(self, package_path):
|
|
39
|
+
"""
|
|
40
|
+
Check specified package content
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
package_path (string): package path
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
dict: package informations::
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
module_name (str): module name
|
|
50
|
+
module_version (str): module version
|
|
51
|
+
has_tests_requirements (bool): True if tests/requirements.txt exists
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
Exception if something bad detected
|
|
56
|
+
"""
|
|
57
|
+
self.logger.info('Checking application package "%s"' % package_path)
|
|
58
|
+
_, module_name, module_version = os.path.basename(package_path).split("_")
|
|
59
|
+
module_version = module_version.replace(".zip", "")[1:]
|
|
60
|
+
|
|
61
|
+
# check raw file
|
|
62
|
+
if not module_version:
|
|
63
|
+
raise Exception("Invalid package filename")
|
|
64
|
+
if not re.match("\d+\.\d+\.\d+", module_version):
|
|
65
|
+
raise Exception("Invalid package filename")
|
|
66
|
+
console = Console()
|
|
67
|
+
resp = console.command('file --keep-going --mime-type "%s"' % package_path)
|
|
68
|
+
if resp["returncode"] != 0:
|
|
69
|
+
raise Exception("Unable to check file validity")
|
|
70
|
+
filetype = resp["stdout"][0].split(": ")[1].strip()
|
|
71
|
+
self.logger.debug("Filetype=%s" % filetype)
|
|
72
|
+
if filetype != "application/zip\\012- application/octet-stream":
|
|
73
|
+
raise Exception("Invalid application package file")
|
|
74
|
+
|
|
75
|
+
# check package structure
|
|
76
|
+
has_tests_requirements = False
|
|
77
|
+
checks = {
|
|
78
|
+
"dir_backend": False,
|
|
79
|
+
"dir_frontend": False,
|
|
80
|
+
"dir_tests": False,
|
|
81
|
+
"file_module_json": False,
|
|
82
|
+
"file_desc_json": False,
|
|
83
|
+
"file_module_py": False,
|
|
84
|
+
"file_backend_init_py": False,
|
|
85
|
+
"file_tests_init_py": False,
|
|
86
|
+
}
|
|
87
|
+
with zipfile.ZipFile(package_path, "r") as zp:
|
|
88
|
+
for zfile in zp.infolist():
|
|
89
|
+
if zfile.filename.startswith("backend/modules/%s" % module_name):
|
|
90
|
+
checks["dir_backend"] = True
|
|
91
|
+
if zfile.filename.startswith("frontend/js/modules/%s" % module_name):
|
|
92
|
+
checks["dir_frontend"] = True
|
|
93
|
+
if zfile.filename.startswith("tests"):
|
|
94
|
+
checks["dir_tests"] = True
|
|
95
|
+
if zfile.filename == "backend/modules/%s/%s.py" % (
|
|
96
|
+
module_name,
|
|
97
|
+
module_name,
|
|
98
|
+
):
|
|
99
|
+
checks["file_module_py"] = True
|
|
100
|
+
if zfile.filename == "frontend/js/modules/%s/desc.json" % module_name:
|
|
101
|
+
checks["file_desc_json"] = True
|
|
102
|
+
if zfile.filename == "module.json":
|
|
103
|
+
checks["file_module_json"] = True
|
|
104
|
+
if zfile.filename == self.TESTS_REQUIREMENTS_TXT:
|
|
105
|
+
has_tests_requirements = True
|
|
106
|
+
if zfile.filename == "backend/modules/%s/__init__.py" % module_name:
|
|
107
|
+
checks["file_backend_init_py"] = True
|
|
108
|
+
if zfile.filename == "tests/__init__.py":
|
|
109
|
+
checks["file_tests_init_py"] = True
|
|
110
|
+
|
|
111
|
+
# special case for custom main module filename
|
|
112
|
+
if checks["file_backend_init_py"] and not checks["file_module_py"]:
|
|
113
|
+
content = (
|
|
114
|
+
zp.read("backend/modules/%s/__init__.py" % module_name)
|
|
115
|
+
.decode("utf8")
|
|
116
|
+
.strip()
|
|
117
|
+
)
|
|
118
|
+
matches = re.findall(self.APP_FILENAME_PATTERN, content)
|
|
119
|
+
if (
|
|
120
|
+
len(matches) == 1
|
|
121
|
+
and "backend/modules/%s/%s.py" % (module_name, matches[0])
|
|
122
|
+
in zp.namelist()
|
|
123
|
+
):
|
|
124
|
+
checks["file_module_py"] = True
|
|
125
|
+
|
|
126
|
+
self.logger.debug("Checks results: %s" % checks)
|
|
127
|
+
if not all(checks.values()):
|
|
128
|
+
raise Exception(
|
|
129
|
+
"Invalid package structure. Make sure to build it with developer application"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"module_name": module_name,
|
|
134
|
+
"module_version": module_version,
|
|
135
|
+
"has_test_requirements": has_tests_requirements,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def mod_install_sources(
|
|
139
|
+
self, package_path, package_infos, no_compatibility_check=False
|
|
140
|
+
):
|
|
141
|
+
"""
|
|
142
|
+
Install module package (zip archive) sources
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
package_path (string): package path
|
|
146
|
+
package_infos (dict): infos returned by mod_check_package function
|
|
147
|
+
no_compatitiblity_check (bool): do not check module compatibility (but only deps compat)
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
Exception if error occured
|
|
151
|
+
"""
|
|
152
|
+
module_name, module_version, has_tests_requirements = package_infos.values()
|
|
153
|
+
self.logger.info(
|
|
154
|
+
"Installing application %s v%s..." % (module_name, module_version)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
cleep_proc = None
|
|
158
|
+
try:
|
|
159
|
+
# start cleep (non blocking)
|
|
160
|
+
self.logger.info(" Starting Cleep...")
|
|
161
|
+
cleep_proc = subprocess.Popen(
|
|
162
|
+
["cleep", "--noro"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
163
|
+
)
|
|
164
|
+
time.sleep(15)
|
|
165
|
+
self._bind_rpc()
|
|
166
|
+
|
|
167
|
+
# make sure to have latest modules.json version
|
|
168
|
+
self.logger.info(" Refreshing markets...")
|
|
169
|
+
resp = self._rpc_post(
|
|
170
|
+
{
|
|
171
|
+
"command": "check_modules_updates",
|
|
172
|
+
"to": "update",
|
|
173
|
+
"timeout": 60.0,
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
resp.raise_for_status()
|
|
177
|
+
resp_json = resp.json()
|
|
178
|
+
if resp_json["error"]:
|
|
179
|
+
raise Exception("Check_modules_updates command failed: %s" % resp_json)
|
|
180
|
+
|
|
181
|
+
# update "update" module to enjoy bug fixes
|
|
182
|
+
self.logger.info(' Updating "update" application...')
|
|
183
|
+
resp = self._rpc_post(
|
|
184
|
+
{
|
|
185
|
+
"command": "update_module",
|
|
186
|
+
"to": "update",
|
|
187
|
+
"params": {
|
|
188
|
+
"module_name": "update",
|
|
189
|
+
},
|
|
190
|
+
"timeout": 60.0,
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
resp.raise_for_status()
|
|
194
|
+
resp_json = resp.json()
|
|
195
|
+
self.logger.debug('Update "update" resp: %s' % resp_json)
|
|
196
|
+
if resp_json["error"]:
|
|
197
|
+
raise Exception('Updating app "update" failed: %s' % resp_json)
|
|
198
|
+
if resp_json["data"]:
|
|
199
|
+
self.__wait_for_cleep_process("update")
|
|
200
|
+
self.logger.info(" Restarting Cleep...")
|
|
201
|
+
cleep_proc.kill()
|
|
202
|
+
cleep_proc = subprocess.Popen(
|
|
203
|
+
["cleep", "--noro"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
204
|
+
)
|
|
205
|
+
time.sleep(15)
|
|
206
|
+
self._bind_rpc()
|
|
207
|
+
|
|
208
|
+
# install module in cleep (it will also install deps)
|
|
209
|
+
self.logger.info(' Installing "%s" application in Cleep' % module_name)
|
|
210
|
+
resp = self._rpc_post(
|
|
211
|
+
{
|
|
212
|
+
"command": "install_module",
|
|
213
|
+
"to": "update",
|
|
214
|
+
"params": {
|
|
215
|
+
"module_name": module_name,
|
|
216
|
+
"package": package_path,
|
|
217
|
+
"no_compatibility_check": no_compatibility_check,
|
|
218
|
+
},
|
|
219
|
+
"timeout": 60.0,
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
resp.raise_for_status()
|
|
223
|
+
resp_json = resp.json()
|
|
224
|
+
if resp_json["error"]:
|
|
225
|
+
raise Exception(
|
|
226
|
+
'Installing "%s" app failed: %s' % (module_name, resp_json)
|
|
227
|
+
)
|
|
228
|
+
self.__wait_for_cleep_process(module_name)
|
|
229
|
+
|
|
230
|
+
# restart cleep
|
|
231
|
+
self.logger.info(" Restarting Cleep...")
|
|
232
|
+
cleep_proc.kill()
|
|
233
|
+
cleep_proc = subprocess.Popen(
|
|
234
|
+
["cleep", "--noro"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
235
|
+
)
|
|
236
|
+
time.sleep(15)
|
|
237
|
+
self._bind_rpc()
|
|
238
|
+
|
|
239
|
+
# check module is installed and running
|
|
240
|
+
self.logger.info(" Checking application is installed...")
|
|
241
|
+
resp = self._rpc_get(self.health_url)
|
|
242
|
+
self.logger.info("Health response: %s", resp.json())
|
|
243
|
+
resp.raise_for_status()
|
|
244
|
+
self.logger.info("Application and its dependencies successfully installed")
|
|
245
|
+
|
|
246
|
+
# install requirements.txt for tests
|
|
247
|
+
if has_tests_requirements:
|
|
248
|
+
self.logger.info("Installing tests python dependencies...")
|
|
249
|
+
console = Console()
|
|
250
|
+
requirements_txt_path = os.path.join(
|
|
251
|
+
config.MODULES_SRC, module_name, self.TESTS_REQUIREMENTS_TXT
|
|
252
|
+
)
|
|
253
|
+
self.logger.debug(
|
|
254
|
+
'Tests requirements.txt path "%s"', requirements_txt_path
|
|
255
|
+
)
|
|
256
|
+
resp = console.command(
|
|
257
|
+
'python3 -m pip install --trusted-host pypi.org -r "%s"'
|
|
258
|
+
% requirements_txt_path,
|
|
259
|
+
900,
|
|
260
|
+
)
|
|
261
|
+
self.logger.debug("Resp: %s" % resp)
|
|
262
|
+
if resp["returncode"] != 0:
|
|
263
|
+
self.logger.error(
|
|
264
|
+
"Error installing tests requirements.txt: %s", resp
|
|
265
|
+
)
|
|
266
|
+
raise Exception(
|
|
267
|
+
"Error installing tests requirements.txt (killed=%s)"
|
|
268
|
+
% resp["killed"]
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
finally:
|
|
272
|
+
if cleep_proc:
|
|
273
|
+
cleep_proc.kill()
|
|
274
|
+
|
|
275
|
+
def _bind_rpc(self):
|
|
276
|
+
"""
|
|
277
|
+
Detect Cleep RPC endpoint (HTTPS first, then HTTP)
|
|
278
|
+
"""
|
|
279
|
+
base = resolve_rpc_url()
|
|
280
|
+
self.command_url = urllib.parse.urljoin(base + "/", "command")
|
|
281
|
+
self.health_url = urllib.parse.urljoin(base + "/", "health")
|
|
282
|
+
|
|
283
|
+
def _rpc_post(self, payload):
|
|
284
|
+
"""
|
|
285
|
+
POST JSON to Cleep /command (self-signed TLS accepted).
|
|
286
|
+
"""
|
|
287
|
+
return requests.post(self.command_url, json=payload, verify=False)
|
|
288
|
+
|
|
289
|
+
def _rpc_get(self, url):
|
|
290
|
+
"""
|
|
291
|
+
GET Cleep RPC url (self-signed TLS accepted).
|
|
292
|
+
"""
|
|
293
|
+
return requests.get(url, verify=False)
|
|
294
|
+
|
|
295
|
+
def __wait_for_cleep_process(self, module_name):
|
|
296
|
+
"""
|
|
297
|
+
Wait for end of current Cleep process (install, update...)
|
|
298
|
+
"""
|
|
299
|
+
while True:
|
|
300
|
+
time.sleep(1.0)
|
|
301
|
+
resp = self._rpc_post(
|
|
302
|
+
{
|
|
303
|
+
"command": "get_modules_updates",
|
|
304
|
+
"to": "update",
|
|
305
|
+
"timeout": 60.0,
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
resp.raise_for_status()
|
|
309
|
+
resp_json = resp.json()
|
|
310
|
+
if resp_json["error"]:
|
|
311
|
+
raise Exception("Get_modules_updates command failed")
|
|
312
|
+
module_updates = resp_json["data"].get(module_name)
|
|
313
|
+
self.logger.debug("Updates: %s" % module_updates)
|
|
314
|
+
if not module_updates:
|
|
315
|
+
raise Exception(
|
|
316
|
+
'No "%s" application info in updates data' % module_name
|
|
317
|
+
)
|
|
318
|
+
if module_updates["processing"] == False:
|
|
319
|
+
if module_updates["update"]["failed"]:
|
|
320
|
+
raise Exception(
|
|
321
|
+
'Application "%s" installation failed' % module_name
|
|
322
|
+
)
|
|
323
|
+
break
|
|
324
|
+
|
|
325
|
+
def mod_extract_sources(self, package_path, package_infos):
|
|
326
|
+
"""
|
|
327
|
+
Extract module package (zip archive) in dev directory
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
package_path (string): package path
|
|
331
|
+
package_infos (dict): infos returned by mod_check_package function
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
Exception if error occured
|
|
335
|
+
"""
|
|
336
|
+
module_name, module_version, _ = package_infos.values()
|
|
337
|
+
|
|
338
|
+
# unzip content
|
|
339
|
+
self.logger.info(
|
|
340
|
+
'Extracting archive "%s" to "%s"' % (package_path, self.EXTRACT_DIR)
|
|
341
|
+
)
|
|
342
|
+
with zipfile.ZipFile(package_path, "r") as package:
|
|
343
|
+
package.extractall(self.EXTRACT_DIR)
|
|
344
|
+
|
|
345
|
+
# install sources
|
|
346
|
+
os.makedirs(os.path.join(config.MODULES_SRC, module_name), exist_ok=True)
|
|
347
|
+
for filepath in glob.glob(self.EXTRACT_DIR + "/**/*.*", recursive=True):
|
|
348
|
+
if filepath.startswith(os.path.join(self.EXTRACT_DIR, "frontend")):
|
|
349
|
+
dest = filepath.replace(
|
|
350
|
+
os.path.join(
|
|
351
|
+
self.EXTRACT_DIR, "frontend/js/modules/%s" % module_name
|
|
352
|
+
),
|
|
353
|
+
os.path.join(config.MODULES_SRC, module_name, "frontend"),
|
|
354
|
+
)
|
|
355
|
+
self.logger.debug(" -> frontend: %s" % dest)
|
|
356
|
+
elif filepath.startswith(os.path.join(self.EXTRACT_DIR, "backend")):
|
|
357
|
+
dest = filepath.replace(
|
|
358
|
+
os.path.join(self.EXTRACT_DIR, "backend/modules/%s" % module_name),
|
|
359
|
+
os.path.join(config.MODULES_SRC, module_name, "backend"),
|
|
360
|
+
)
|
|
361
|
+
self.logger.debug(" -> backend: %s" % dest)
|
|
362
|
+
elif filepath.startswith(os.path.join(self.EXTRACT_DIR, "tests")):
|
|
363
|
+
dest = filepath.replace(
|
|
364
|
+
os.path.join(self.EXTRACT_DIR, "tests"),
|
|
365
|
+
os.path.join(config.MODULES_SRC, module_name, "tests"),
|
|
366
|
+
)
|
|
367
|
+
self.logger.debug(" -> tests: %s" % dest)
|
|
368
|
+
elif filepath.startswith(os.path.join(self.EXTRACT_DIR, "scripts")):
|
|
369
|
+
dest = filepath.replace(
|
|
370
|
+
os.path.join(self.EXTRACT_DIR, "scripts"),
|
|
371
|
+
os.path.join(config.MODULES_SRC, module_name, "scripts"),
|
|
372
|
+
)
|
|
373
|
+
self.logger.debug(" -> scripts: %s" % dest)
|
|
374
|
+
else:
|
|
375
|
+
dest = filepath.replace(
|
|
376
|
+
self.EXTRACT_DIR, os.path.join(config.MODULES_SRC, module_name)
|
|
377
|
+
)
|
|
378
|
+
self.logger.debug(" -> other: %s" % dest)
|
|
379
|
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
380
|
+
os.rename(filepath, dest)
|
|
381
|
+
|
|
382
|
+
def mod_check(self, module_name):
|
|
383
|
+
"""
|
|
384
|
+
Perform some checkings (see check.py file) for continuous integration
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
module_name (string): module name
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
Exception if error occured
|
|
391
|
+
"""
|
|
392
|
+
check = Check()
|
|
393
|
+
|
|
394
|
+
check.check_backend(module_name)
|
|
395
|
+
check.check_frontend(module_name)
|
|
396
|
+
check.check_scripts(module_name)
|
|
397
|
+
check.check_tests(module_name)
|
|
@@ -10,6 +10,50 @@ import requests
|
|
|
10
10
|
import json
|
|
11
11
|
import urllib.parse
|
|
12
12
|
|
|
13
|
+
requests.packages.urllib3.disable_warnings()
|
|
14
|
+
|
|
15
|
+
# Cleep 0.1+ serves HTTPS by default; older versions listen on HTTP only.
|
|
16
|
+
DEFAULT_RPC_URLS = (
|
|
17
|
+
'https://127.0.0.1:443',
|
|
18
|
+
'http://127.0.0.1:80',
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_rpc_url(urls=None, timeout=3.0):
|
|
23
|
+
"""
|
|
24
|
+
Return the first reachable Cleep RPC base URL.
|
|
25
|
+
|
|
26
|
+
Tries HTTPS first (Cleep default), then HTTP. Self-signed certificates
|
|
27
|
+
are accepted. A candidate is considered reachable as soon as a TCP/TLS
|
|
28
|
+
session is established (any HTTP status).
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
urls (tuple): candidate base URLs
|
|
32
|
+
timeout (float): per-candidate timeout in seconds
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
str: reachable base URL (no trailing slash)
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
Exception: if none of the candidates respond
|
|
39
|
+
"""
|
|
40
|
+
logger = logging.getLogger('CleepApi')
|
|
41
|
+
candidates = urls or DEFAULT_RPC_URLS
|
|
42
|
+
last_error = None
|
|
43
|
+
for base in candidates:
|
|
44
|
+
health_url = urllib.parse.urljoin(base.rstrip('/') + '/', 'health')
|
|
45
|
+
try:
|
|
46
|
+
logger.debug('Probing Cleep RPC %s', health_url)
|
|
47
|
+
requests.get(health_url, timeout=timeout, verify=False)
|
|
48
|
+
logger.info('Using Cleep RPC %s', base)
|
|
49
|
+
return base.rstrip('/')
|
|
50
|
+
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout, requests.exceptions.SSLError) as error:
|
|
51
|
+
logger.debug('Cleep RPC not reachable at %s: %s', base, error)
|
|
52
|
+
last_error = error
|
|
53
|
+
|
|
54
|
+
raise Exception('Unable to reach Cleep RPC (tried %s): %s' % (', '.join(candidates), last_error))
|
|
55
|
+
|
|
56
|
+
|
|
13
57
|
class CleepApi():
|
|
14
58
|
"""
|
|
15
59
|
Cleep api helper
|
|
@@ -19,7 +63,10 @@ class CleepApi():
|
|
|
19
63
|
self.logger = logging.getLogger(self.__class__.__name__)
|
|
20
64
|
|
|
21
65
|
if not rpc_url:
|
|
22
|
-
|
|
66
|
+
try:
|
|
67
|
+
rpc_url = resolve_rpc_url()
|
|
68
|
+
except Exception:
|
|
69
|
+
rpc_url = DEFAULT_RPC_URLS[0]
|
|
23
70
|
self.logger.debug('RPC url: %s', rpc_url)
|
|
24
71
|
|
|
25
72
|
self.command_url = urllib.parse.urljoin(rpc_url, "/command")
|