cleepcli 1.46.0__tar.gz → 1.48.1__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.
Files changed (42) hide show
  1. {cleepcli-1.46.0 → cleepcli-1.48.1}/MANIFEST.in +1 -0
  2. {cleepcli-1.46.0/cleepcli.egg-info → cleepcli-1.48.1}/PKG-INFO +25 -9
  3. {cleepcli-1.46.0 → cleepcli-1.48.1}/README.md +19 -3
  4. {cleepcli-1.46.0 → cleepcli-1.48.1}/bin/ccli +65 -4
  5. {cleepcli-1.46.0 → cleepcli-1.48.1}/bin/cleep-cli +65 -4
  6. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/check.py +44 -0
  7. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/cleepapi.py +26 -0
  8. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/file.py +45 -0
  9. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/package.py +10 -0
  10. cleepcli-1.48.1/cleepcli/systemd/cleep-watch.service +18 -0
  11. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/version.py +1 -1
  12. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/watch.py +30 -2
  13. cleepcli-1.48.1/cleepcli/watchsvc.py +214 -0
  14. {cleepcli-1.46.0 → cleepcli-1.48.1/cleepcli.egg-info}/PKG-INFO +25 -9
  15. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli.egg-info/SOURCES.txt +5 -1
  16. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli.egg-info/requires.txt +5 -5
  17. cleepcli-1.48.1/requirements.txt +18 -0
  18. {cleepcli-1.46.0 → cleepcli-1.48.1}/setup.py +1 -0
  19. cleepcli-1.48.1/tests/test_file_sync_all.py +101 -0
  20. {cleepcli-1.46.0 → cleepcli-1.48.1}/tests/test_watch.py +7 -0
  21. cleepcli-1.48.1/tests/test_watchsvc.py +57 -0
  22. cleepcli-1.46.0/requirements.txt +0 -15
  23. {cleepcli-1.46.0 → cleepcli-1.48.1}/LICENSE +0 -0
  24. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/__init__.py +0 -0
  25. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/ci.py +0 -0
  26. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/config.py +0 -0
  27. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/console.py +0 -0
  28. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/docs.py +0 -0
  29. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/git.py +0 -0
  30. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/module.py +0 -0
  31. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/test.py +0 -0
  32. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli/tools.py +0 -0
  33. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli.egg-info/dependency_links.txt +0 -0
  34. {cleepcli-1.46.0 → cleepcli-1.48.1}/cleepcli.egg-info/top_level.txt +0 -0
  35. {cleepcli-1.46.0 → cleepcli-1.48.1}/pyproject.toml +0 -0
  36. {cleepcli-1.46.0 → cleepcli-1.48.1}/setup.cfg +0 -0
  37. {cleepcli-1.46.0 → cleepcli-1.48.1}/tests/test_check.py +0 -0
  38. {cleepcli-1.46.0 → cleepcli-1.48.1}/tests/test_config.py +0 -0
  39. {cleepcli-1.46.0 → cleepcli-1.48.1}/tests/test_console.py +0 -0
  40. {cleepcli-1.46.0 → cleepcli-1.48.1}/tests/test_coverage.py +0 -0
  41. {cleepcli-1.46.0 → cleepcli-1.48.1}/tests/test_docs_breaking.py +0 -0
  42. {cleepcli-1.46.0 → cleepcli-1.48.1}/tests/test_module_scaffold.py +0 -0
@@ -7,3 +7,4 @@ include setup.cfg
7
7
  include MANIFEST.in
8
8
  recursive-include bin *
9
9
  recursive-include cleepcli *.py
10
+ recursive-include cleepcli/systemd *
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cleepcli
3
- Version: 1.46.0
3
+ Version: 1.48.1
4
4
  Summary: Cleep-cli helps developers to build great Cleep applications from command line.
5
5
  Home-page: https://github.com/CleepDevice/cleep-cli
6
6
  Author: Tanguy Bonneau
@@ -17,11 +17,11 @@ Requires-Dist: sphinx-rtd-theme>=1.0.0
17
17
  Requires-Dist: CProfileV>=1.0.7
18
18
  Requires-Dist: PyGithub==1.54.1
19
19
  Requires-Dist: pylint<3.0.0,>=2.11.1
20
- Requires-Dist: semver==3.0.0
21
- Requires-Dist: coverage==7.2.7
22
- Requires-Dist: passlib==1.7.4
23
- Requires-Dist: requests==2.31.0
24
- Requires-Dist: psutil==5.9.4
20
+ Requires-Dist: semver<4.0.0,>=3.0.0
21
+ Requires-Dist: coverage<8.0.0,>=7.2.7
22
+ Requires-Dist: passlib<2.0.0,>=1.7.4
23
+ Requires-Dist: requests<3.0.0,>=2.32.5
24
+ Requires-Dist: psutil<8.0.0,>=5.9.4
25
25
  Dynamic: author
26
26
  Dynamic: author-email
27
27
  Dynamic: description
@@ -61,7 +61,9 @@ This utility helps developers to build Cleep applications providing some useful
61
61
  - `modcheckscripts` to check app scripts files
62
62
  - `modcheckcode` to check backend code quality (run pylint)
63
63
  - Watch commands:
64
- - `watch` to watch for repository filesystem changes and automatically update files on execution folders. It also restart backend or/and frontend according to changes.
64
+ - `watch` to watch for repository filesystem changes and automatically update files on execution folders. It also restart backend or/and frontend according to changes (catch-up sync on start).
65
+ - `watchsvc` to install/enable/disable/restart the durable `cleep-watch` systemd unit (runs `watch` outside `cleep.service`).
66
+ - `syncall` to force a one-shot sync of core + all modules, disable static cache, and reload the UI.
65
67
  - Misc commands:
66
68
  - `version` to get cleep-cli version
67
69
 
@@ -85,9 +87,23 @@ Execute this command to get help on this cli:
85
87
 
86
88
  ## Watch usage
87
89
 
88
- Launch `watch` cli command to monitor changes on "/root/cleep-dev" directory after getting official Cleep repository using `coreget` command.
90
+ Preferred: let the CLI own the durable systemd unit:
89
91
 
90
- 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.
92
+ ```
93
+ cleep-cli watchsvc install
94
+ cleep-cli watchsvc enable
95
+ ```
96
+
97
+ The unit runs `cleep-cli watch`, survives `systemctl restart cleep`, and on each start performs a catch-up `syncall` so files written while the watcher was down are applied.
98
+
99
+ Manual one-shot resync (when the UI looks stale) — `syncall` also mints a fresh asset bust and reloads the device UI:
100
+
101
+ ```
102
+ cleep-cli syncall
103
+ cleep-cli watchsvc restart # catch-up again + resume watch
104
+ ```
105
+
106
+ You can still run `cleep-cli watch` in a terminal for debugging. Push sources to `/root/cleep-dev` (CleepDesktop developer sync or SFTP); the watcher copies them into the execution folders and restarts backend/frontend as needed.
91
107
 
92
108
  ### Paths
93
109
 
@@ -25,7 +25,9 @@ This utility helps developers to build Cleep applications providing some useful
25
25
  - `modcheckscripts` to check app scripts files
26
26
  - `modcheckcode` to check backend code quality (run pylint)
27
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.
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 (catch-up sync on start).
29
+ - `watchsvc` to install/enable/disable/restart the durable `cleep-watch` systemd unit (runs `watch` outside `cleep.service`).
30
+ - `syncall` to force a one-shot sync of core + all modules, disable static cache, and reload the UI.
29
31
  - Misc commands:
30
32
  - `version` to get cleep-cli version
31
33
 
@@ -49,9 +51,23 @@ Execute this command to get help on this cli:
49
51
 
50
52
  ## Watch usage
51
53
 
52
- Launch `watch` cli command to monitor changes on "/root/cleep-dev" directory after getting official Cleep repository using `coreget` command.
54
+ Preferred: let the CLI own the durable systemd unit:
53
55
 
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.
56
+ ```
57
+ cleep-cli watchsvc install
58
+ cleep-cli watchsvc enable
59
+ ```
60
+
61
+ The unit runs `cleep-cli watch`, survives `systemctl restart cleep`, and on each start performs a catch-up `syncall` so files written while the watcher was down are applied.
62
+
63
+ Manual one-shot resync (when the UI looks stale) — `syncall` also mints a fresh asset bust and reloads the device UI:
64
+
65
+ ```
66
+ cleep-cli syncall
67
+ cleep-cli watchsvc restart # catch-up again + resume watch
68
+ ```
69
+
70
+ You can still run `cleep-cli watch` in a terminal for debugging. Push sources to `/root/cleep-dev` (CleepDesktop developer sync or SFTP); the watcher copies them into the execution folders and restarts backend/frontend as needed.
55
71
 
56
72
  ### Paths
57
73
 
@@ -17,6 +17,7 @@ from cleepcli.git import Git
17
17
  from cleepcli.module import Module
18
18
  from cleepcli.file import File
19
19
  from cleepcli.watch import CleepWatchdog
20
+ from cleepcli.watchsvc import WatchService
20
21
  from cleepcli.test import Test, is_file_coverage_valid
21
22
  from cleepcli.package import Package
22
23
  from cleepcli.docs import Docs
@@ -108,6 +109,24 @@ def coresync():
108
109
  sys.exit(1)
109
110
  logging.info('Done')
110
111
 
112
+ @core.command()
113
+ def syncall():
114
+ """
115
+ Synchronize core and all modules from /root/cleep-dev to execution folders.
116
+
117
+ Same catch-up used when cleep-watch.service starts. Also disables frontend
118
+ static cache, mints a new asset bust, and reloads the UI so the device page
119
+ cannot keep stale JS.
120
+ """
121
+ from cleepcli.cleepapi import CleepApi
122
+
123
+ f = File()
124
+ if not f.sync_all():
125
+ sys.exit(1)
126
+ api = CleepApi(None)
127
+ api.ensure_frontend_nocache()
128
+ api.restart_frontend()
129
+
111
130
  @core.command()
112
131
  @click.option('--coverage', is_flag=True, help='Display coverage report.')
113
132
  @click.option('--output', is_flag=True, help='Display tests output.')
@@ -358,9 +377,13 @@ def watchdog():
358
377
  @click.option('--quiet', is_flag=True, help='Disable logging.')
359
378
  @click.option('--loglevel', default=logging.INFO, help='Logging level (10=DEBUG, 20=INFO, 30=WARN, 40=ERROR).')
360
379
  @click.option('--rpc-url', default='https://127.0.0.1:443', help='Cleep RPC url (ex https://ip|host:port')
361
- def watch(quiet, loglevel, rpc_url):
380
+ @click.option('--skip-catchup', is_flag=True, help='Do not syncall before watching (not recommended).')
381
+ def watch(quiet, loglevel, rpc_url, skip_catchup):
362
382
  """
363
- Start watchdog that monitors filesystem changes on Cleep sources
383
+ Start watchdog that monitors filesystem changes on Cleep sources.
384
+
385
+ Prefer managing the durable unit with watchsvc (survives Cleep restarts).
386
+ On start, syncs core + all modules once unless --skip-catchup is set.
364
387
  """
365
388
  if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
366
389
  # do not overwrite root logger level if configured to DEBUG (dev mode)
@@ -369,13 +392,51 @@ def watch(quiet, loglevel, rpc_url):
369
392
  logging.disable(logging.CRITICAL)
370
393
  else:
371
394
  logging.getLogger().setLevel(loglevel)
372
-
373
- w = CleepWatchdog(rpc_url)
395
+
396
+ w = CleepWatchdog(rpc_url, catch_up=not skip_catchup)
374
397
  res = w.watch()
375
398
 
376
399
  if not res:
377
400
  sys.exit(1)
378
401
 
402
+ @watchdog.command('watchsvc')
403
+ @click.argument(
404
+ 'action',
405
+ type=click.Choice(
406
+ ['install', 'uninstall', 'enable', 'disable', 'start', 'stop', 'restart', 'status']
407
+ ),
408
+ )
409
+ def watchsvc_cmd(action):
410
+ """
411
+ Manage the durable cleep-watch systemd unit.
412
+
413
+ ACTION is one of: install, uninstall, enable, disable, start, stop, restart, status.
414
+
415
+ The unit runs ``cleep-cli watch`` outside of cleep.service. Typical setup::
416
+
417
+ cleep-cli watchsvc install
418
+ cleep-cli watchsvc enable
419
+ """
420
+ svc = WatchService()
421
+ handlers = {
422
+ 'install': svc.install,
423
+ 'uninstall': svc.uninstall,
424
+ 'enable': svc.enable,
425
+ 'disable': svc.disable,
426
+ 'start': svc.start,
427
+ 'stop': svc.stop,
428
+ 'restart': svc.restart,
429
+ }
430
+ if action == 'status':
431
+ status = svc.status()
432
+ logging.info(status)
433
+ if status != 'active':
434
+ sys.exit(1)
435
+ return
436
+
437
+ if not handlers[action]():
438
+ sys.exit(1)
439
+
379
440
  @click.group()
380
441
  def test():
381
442
  pass
@@ -17,6 +17,7 @@ from cleepcli.git import Git
17
17
  from cleepcli.module import Module
18
18
  from cleepcli.file import File
19
19
  from cleepcli.watch import CleepWatchdog
20
+ from cleepcli.watchsvc import WatchService
20
21
  from cleepcli.test import Test, is_file_coverage_valid
21
22
  from cleepcli.package import Package
22
23
  from cleepcli.docs import Docs
@@ -108,6 +109,24 @@ def coresync():
108
109
  sys.exit(1)
109
110
  logging.info('Done')
110
111
 
112
+ @core.command()
113
+ def syncall():
114
+ """
115
+ Synchronize core and all modules from /root/cleep-dev to execution folders.
116
+
117
+ Same catch-up used when cleep-watch.service starts. Also disables frontend
118
+ static cache, mints a new asset bust, and reloads the UI so the device page
119
+ cannot keep stale JS.
120
+ """
121
+ from cleepcli.cleepapi import CleepApi
122
+
123
+ f = File()
124
+ if not f.sync_all():
125
+ sys.exit(1)
126
+ api = CleepApi(None)
127
+ api.ensure_frontend_nocache()
128
+ api.restart_frontend()
129
+
111
130
  @core.command()
112
131
  @click.option('--coverage', is_flag=True, help='Display coverage report.')
113
132
  @click.option('--output', is_flag=True, help='Display tests output.')
@@ -358,9 +377,13 @@ def watchdog():
358
377
  @click.option('--quiet', is_flag=True, help='Disable logging.')
359
378
  @click.option('--loglevel', default=logging.INFO, help='Logging level (10=DEBUG, 20=INFO, 30=WARN, 40=ERROR).')
360
379
  @click.option('--rpc-url', default='https://127.0.0.1:443', help='Cleep RPC url (ex https://ip|host:port')
361
- def watch(quiet, loglevel, rpc_url):
380
+ @click.option('--skip-catchup', is_flag=True, help='Do not syncall before watching (not recommended).')
381
+ def watch(quiet, loglevel, rpc_url, skip_catchup):
362
382
  """
363
- Start watchdog that monitors filesystem changes on Cleep sources
383
+ Start watchdog that monitors filesystem changes on Cleep sources.
384
+
385
+ Prefer managing the durable unit with watchsvc (survives Cleep restarts).
386
+ On start, syncs core + all modules once unless --skip-catchup is set.
364
387
  """
365
388
  if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
366
389
  # do not overwrite root logger level if configured to DEBUG (dev mode)
@@ -369,13 +392,51 @@ def watch(quiet, loglevel, rpc_url):
369
392
  logging.disable(logging.CRITICAL)
370
393
  else:
371
394
  logging.getLogger().setLevel(loglevel)
372
-
373
- w = CleepWatchdog(rpc_url)
395
+
396
+ w = CleepWatchdog(rpc_url, catch_up=not skip_catchup)
374
397
  res = w.watch()
375
398
 
376
399
  if not res:
377
400
  sys.exit(1)
378
401
 
402
+ @watchdog.command('watchsvc')
403
+ @click.argument(
404
+ 'action',
405
+ type=click.Choice(
406
+ ['install', 'uninstall', 'enable', 'disable', 'start', 'stop', 'restart', 'status']
407
+ ),
408
+ )
409
+ def watchsvc_cmd(action):
410
+ """
411
+ Manage the durable cleep-watch systemd unit.
412
+
413
+ ACTION is one of: install, uninstall, enable, disable, start, stop, restart, status.
414
+
415
+ The unit runs ``cleep-cli watch`` outside of cleep.service. Typical setup::
416
+
417
+ cleep-cli watchsvc install
418
+ cleep-cli watchsvc enable
419
+ """
420
+ svc = WatchService()
421
+ handlers = {
422
+ 'install': svc.install,
423
+ 'uninstall': svc.uninstall,
424
+ 'enable': svc.enable,
425
+ 'disable': svc.disable,
426
+ 'start': svc.start,
427
+ 'stop': svc.stop,
428
+ 'restart': svc.restart,
429
+ }
430
+ if action == 'status':
431
+ status = svc.status()
432
+ logging.info(status)
433
+ if status != 'active':
434
+ sys.exit(1)
435
+ return
436
+
437
+ if not handlers[action]():
438
+ sys.exit(1)
439
+
379
440
  @click.group()
380
441
  def test():
381
442
  pass
@@ -1497,6 +1497,50 @@ overgeneral-exceptions=Exception
1497
1497
  """
1498
1498
  return self._check_module_tree(module_name, "asset")
1499
1499
 
1500
+ def check_firmwares(self, module_name):
1501
+ """
1502
+ Validate optional ``firmwares/`` zip+sidecar ``.manifest`` pairs and list
1503
+ files for packaging (see ``cleep.libs.internals.firmware_manifest``).
1504
+
1505
+ Returns:
1506
+ dict: errors / warnings / files (same shape as check_scripts)
1507
+ """
1508
+ if not os.path.exists(os.path.join(config.MODULES_DST, module_name)):
1509
+ raise Exception('Module "%s" does not exist' % module_name)
1510
+
1511
+ firmwares_path = os.path.join(config.MODULES_SRC, module_name, "firmwares")
1512
+ out = {
1513
+ "errors": [],
1514
+ "warnings": [],
1515
+ "files": [],
1516
+ }
1517
+ if not os.path.isdir(firmwares_path):
1518
+ return out
1519
+
1520
+ try:
1521
+ from cleep.libs.internals.firmware_manifest import (
1522
+ FirmwareManifestError,
1523
+ scan_firmwares_dir,
1524
+ )
1525
+ except ImportError:
1526
+ out["errors"].append(
1527
+ "Unable to import cleep.libs.internals.firmware_manifest "
1528
+ "(is Cleep core available on PYTHONPATH?)"
1529
+ )
1530
+ return out
1531
+
1532
+ try:
1533
+ scan_firmwares_dir(firmwares_path)
1534
+ except FirmwareManifestError as error:
1535
+ out["errors"].append(str(error))
1536
+ return out
1537
+
1538
+ # Include valid pairs in the package file list
1539
+ tree = self._check_module_tree(module_name, "firmwares")
1540
+ out["files"] = tree["files"]
1541
+ out["warnings"] = tree["warnings"]
1542
+ return out
1543
+
1500
1544
  def check_tests(self, module_name):
1501
1545
  """
1502
1546
  Check test files
@@ -243,6 +243,32 @@ class CleepApi():
243
243
  return False
244
244
  return True
245
245
 
246
+ def ensure_frontend_nocache(self):
247
+ """
248
+ Disable RPC static cache and mint a fresh asset bust token.
249
+
250
+ Safe to call repeatedly; used after syncall / frontend watch batches so
251
+ the browser cannot keep stale core or module JS.
252
+ """
253
+ data = {
254
+ 'to': 'inventory',
255
+ 'command': 'bump_rpc_asset_cache',
256
+ 'params': {},
257
+ }
258
+ result = self.__post(self.command_url, data)
259
+ if not result or result[0] != 200:
260
+ # Older cores only expose set_rpc_cache_control
261
+ data = {
262
+ 'to': 'inventory',
263
+ 'command': 'set_rpc_cache_control',
264
+ 'params': {'cache_enabled': False},
265
+ }
266
+ result = self.__post(self.command_url, data)
267
+ if not result or result[0] != 200:
268
+ self.logger.debug('Unable to disable frontend static cache')
269
+ return False
270
+ return True
271
+
246
272
  def notify_sync_started(self):
247
273
  """
248
274
  Tell the developer UI a watch sync is starting.
@@ -60,6 +60,51 @@ rsync -av "%(REPO_DIR)s/medias/sounds" "%(MEDIA_DST)s/sounds/" --delete
60
60
 
61
61
  return True
62
62
 
63
+ def list_modules(self):
64
+ """
65
+ List module folder names under MODULES_SRC (skips hidden dirs).
66
+
67
+ Returns:
68
+ list: sorted module names
69
+ """
70
+ if not os.path.isdir(config.MODULES_SRC):
71
+ return []
72
+ names = []
73
+ try:
74
+ for name in os.listdir(config.MODULES_SRC):
75
+ if name.startswith("."):
76
+ continue
77
+ path = os.path.join(config.MODULES_SRC, name)
78
+ if os.path.isdir(path):
79
+ names.append(name)
80
+ except OSError as error:
81
+ self.logger.error("Unable to list modules: %s", error)
82
+ return []
83
+ return sorted(names)
84
+
85
+ def sync_all(self):
86
+ """
87
+ Synchronize core and every module from REPO_DIR to execution folders.
88
+
89
+ Used as watch catch-up on start and by the ``syncall`` CLI command so a
90
+ full resync is always one explicit action away.
91
+
92
+ Returns:
93
+ bool: True if every sync succeeded
94
+ """
95
+ self.logger.info("Synchronizing core sources...")
96
+ if not self.core_sync():
97
+ return False
98
+
99
+ modules = self.list_modules()
100
+ for module_name in modules:
101
+ self.logger.info('Synchronizing module "%s" sources...', module_name)
102
+ if not self.module_sync(module_name):
103
+ return False
104
+
105
+ self.logger.info("Sync all done (%d module(s))", len(modules))
106
+ return True
107
+
63
108
  def module_sync(self, module_name, backend=True, frontend=True, scripts=True):
64
109
  """
65
110
  Copy specified module content to valid execution location
@@ -516,6 +516,12 @@ sha256sum $DEB > $SHA256
516
516
  "Error in asset/. Fix it before packaging application: %s"
517
517
  % data_asset["errors"]
518
518
  )
519
+ data_firmwares = check.check_firmwares(module_name)
520
+ if len(data_firmwares["errors"]) > 0:
521
+ raise Exception(
522
+ "Error in firmwares/. Fix it before packaging application: %s"
523
+ % data_firmwares["errors"]
524
+ )
519
525
  data_tests = check.check_tests(module_name)
520
526
  if len(data_tests["errors"]) > 0:
521
527
  raise Exception(
@@ -629,6 +635,10 @@ sha256sum $DEB > $SHA256
629
635
  for a_file in data_asset["files"]:
630
636
  archive.write(a_file["fullpath"], a_file["path"])
631
637
 
638
+ # add firmwares/ (zip + sidecar .manifest; installer copies to APP_FIRMWARE)
639
+ for a_file in data_firmwares["files"]:
640
+ archive.write(a_file["fullpath"], a_file["path"])
641
+
632
642
  # add module.json
633
643
  archive.write(module_json, "module.json")
634
644
 
@@ -0,0 +1,18 @@
1
+ [Unit]
2
+ Description=Cleep development source watcher (cleep-cli watch)
3
+ Documentation=https://github.com/CleepDevice/cleep-cli
4
+ # Independent from cleep.service so backend restarts do not kill the watcher
5
+ # mid-sync (Desktop writes would otherwise be missed until the next edit).
6
+ After=network-online.target
7
+ Wants=network-online.target
8
+
9
+ [Service]
10
+ Type=simple
11
+ ExecStart=/usr/local/bin/cleep-cli watch --loglevel=20
12
+ Restart=always
13
+ RestartSec=3
14
+ # Allow a long catch-up sync on start before the main loop
15
+ TimeoutStartSec=180
16
+
17
+ [Install]
18
+ WantedBy=multi-user.target
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
- VERSION = "1.46.0"
4
+ VERSION = "1.48.1"
@@ -18,6 +18,7 @@ from watchdog.events import (
18
18
  import watchdog.events as events
19
19
  from .cleepapi import CleepApi
20
20
  from .file import File
21
+ from .watchsvc import WATCH_SERVICE
21
22
 
22
23
  _WATCHED_EVENT_TYPES = (
23
24
  EVENT_TYPE_CREATED,
@@ -164,6 +165,9 @@ class ActionsExecutor(Thread):
164
165
  self.logger.error("Sync failed, Cleep was not restarted")
165
166
  self.cleep.notify_sync_failed()
166
167
  return
168
+ # Always mint a fresh asset bust before UI reload so Chrome/Electron
169
+ # cannot reuse ?cleepversion / max-age=3600 core scripts.
170
+ self.cleep.ensure_frontend_nocache()
167
171
  if batch.backend:
168
172
  # Backend restart drops the UI connection. The page reloads on its own
169
173
  # once Cleep answers again, with the frontend files already synced.
@@ -206,6 +210,11 @@ class CleepHandler(FileSystemEventHandler):
206
210
  REJECTED_FILENAMES = [
207
211
  "4913", # vim furtive temp file to check user permissions
208
212
  ".gitignore",
213
+ # Module-root docs are not synced by module_sync; ignore to avoid noise.
214
+ "CHANGELOG.md",
215
+ "README.md",
216
+ "LICENSE",
217
+ "COPYING",
209
218
  ]
210
219
  REJECTED_EXTENSIONS = [
211
220
  ".swp", # vim
@@ -521,22 +530,24 @@ class CleepWatchdog:
521
530
  Cleep core and module watchdog
522
531
  """
523
532
 
524
- def __init__(self, rpc_url):
533
+ def __init__(self, rpc_url, catch_up=True):
525
534
  """
526
535
  Constructor
527
536
 
528
537
  Args:
529
538
  rpc_url (str): Cleep RPC url
539
+ catch_up (bool): when True, sync core + all modules before watching
540
+ so files written while the watcher was down are not missed
530
541
  """
531
542
  self.logger = logging.getLogger(self.__class__.__name__)
532
543
  self.actions_executor = ActionsExecutor(rpc_url)
544
+ self.catch_up = catch_up
533
545
 
534
546
  def watch(self):
535
547
  """
536
548
  Start watchdog that monitors filesystem changes on Cleep sources
537
549
  """
538
550
  path = "%s/" % config.REPO_DIR
539
- self.actions_executor.start()
540
551
 
541
552
  if not os.path.exists(path):
542
553
  self.logger.error(
@@ -544,6 +555,23 @@ class CleepWatchdog:
544
555
  )
545
556
  return False
546
557
 
558
+ # Catch-up before inotify: Desktop (or editors) may have written while
559
+ # this process was stopped (cleep restart, create-app pause, boot).
560
+ if self.catch_up:
561
+ self.logger.info("Catch-up sync before watching...")
562
+ if not File().sync_all():
563
+ self.logger.error(
564
+ "Catch-up sync failed; fix errors then retry "
565
+ "(cleep-cli syncall / systemctl restart %s)",
566
+ WATCH_SERVICE,
567
+ )
568
+ return False
569
+ # Files may have landed while Cleep/UI were up with an old bust token.
570
+ self.actions_executor.cleep.ensure_frontend_nocache()
571
+ self.actions_executor.cleep.restart_frontend()
572
+
573
+ self.actions_executor.start()
574
+
547
575
  event_handler = CleepHandler(self.actions_executor)
548
576
  observer = Observer()
549
577
  event_handler.observer = observer
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Manage the durable cleep-watch systemd unit.
6
+
7
+ The unit runs ``cleep-cli watch`` outside of cleep.service so backend restarts
8
+ do not kill the watcher mid-sync.
9
+ """
10
+
11
+ import logging
12
+ import os
13
+ import shutil
14
+
15
+ from .console import Console
16
+
17
+ WATCH_SERVICE = "cleep-watch.service"
18
+ WATCH_UNIT_DST = "/lib/systemd/system/%s" % WATCH_SERVICE
19
+
20
+
21
+ def bundled_unit_path():
22
+ """
23
+ Path to the unit file shipped inside the cleepcli package.
24
+
25
+ Returns:
26
+ str: absolute path to cleep-watch.service
27
+ """
28
+ return os.path.join(os.path.dirname(__file__), "systemd", WATCH_SERVICE)
29
+
30
+
31
+ class WatchService:
32
+ """
33
+ Install / enable / control cleep-watch.service via systemctl.
34
+ """
35
+
36
+ def __init__(self):
37
+ self.logger = logging.getLogger(self.__class__.__name__)
38
+ self.console = Console()
39
+
40
+ def _systemctl(self, *args, timeout=60.0):
41
+ cmd = "/bin/systemctl " + " ".join(args)
42
+ self.logger.debug("systemctl cmd: %s", cmd)
43
+ return self.console.command(cmd, timeout)
44
+
45
+ def _ok(self, resp):
46
+ return not resp.get("killed") and resp.get("returncode", 1) == 0
47
+
48
+ def _daemon_reload(self):
49
+ return self._ok(self._systemctl("daemon-reload"))
50
+
51
+ def install(self):
52
+ """
53
+ Copy the bundled unit to systemd and reload the daemon.
54
+
55
+ Returns:
56
+ bool: True on success
57
+ """
58
+ src = bundled_unit_path()
59
+ if not os.path.isfile(src):
60
+ self.logger.error("Bundled unit not found: %s", src)
61
+ return False
62
+ try:
63
+ os.makedirs(os.path.dirname(WATCH_UNIT_DST), exist_ok=True)
64
+ shutil.copyfile(src, WATCH_UNIT_DST)
65
+ except OSError as error:
66
+ self.logger.error("Unable to install %s: %s", WATCH_UNIT_DST, error)
67
+ return False
68
+ if not self._daemon_reload():
69
+ self.logger.error("systemctl daemon-reload failed after install")
70
+ return False
71
+ self.logger.info("Installed %s", WATCH_UNIT_DST)
72
+ return True
73
+
74
+ def uninstall(self):
75
+ """
76
+ Disable the unit, remove the unit file, reload systemd.
77
+
78
+ Returns:
79
+ bool: True on success (missing unit is OK)
80
+ """
81
+ self.disable()
82
+ try:
83
+ if os.path.isfile(WATCH_UNIT_DST):
84
+ os.remove(WATCH_UNIT_DST)
85
+ self.logger.info("Removed %s", WATCH_UNIT_DST)
86
+ except OSError as error:
87
+ self.logger.error("Unable to remove %s: %s", WATCH_UNIT_DST, error)
88
+ return False
89
+ if not self._daemon_reload():
90
+ self.logger.error("systemctl daemon-reload failed after uninstall")
91
+ return False
92
+ return True
93
+
94
+ def enable(self):
95
+ """
96
+ Enable and start the unit (installs the unit file first if missing).
97
+
98
+ Returns:
99
+ bool: True on success
100
+ """
101
+ if not os.path.isfile(WATCH_UNIT_DST):
102
+ if not self.install():
103
+ return False
104
+ resp = self._systemctl("enable", "--now", WATCH_SERVICE)
105
+ if not self._ok(resp):
106
+ self.logger.error(
107
+ "Unable to enable %s: %s %s",
108
+ WATCH_SERVICE,
109
+ resp.get("stdout"),
110
+ resp.get("stderr"),
111
+ )
112
+ return False
113
+ return self.is_active()
114
+
115
+ def disable(self):
116
+ """
117
+ Disable and stop the unit.
118
+
119
+ Returns:
120
+ bool: True on success (or if unit was not installed)
121
+ """
122
+ if not os.path.isfile(WATCH_UNIT_DST):
123
+ return True
124
+ resp = self._systemctl("disable", "--now", WATCH_SERVICE)
125
+ if not self._ok(resp):
126
+ # Unit may already be inactive/disabled
127
+ self.logger.debug(
128
+ "disable --now %s: %s %s",
129
+ WATCH_SERVICE,
130
+ resp.get("stdout"),
131
+ resp.get("stderr"),
132
+ )
133
+ return True
134
+
135
+ def start(self):
136
+ """
137
+ Start the unit (catch-up sync runs on ExecStart).
138
+
139
+ Returns:
140
+ bool: True on success
141
+ """
142
+ if not os.path.isfile(WATCH_UNIT_DST):
143
+ if not self.install():
144
+ return False
145
+ resp = self._systemctl("start", WATCH_SERVICE)
146
+ if not self._ok(resp):
147
+ self.logger.error(
148
+ "Unable to start %s: %s %s",
149
+ WATCH_SERVICE,
150
+ resp.get("stdout"),
151
+ resp.get("stderr"),
152
+ )
153
+ return False
154
+ return True
155
+
156
+ def stop(self):
157
+ """
158
+ Stop the unit.
159
+
160
+ Returns:
161
+ bool: True on success
162
+ """
163
+ if not os.path.isfile(WATCH_UNIT_DST):
164
+ return True
165
+ resp = self._systemctl("stop", WATCH_SERVICE)
166
+ if not self._ok(resp):
167
+ self.logger.error(
168
+ "Unable to stop %s: %s %s",
169
+ WATCH_SERVICE,
170
+ resp.get("stdout"),
171
+ resp.get("stderr"),
172
+ )
173
+ return False
174
+ return True
175
+
176
+ def restart(self):
177
+ """
178
+ Restart the unit (re-runs catch-up sync on start).
179
+
180
+ Returns:
181
+ bool: True on success
182
+ """
183
+ if not os.path.isfile(WATCH_UNIT_DST):
184
+ return self.enable()
185
+ resp = self._systemctl("restart", WATCH_SERVICE, timeout=180.0)
186
+ if not self._ok(resp):
187
+ self.logger.error(
188
+ "Unable to restart %s: %s %s",
189
+ WATCH_SERVICE,
190
+ resp.get("stdout"),
191
+ resp.get("stderr"),
192
+ )
193
+ return False
194
+ return True
195
+
196
+ def is_active(self):
197
+ """
198
+ Return True if the unit is active.
199
+ """
200
+ resp = self._systemctl("is-active", WATCH_SERVICE, timeout=5.0)
201
+ active = "".join(resp.get("stdout") or []).strip()
202
+ return active == "active"
203
+
204
+ def status(self):
205
+ """
206
+ Print / return a short status string for the unit.
207
+
208
+ Returns:
209
+ str: systemctl is-active output (or "not-installed")
210
+ """
211
+ if not os.path.isfile(WATCH_UNIT_DST):
212
+ return "not-installed"
213
+ resp = self._systemctl("is-active", WATCH_SERVICE, timeout=5.0)
214
+ return "".join(resp.get("stdout") or []).strip() or "unknown"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cleepcli
3
- Version: 1.46.0
3
+ Version: 1.48.1
4
4
  Summary: Cleep-cli helps developers to build great Cleep applications from command line.
5
5
  Home-page: https://github.com/CleepDevice/cleep-cli
6
6
  Author: Tanguy Bonneau
@@ -17,11 +17,11 @@ Requires-Dist: sphinx-rtd-theme>=1.0.0
17
17
  Requires-Dist: CProfileV>=1.0.7
18
18
  Requires-Dist: PyGithub==1.54.1
19
19
  Requires-Dist: pylint<3.0.0,>=2.11.1
20
- Requires-Dist: semver==3.0.0
21
- Requires-Dist: coverage==7.2.7
22
- Requires-Dist: passlib==1.7.4
23
- Requires-Dist: requests==2.31.0
24
- Requires-Dist: psutil==5.9.4
20
+ Requires-Dist: semver<4.0.0,>=3.0.0
21
+ Requires-Dist: coverage<8.0.0,>=7.2.7
22
+ Requires-Dist: passlib<2.0.0,>=1.7.4
23
+ Requires-Dist: requests<3.0.0,>=2.32.5
24
+ Requires-Dist: psutil<8.0.0,>=5.9.4
25
25
  Dynamic: author
26
26
  Dynamic: author-email
27
27
  Dynamic: description
@@ -61,7 +61,9 @@ This utility helps developers to build Cleep applications providing some useful
61
61
  - `modcheckscripts` to check app scripts files
62
62
  - `modcheckcode` to check backend code quality (run pylint)
63
63
  - Watch commands:
64
- - `watch` to watch for repository filesystem changes and automatically update files on execution folders. It also restart backend or/and frontend according to changes.
64
+ - `watch` to watch for repository filesystem changes and automatically update files on execution folders. It also restart backend or/and frontend according to changes (catch-up sync on start).
65
+ - `watchsvc` to install/enable/disable/restart the durable `cleep-watch` systemd unit (runs `watch` outside `cleep.service`).
66
+ - `syncall` to force a one-shot sync of core + all modules, disable static cache, and reload the UI.
65
67
  - Misc commands:
66
68
  - `version` to get cleep-cli version
67
69
 
@@ -85,9 +87,23 @@ Execute this command to get help on this cli:
85
87
 
86
88
  ## Watch usage
87
89
 
88
- Launch `watch` cli command to monitor changes on "/root/cleep-dev" directory after getting official Cleep repository using `coreget` command.
90
+ Preferred: let the CLI own the durable systemd unit:
89
91
 
90
- 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.
92
+ ```
93
+ cleep-cli watchsvc install
94
+ cleep-cli watchsvc enable
95
+ ```
96
+
97
+ The unit runs `cleep-cli watch`, survives `systemctl restart cleep`, and on each start performs a catch-up `syncall` so files written while the watcher was down are applied.
98
+
99
+ Manual one-shot resync (when the UI looks stale) — `syncall` also mints a fresh asset bust and reloads the device UI:
100
+
101
+ ```
102
+ cleep-cli syncall
103
+ cleep-cli watchsvc restart # catch-up again + resume watch
104
+ ```
105
+
106
+ You can still run `cleep-cli watch` in a terminal for debugging. Push sources to `/root/cleep-dev` (CleepDesktop developer sync or SFTP); the watcher copies them into the execution folders and restarts backend/frontend as needed.
91
107
 
92
108
  ### Paths
93
109
 
@@ -22,15 +22,19 @@ cleepcli/test.py
22
22
  cleepcli/tools.py
23
23
  cleepcli/version.py
24
24
  cleepcli/watch.py
25
+ cleepcli/watchsvc.py
25
26
  cleepcli.egg-info/PKG-INFO
26
27
  cleepcli.egg-info/SOURCES.txt
27
28
  cleepcli.egg-info/dependency_links.txt
28
29
  cleepcli.egg-info/requires.txt
29
30
  cleepcli.egg-info/top_level.txt
31
+ cleepcli/systemd/cleep-watch.service
30
32
  tests/test_check.py
31
33
  tests/test_config.py
32
34
  tests/test_console.py
33
35
  tests/test_coverage.py
34
36
  tests/test_docs_breaking.py
37
+ tests/test_file_sync_all.py
35
38
  tests/test_module_scaffold.py
36
- tests/test_watch.py
39
+ tests/test_watch.py
40
+ tests/test_watchsvc.py
@@ -5,8 +5,8 @@ sphinx-rtd-theme>=1.0.0
5
5
  CProfileV>=1.0.7
6
6
  PyGithub==1.54.1
7
7
  pylint<3.0.0,>=2.11.1
8
- semver==3.0.0
9
- coverage==7.2.7
10
- passlib==1.7.4
11
- requests==2.31.0
12
- psutil==5.9.4
8
+ semver<4.0.0,>=3.0.0
9
+ coverage<8.0.0,>=7.2.7
10
+ passlib<2.0.0,>=1.7.4
11
+ requests<3.0.0,>=2.32.5
12
+ psutil<8.0.0,>=5.9.4
@@ -0,0 +1,18 @@
1
+ # CLI-only
2
+ Click>=8.0,<9.0
3
+ watchdog>=3.0.0,<4.0.0
4
+ Sphinx>=5.0.0,<6.0.0
5
+ sphinx-rtd-theme>=1.0.0
6
+ CProfileV>=1.0.7
7
+ # Force version due to pynacl package introduced in 1.55 that makes 15-20 mins to build on raspi
8
+ # This package seems useful only for github actions we are not using.
9
+ PyGithub==1.54.1
10
+ pylint>=2.11.1,<3.0.0
11
+ semver>=3.0.0,<4.0.0
12
+
13
+ # Shared with the Cleep device env — permissive ranges only.
14
+ # Exact pins are owned by Cleep (requests, passlib, coverage) or cleepapp-system (psutil).
15
+ coverage>=7.2.7,<8.0.0
16
+ passlib>=1.7.4,<2.0.0
17
+ requests>=2.32.5,<3.0.0
18
+ psutil>=5.9.4,<8.0.0
@@ -39,6 +39,7 @@ setup(
39
39
  maintainer_email="tanguy.bonneau@gmail.com",
40
40
  url="https://github.com/CleepDevice/cleep-cli",
41
41
  packages=["cleepcli"],
42
+ package_data={"cleepcli": ["systemd/cleep-watch.service"]},
42
43
  include_package_data=True,
43
44
  install_requires=get_requirements(),
44
45
  python_requires=">=3.7",
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ import shutil
6
+ import tempfile
7
+
8
+ from unittest.mock import Mock, patch
9
+
10
+ from cleepcli import config
11
+ from cleepcli.file import File
12
+
13
+
14
+ def test_list_modules_sorted():
15
+ repo = tempfile.mkdtemp()
16
+ try:
17
+ modules = os.path.join(repo, "modules")
18
+ os.makedirs(os.path.join(modules, "zeta"))
19
+ os.makedirs(os.path.join(modules, "alpha"))
20
+ os.makedirs(os.path.join(modules, ".hidden"))
21
+ open(os.path.join(modules, "file.txt"), "w", encoding="utf-8").close()
22
+
23
+ with patch.object(config, "MODULES_SRC", modules):
24
+ assert File().list_modules() == ["alpha", "zeta"]
25
+ finally:
26
+ shutil.rmtree(repo)
27
+
28
+
29
+ def test_sync_all_runs_core_then_modules():
30
+ f = File()
31
+ f.core_sync = Mock(return_value=True)
32
+ f.module_sync = Mock(return_value=True)
33
+ f.list_modules = Mock(return_value=["audio", "developer"])
34
+
35
+ assert f.sync_all() is True
36
+ f.core_sync.assert_called_once_with()
37
+ assert f.module_sync.call_args_list[0].args[0] == "audio"
38
+ assert f.module_sync.call_args_list[1].args[0] == "developer"
39
+
40
+
41
+ def test_sync_all_stops_on_core_failure():
42
+ f = File()
43
+ f.core_sync = Mock(return_value=False)
44
+ f.module_sync = Mock(return_value=True)
45
+ f.list_modules = Mock(return_value=["audio"])
46
+
47
+ assert f.sync_all() is False
48
+ f.module_sync.assert_not_called()
49
+
50
+
51
+ def test_watch_catch_up_calls_sync_all():
52
+ from cleepcli.watch import CleepWatchdog
53
+
54
+ with patch("cleepcli.watch.File") as file_cls, patch(
55
+ "cleepcli.watch.os.path.exists", return_value=True
56
+ ), patch("cleepcli.watch.Observer"), patch(
57
+ "cleepcli.watch.CleepHandler"
58
+ ) as handler_cls, patch(
59
+ "cleepcli.watch.ActionsExecutor"
60
+ ) as executor_cls, patch(
61
+ "cleepcli.watch.time.sleep", side_effect=KeyboardInterrupt
62
+ ):
63
+ file_cls.return_value.sync_all.return_value = True
64
+ handler = handler_cls.return_value
65
+ handler._watched = {"/tmp"}
66
+ handler.schedule_all = Mock()
67
+ executor = executor_cls.return_value
68
+ executor.cleep.ensure_frontend_nocache = Mock(return_value=True)
69
+ executor.cleep.restart_frontend = Mock(return_value=True)
70
+
71
+ watchdog = CleepWatchdog("https://127.0.0.1:443", catch_up=True)
72
+ assert watchdog.watch() is True
73
+ file_cls.return_value.sync_all.assert_called_once_with()
74
+ executor.cleep.ensure_frontend_nocache.assert_called_once_with()
75
+ executor.cleep.restart_frontend.assert_called_once_with()
76
+
77
+
78
+ def test_watch_skip_catch_up():
79
+ from cleepcli.watch import CleepWatchdog
80
+
81
+ with patch("cleepcli.watch.File") as file_cls, patch(
82
+ "cleepcli.watch.os.path.exists", return_value=True
83
+ ), patch("cleepcli.watch.Observer"), patch(
84
+ "cleepcli.watch.CleepHandler"
85
+ ) as handler_cls, patch(
86
+ "cleepcli.watch.ActionsExecutor"
87
+ ) as executor_cls, patch(
88
+ "cleepcli.watch.time.sleep", side_effect=KeyboardInterrupt
89
+ ):
90
+ handler = handler_cls.return_value
91
+ handler._watched = {"/tmp"}
92
+ handler.schedule_all = Mock()
93
+ executor = executor_cls.return_value
94
+ executor.cleep.ensure_frontend_nocache = Mock()
95
+ executor.cleep.restart_frontend = Mock()
96
+
97
+ watchdog = CleepWatchdog("https://127.0.0.1:443", catch_up=False)
98
+ assert watchdog.watch() is True
99
+ file_cls.return_value.sync_all.assert_not_called()
100
+ executor.cleep.ensure_frontend_nocache.assert_not_called()
101
+ executor.cleep.restart_frontend.assert_not_called()
@@ -131,6 +131,13 @@ def test_module_frontend_is_queued(handler):
131
131
  )
132
132
 
133
133
 
134
+ def test_module_changelog_is_ignored(handler):
135
+ h, executor = handler
136
+ path = os.path.join(config.MODULES_SRC, "audio", "CHANGELOG.md")
137
+ h.on_any_event(FileModifiedEvent(path))
138
+ executor.note_change.assert_not_called()
139
+
140
+
134
141
  def test_created_git_directory_is_not_watched(handler):
135
142
  h, executor = handler
136
143
  h.observer = Mock()
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ from unittest.mock import Mock, patch
6
+
7
+ from cleepcli.watchsvc import WatchService, WATCH_UNIT_DST, bundled_unit_path
8
+
9
+
10
+ def test_bundled_unit_path_exists():
11
+ assert os.path.isfile(bundled_unit_path())
12
+
13
+
14
+ def test_install_copies_unit_and_reloads():
15
+ svc = WatchService()
16
+ svc.console = Mock()
17
+ svc.console.command.return_value = {"returncode": 0, "stdout": "", "stderr": ""}
18
+
19
+ with patch("cleepcli.watchsvc.shutil.copyfile") as copyfile, patch(
20
+ "cleepcli.watchsvc.os.makedirs"
21
+ ), patch("cleepcli.watchsvc.os.path.isfile", return_value=True):
22
+ assert svc.install() is True
23
+ copyfile.assert_called_once()
24
+ assert copyfile.call_args.args[1] == WATCH_UNIT_DST
25
+ svc.console.command.assert_called_with(
26
+ "/bin/systemctl daemon-reload", 60.0
27
+ )
28
+
29
+
30
+ def test_enable_installs_when_missing():
31
+ svc = WatchService()
32
+ svc.install = Mock(return_value=True)
33
+ svc.is_active = Mock(return_value=True)
34
+ svc.console = Mock()
35
+ svc.console.command.return_value = {"returncode": 0, "stdout": "", "stderr": ""}
36
+
37
+ with patch("cleepcli.watchsvc.os.path.isfile", return_value=False):
38
+ assert svc.enable() is True
39
+ svc.install.assert_called_once_with()
40
+
41
+
42
+ def test_status_not_installed():
43
+ svc = WatchService()
44
+ with patch("cleepcli.watchsvc.os.path.isfile", return_value=False):
45
+ assert svc.status() == "not-installed"
46
+
47
+
48
+ def test_status_active():
49
+ svc = WatchService()
50
+ svc.console = Mock()
51
+ svc.console.command.return_value = {
52
+ "returncode": 0,
53
+ "stdout": ["active"],
54
+ "stderr": "",
55
+ }
56
+ with patch("cleepcli.watchsvc.os.path.isfile", return_value=True):
57
+ assert svc.status() == "active"
@@ -1,15 +0,0 @@
1
- Click>=8.0,<9.0
2
- watchdog>=3.0.0,<4.0.0
3
- Sphinx>=5.0.0,<6.0.0
4
- sphinx-rtd-theme>=1.0.0
5
- CProfileV>=1.0.7
6
- # Force version due to pynacl package introduced in 1.55 that makes 15-20 mins to build on raspi
7
- # This package seems useful only for github actions we are not using.
8
- PyGithub==1.54.1
9
- pylint>=2.11.1,<3.0.0
10
- semver==3.0.0
11
- # Make sure deps below match with cleep ones
12
- coverage==7.2.7
13
- passlib==1.7.4
14
- requests==2.31.0
15
- psutil==5.9.4
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes