dron 0.1.20241008__py3-none-any.whl → 0.2.20251011__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dron/systemd.py CHANGED
@@ -5,14 +5,14 @@ import os
5
5
  import re
6
6
  import shlex
7
7
  import shutil
8
- from datetime import datetime, timedelta, timezone
8
+ from collections.abc import Iterator, Sequence
9
+ from datetime import UTC, datetime, timedelta
9
10
  from functools import lru_cache
10
11
  from itertools import groupby
11
12
  from pathlib import Path
12
13
  from subprocess import PIPE, Popen, run
13
14
  from tempfile import TemporaryDirectory
14
- from typing import Any, Iterator, Sequence
15
-
15
+ from typing import Any
16
16
  from zoneinfo import ZoneInfo
17
17
 
18
18
  from .api import (
@@ -26,9 +26,9 @@ from .common import (
26
26
  MonitorEntry,
27
27
  MonitorParams,
28
28
  State,
29
+ SystemdUnitState,
29
30
  TimerSpec,
30
31
  Unit,
31
- UnitState,
32
32
  datetime_aware,
33
33
  escape,
34
34
  is_managed,
@@ -81,11 +81,11 @@ WantedBy=timers.target
81
81
  # TODO add Restart=always and RestartSec?
82
82
  # TODO allow to pass extra args
83
83
  def service(
84
- *,
85
- unit_name: str,
86
- command: Command,
87
- on_failure: Sequence[OnFailureAction],
88
- **kwargs: str,
84
+ *,
85
+ unit_name: str,
86
+ command: Command,
87
+ on_failure: Sequence[OnFailureAction],
88
+ **kwargs: str,
89
89
  ) -> str:
90
90
  # TODO not sure if something else needs to be escaped for ExecStart??
91
91
  # todo systemd-escape? but only can be used for names
@@ -96,14 +96,11 @@ def service(
96
96
  cmd = escape(command)
97
97
 
98
98
  exec_stop_posts = [
99
- f"ExecStopPost=/bin/sh -c 'if [ $$EXIT_STATUS != 0 ]; then {action}; fi'"
100
- for action in on_failure
99
+ f"ExecStopPost=/bin/sh -c 'if [ $$EXIT_STATUS != 0 ]; then {action}; fi'" for action in on_failure
101
100
  ]
102
101
 
103
102
  sections: dict[str, list[str]] = {}
104
- sections['[Unit]'] = [f'''
105
- Description=Service for {unit_name} {MANAGED_MARKER}
106
- '''.strip()]
103
+ sections['[Unit]'] = [f'Description=Service for {unit_name} {MANAGED_MARKER}']
107
104
 
108
105
  sections['[Service]'] = [
109
106
  f'ExecStart={cmd}',
@@ -188,40 +185,48 @@ def test_verify_systemd() -> None:
188
185
  skip_if_no_systemd()
189
186
  from .dron import verify_unit
190
187
 
191
- def fails(body: str) -> None:
188
+ def FAILS(body: str) -> None:
192
189
  import pytest
190
+
193
191
  with pytest.raises(Exception):
194
192
  verify_unit(unit_name='whatever.service', body=body)
195
193
 
196
- def ok(body: str) -> None:
194
+ def OK(body: str) -> None:
197
195
  verify_unit(unit_name='ok.service', body=body)
198
196
 
199
- ok(body='''
197
+ OK(
198
+ body='''
200
199
  [Service]
201
200
  ExecStart=/bin/echo 123
202
- ''')
201
+ '''
202
+ )
203
203
 
204
204
  from .api import notify
205
+
205
206
  on_failure = (
206
207
  notify.email('test@gmail.com'),
207
208
  notify.desktop_notification,
208
209
  )
209
- ok(body=service(unit_name='alala', command='/bin/echo 123', on_failure=on_failure))
210
+ OK(body=service(unit_name='alala', command='/bin/echo 123', on_failure=on_failure))
210
211
 
211
212
  # garbage
212
- fails(body='fewfewf')
213
+ FAILS(body='fewfewf')
213
214
 
214
215
  # no execstart
215
- fails(body='''
216
+ FAILS(
217
+ body='''
216
218
  [Service]
217
219
  StandardOutput=journal
218
- ''')
220
+ '''
221
+ )
219
222
 
220
- fails(body='''
223
+ FAILS(
224
+ body='''
221
225
  [Service]
222
226
  ExecStart=yes
223
227
  StandardOutput=baaad
224
- ''')
228
+ '''
229
+ )
225
230
 
226
231
 
227
232
  def _sd(s: str) -> str:
@@ -235,6 +240,7 @@ class BusManager:
235
240
  Interface,
236
241
  SessionBus,
237
242
  )
243
+
238
244
  self.Interface = Interface # meh
239
245
 
240
246
  self.bus = SessionBus() # note: SystemBus is for system-wide services
@@ -261,7 +267,7 @@ def systemd_state(*, with_body: bool) -> State:
261
267
  states = bus.manager.ListUnits() # ok nice, it's basically instant
262
268
 
263
269
  for state in states:
264
- name = state[0]
270
+ name = state[0]
265
271
  descr = state[1]
266
272
  if not is_managed(descr):
267
273
  continue
@@ -271,17 +277,19 @@ def systemd_state(*, with_body: bool) -> State:
271
277
 
272
278
  # useful for debugging, can also use .Service if it's not a timer
273
279
  # all_properties = props.GetAll(_sd('.Unit'))
280
+ # however GetAll seems slower than gettind individual properties
274
281
 
275
282
  # stale = int(bus.prop(props, '.Unit', 'NeedDaemonReload')) == 1
283
+ # TODO do we actually need to resolve?
276
284
  unit_file = Path(str(bus.prop(props, '.Unit', 'FragmentPath'))).resolve()
277
285
  body = unit_file.read_text() if with_body else None
278
286
  cmdline: Sequence[str] | None
279
- if '.timer' in name: # meh
287
+ if '.timer' in name: # meh
280
288
  cmdline = None
281
289
  else:
282
290
  cmdline = BusManager.exec_start(props)
283
291
 
284
- yield UnitState(unit_file=unit_file, body=body, cmdline=cmdline)
292
+ yield SystemdUnitState(unit_file=unit_file, body=body, cmdline=cmdline, dbus_properties=props)
285
293
 
286
294
 
287
295
  def test_managed_units() -> None:
@@ -297,27 +305,28 @@ def test_managed_units() -> None:
297
305
  # dbus.exceptions.DBusException: org.freedesktop.DBus.Error.BadAddress: Address does not contain a colon
298
306
  # todo maybe don't need it anymore with 20.04 circleci?
299
307
  if 'CI' not in os.environ:
300
- cmd_monitor(MonitorParams(with_success_rate=True, with_command=True))
308
+ cmd_monitor.callback(n=1, once=True, command=True, rate=True) # type: ignore[misc]
301
309
 
302
310
 
303
311
  def skip_if_no_systemd() -> None:
304
312
  import pytest
313
+
305
314
  reason = _is_missing_systemd()
306
315
  if reason is not None:
307
316
  pytest.skip(f'No systemd: {reason}')
308
317
 
309
318
 
310
- _UTCMAX = datetime.max.replace(tzinfo=timezone.utc)
319
+ _UTCMAX = datetime.max.replace(tzinfo=UTC)
311
320
 
312
321
 
313
322
  class MonitorHelper:
314
323
  def from_usec(self, usec) -> datetime_aware:
315
324
  u = int(usec)
316
- if u == 2 ** 64 - 1: # apparently systemd uses max uint64
325
+ if u == 2**64 - 1: # apparently systemd uses max uint64
317
326
  # happens if the job is running ATM?
318
327
  return _UTCMAX
319
328
  else:
320
- return datetime.fromtimestamp(u / 10 ** 6, tz=timezone.utc)
329
+ return datetime.fromtimestamp(u / 10**6, tz=UTC)
321
330
 
322
331
  @property
323
332
  @lru_cache # noqa: B019
@@ -326,30 +335,65 @@ class MonitorHelper:
326
335
  # it's a required dependency, but still might fail in some weird environments?
327
336
  # e.g. if zoneinfo information isn't available
328
337
  from tzlocal import get_localzone
338
+
329
339
  return get_localzone()
330
- except Exception as e:
340
+ except Exception:
331
341
  logger.error("Couldn't determine local timezone! Falling back to UTC")
332
342
  return ZoneInfo('UTC')
333
343
 
334
344
 
335
- def get_entries_for_monitor(managed: State, *, params: MonitorParams) -> list[MonitorEntry]:
336
- # TODO reorder timers and services so timers go before?
337
- sd = lambda s: f'org.freedesktop.systemd1{s}'
345
+ # TODO maybe format seconds prettier. dunno
346
+ def _fmt_delta(d: timedelta) -> str:
347
+ # format to reduce constant countdown...
348
+ ad = abs(d)
349
+ # get rid of microseconds
350
+ ad = ad - timedelta(microseconds=ad.microseconds)
351
+
352
+ day = timedelta(days=1)
353
+ hour = timedelta(hours=1)
354
+ minute = timedelta(minutes=1)
355
+ gt = False
356
+ if ad > day:
357
+ full_days = ad // day
358
+ hours = (ad % day) // hour
359
+ ads = f'{full_days}d {hours}h'
360
+ gt = True
361
+ elif ad > minute:
362
+ full_mins = ad // minute
363
+ ad = timedelta(minutes=full_mins)
364
+ ads = str(ad)
365
+ gt = True
366
+ else:
367
+ # show exact
368
+ ads = str(ad)
369
+ if len(ads) == 7:
370
+ ads = '0' + ads # meh. fix missing leading zero in hours..
371
+ ads = ('>' if gt else '') + ads
372
+ return ads
373
+
338
374
 
375
+ def get_entries_for_monitor(managed: State, *, params: MonitorParams) -> list[MonitorEntry]:
339
376
  mon = MonitorHelper()
340
377
 
341
- UTCNOW = datetime.now(tz=timezone.utc)
378
+ UTCNOW = datetime.now(tz=UTC)
342
379
 
343
380
  bus = BusManager()
344
381
 
345
382
  entries: list[MonitorEntry] = []
346
- names = sorted(s.unit_file.name for s in managed)
347
- uname = lambda full: full.split('.')[0]
348
- for k, _gr in groupby(names, key=uname):
349
- gr = list(_gr)
383
+
384
+ # sort so that neigbouring unit.service/unit.timer go one after another for grouping
385
+ sort_key = lambda unit: unit.unit_file.name
386
+ # for grouping, group by common stem of timers and services
387
+ stem_name = lambda unit: sort_key(unit).split('.')[0]
388
+ for k, _gr in groupby(sorted(managed, key=sort_key), key=stem_name):
389
+ gr: list[SystemdUnitState] = []
390
+ for x in _gr:
391
+ assert isinstance(x, SystemdUnitState), x # guaranteed by managed_units function
392
+ gr.append(x)
393
+
350
394
  # if timer is None, guess that means the job is always running?
351
- timer: str | None
352
- service: str
395
+ timer: SystemdUnitState | None
396
+ service: SystemdUnitState
353
397
  if len(gr) == 2:
354
398
  [service, timer] = gr
355
399
  else:
@@ -357,14 +401,16 @@ def get_entries_for_monitor(managed: State, *, params: MonitorParams) -> list[Mo
357
401
  [service] = gr
358
402
  timer = None
359
403
 
404
+ service_props = service.dbus_properties
405
+
360
406
  if timer is not None:
361
- props = bus.properties(timer)
362
- cal = bus.prop(props, '.Timer', 'TimersCalendar')
407
+ props = timer.dbus_properties
408
+ # FIXME this might be io bound? maybe make async or use thread pool?
409
+ cal = bus.prop(props, '.Timer', 'TimersCalendar')
363
410
  next_ = bus.prop(props, '.Timer', 'NextElapseUSecRealtime')
364
411
 
365
- unit_props = bus.properties(service)
366
412
  # note: there is also bus.prop(props, '.Timer', 'LastTriggerUSec'), but makes more sense to use unit to account for manual runs
367
- last = bus.prop(unit_props, '.Unit', 'ActiveExitTimestamp')
413
+ last = bus.prop(service_props, '.Unit', 'ActiveExitTimestamp')
368
414
 
369
415
  schedule = cal[0][1] # TODO is there a more reliable way to retrieve it??
370
416
  # todo not sure if last is really that useful..
@@ -376,101 +422,77 @@ def get_entries_for_monitor(managed: State, *, params: MonitorParams) -> list[Mo
376
422
  if next_dt == datetime.max:
377
423
  left_delta = timedelta(0)
378
424
  else:
379
- left_delta = next_dt - UTCNOW
425
+ left_delta = next_dt - UTCNOW
380
426
  else:
381
- left_delta = timedelta(0) # TODO
427
+ left_delta = timedelta(0) # TODO
382
428
  last_dt = UTCNOW
383
429
  nexts = 'n/a'
384
430
  schedule = 'always'
385
431
 
386
- # TODO maybe format seconds prettier. dunno
387
- def fmt_delta(d: timedelta) -> str:
388
- # format to reduce constant countdown...
389
- ad = abs(d)
390
- # get rid of microseconds
391
- ad = ad - timedelta(microseconds=ad.microseconds)
392
-
393
- day = timedelta(days=1)
394
- hour = timedelta(hours=1)
395
- minute = timedelta(minutes=1)
396
- gt = False
397
- if ad > day:
398
- full_days = ad // day
399
- hours = (ad % day) // hour
400
- ads = f'{full_days}d {hours}h'
401
- gt = True
402
- elif ad > minute:
403
- full_mins = ad // minute
404
- ad = timedelta(minutes=full_mins)
405
- ads = str(ad)
406
- gt = True
407
- else:
408
- # show exact
409
- ads = str(ad)
410
- if len(ads) == 7:
411
- ads = '0' + ads # meh. fix missing leading zero in hours..
412
- ads = ('>' if gt else '') + ads
413
- return ads
414
-
415
-
416
- left = f'{fmt_delta(left_delta)!s:<9}'
432
+ left = f'{_fmt_delta(left_delta)!s:<9}'
417
433
  if last_dt.timestamp() == 0:
418
- ago = 'never' # TODO yellow?
434
+ ago = 'never' # TODO yellow?
419
435
  else:
420
436
  passed_delta = UTCNOW - last_dt
421
- ago = str(fmt_delta(passed_delta))
437
+ ago = str(_fmt_delta(passed_delta))
422
438
  # TODO instead of hacking microsecond, use 'NOW' or something?
423
439
 
424
- props = bus.properties(service)
425
440
  # TODO some summary too? e.g. how often in failed
426
- # TODO make defensive?
427
- result = bus.prop(props, '.Service', 'Result')
428
- exec_start = BusManager.exec_start(props)
429
- assert exec_start is not None, service # not None for services
430
- command = ' '.join(map(shlex.quote, exec_start)) if params.with_command else None
431
- _pid: int | None = int(bus.prop(props, '.Service', 'MainPID'))
432
- pid = None if _pid == 0 else str(_pid)
441
+ if params.with_command:
442
+ exec_start = service.cmdline
443
+ assert exec_start is not None, service # not None for services
444
+ command = shlex.join(exec_start)
445
+ else:
446
+ command = None
447
+ _pid: int | None = int(bus.prop(service_props, '.Service', 'MainPID'))
448
+ pid = None if _pid == 0 else str(_pid)
433
449
 
434
450
  if params.with_success_rate:
435
- rate = _unit_success_rate(service)
451
+ rate = _unit_success_rate(service.unit_file.name)
436
452
  rates = f' {rate:.2f}'
437
453
  else:
438
454
  rates = ''
439
455
 
440
- status_ok = result == 'success'
441
- status = f'{result:<9} {ago:<8}{rates}'
442
-
443
- entries.append(MonitorEntry(
444
- unit=k,
445
- status=status,
446
- left=left,
447
- next=nexts,
448
- schedule=schedule,
449
- command=command,
450
- pid=pid,
451
- status_ok=status_ok,
452
- ))
456
+ service_result = bus.prop(service_props, '.Service', 'Result')
457
+ status_ok = service_result == 'success'
458
+ status = f'{service_result:<9} {ago:<8}{rates}'
459
+
460
+ entries.append(
461
+ MonitorEntry(
462
+ unit=k,
463
+ status=status,
464
+ left=left,
465
+ next=nexts,
466
+ schedule=schedule,
467
+ command=command,
468
+ pid=pid,
469
+ status_ok=status_ok,
470
+ )
471
+ )
453
472
  return entries
454
473
 
455
474
 
456
475
  Json = dict[str, Any]
476
+
477
+
457
478
  def _unit_logs(unit: Unit) -> Iterator[Json]:
458
479
  # TODO so do I need to parse logs to get failure stats? perhaps json would be more reliable
459
480
  cmd = f'journalctl --user -u {unit} -o json -t systemd --output-fields UNIT_RESULT,JOB_TYPE,MESSAGE'
460
481
  with Popen(cmd.split(), stdout=PIPE) as po:
461
- stdout = po.stdout; assert stdout is not None
482
+ stdout = po.stdout
483
+ assert stdout is not None
462
484
  for line in stdout:
463
485
  j = json.loads(line.decode('utf8'))
464
486
  # apparently, successful runs aren't getting logged? not sure why
465
- jt = j.get('JOB_TYPE')
466
- ur = j.get('UNIT_RESULT')
487
+ # jt = j.get('JOB_TYPE')
488
+ # ur = j.get('UNIT_RESULT')
467
489
  # not sure about this..
468
490
  yield j
469
491
 
470
492
 
471
493
  def _unit_success_rate(unit: Unit) -> float:
472
494
  started = 0
473
- failed = 0
495
+ failed = 0
474
496
  # TODO not sure how much time it takes to query all journals?
475
497
  for j in _unit_logs(unit):
476
498
  jt = j.get('JOB_TYPE')
dron/tests/test_dron.py CHANGED
@@ -1,14 +1,27 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
4
+ from collections.abc import Iterator
3
5
  from pathlib import Path
4
6
 
5
7
  import pytest
6
8
 
7
- from ..dron import load_jobs
9
+ from ..dron import do_lint, load_jobs
8
10
 
9
11
 
10
- def test_load_jobs_basic(tmp_path: Path) -> None:
11
- tpath = Path(tmp_path) / 'drontab.py'
12
+ @pytest.fixture
13
+ def tmp_pythonpath(tmp_path: Path) -> Iterator[Path]:
14
+ ps = str(tmp_path)
15
+ assert ps not in sys.path # just in case
16
+ sys.path.insert(0, ps)
17
+ try:
18
+ yield tmp_path
19
+ finally:
20
+ sys.path.remove(ps)
21
+
22
+
23
+ def test_load_jobs_basic(tmp_pythonpath: Path) -> None:
24
+ tpath = Path(tmp_pythonpath) / 'test_drontab.py'
12
25
  tpath.write_text(
13
26
  '''
14
27
  from typing import Iterator
@@ -37,7 +50,7 @@ def jobs() -> Iterator[Job]:
37
50
 
38
51
  '''
39
52
  )
40
- loaded = list(load_jobs(tabfile=tpath, ppath=tmp_path))
53
+ loaded = list(load_jobs(tab_module='test_drontab'))
41
54
  [job1, job2, job3] = loaded
42
55
 
43
56
  assert job1.when == '01:10'
@@ -53,8 +66,8 @@ def jobs() -> Iterator[Job]:
53
66
  assert job3.unit_name == 'job3'
54
67
 
55
68
 
56
- def test_load_jobs_dupes(tmp_path: Path) -> None:
57
- tpath = Path(tmp_path) / 'drontab.py'
69
+ def test_load_jobs_dupes(tmp_pythonpath: Path) -> None:
70
+ tpath = Path(tmp_pythonpath) / 'test_drontab.py'
58
71
  tpath.write_text(
59
72
  '''
60
73
  from typing import Iterator
@@ -69,11 +82,11 @@ def jobs() -> Iterator[Job]:
69
82
  '''
70
83
  )
71
84
  with pytest.raises(AssertionError):
72
- _loaded = list(load_jobs(tabfile=tpath, ppath=tmp_path))
85
+ _loaded = list(load_jobs(tab_module='test_drontab'))
73
86
 
74
87
 
75
- def test_jobs_auto_naming(tmp_path: Path) -> None:
76
- tpath = Path(tmp_path) / 'drontab.py'
88
+ def test_jobs_auto_naming(tmp_pythonpath: Path) -> None:
89
+ tpath = Path(tmp_pythonpath) / 'test_drontab.py'
77
90
  tpath.write_text(
78
91
  '''
79
92
  from typing import Iterator
@@ -105,7 +118,7 @@ def jobs() -> Iterator[Job]:
105
118
  yield job4
106
119
  '''
107
120
  )
108
- loaded = list(load_jobs(tabfile=tpath, ppath=tmp_path))
121
+ loaded = list(load_jobs(tab_module='test_drontab'))
109
122
  (job2, job_named, job_1, job5, job4) = loaded
110
123
  assert job_1.unit_name == 'job_1'
111
124
  assert job_1.when == '00:01'
@@ -117,3 +130,61 @@ def jobs() -> Iterator[Job]:
117
130
  assert job4.when == '00:04'
118
131
  assert job5.unit_name == 'job5'
119
132
  assert job5.when == '00:05'
133
+
134
+
135
+ def test_do_lint(tmp_pythonpath: Path) -> None:
136
+ def OK(body: str) -> None:
137
+ tpath = Path(tmp_pythonpath) / 'test_drontab.py'
138
+ tpath.write_text(body)
139
+ do_lint(tab_module='test_drontab')
140
+
141
+ def FAILS(body: str) -> None:
142
+ with pytest.raises(Exception):
143
+ OK(body)
144
+
145
+ FAILS(
146
+ body='''
147
+ None.whatever
148
+ '''
149
+ )
150
+
151
+ # no jobs
152
+ FAILS(
153
+ body='''
154
+ '''
155
+ )
156
+
157
+ OK(
158
+ body='''
159
+ def jobs():
160
+ yield from []
161
+ '''
162
+ )
163
+
164
+ OK(
165
+ body='''
166
+ from dron.api import job
167
+ def jobs():
168
+ yield job(
169
+ 'hourly',
170
+ ['/bin/echo', '123'],
171
+ unit_name='unit_test',
172
+ )
173
+ '''
174
+ )
175
+
176
+ from ..systemd import _is_missing_systemd
177
+
178
+ if not _is_missing_systemd():
179
+ from ..cli import _drontab_example
180
+
181
+ # this test doesn't work without systemd yet, because launchd adapter doesn't support unquoted commands, at least yet..
182
+ example = _drontab_example()
183
+ # ugh. some hackery to make it find the executable..
184
+ echo = " '/bin/echo"
185
+ example = (
186
+ example.replace(" 'linkchecker", echo)
187
+ .replace(" '/home/user/scripts/run-borg", echo)
188
+ .replace(" 'ping", " '/bin/ping")
189
+ )
190
+ OK(body=example)
@@ -1,6 +1,8 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: dron
3
- Version: 0.1.20241008
3
+ Version: 0.2.20251011
4
+ Summary: What if cron and systemd had a baby?
5
+ Project-URL: Homepage, https://github.com/karlicoss/dron
4
6
  Author-email: "Dima Gerasimov (@karlicoss)" <karlicoss@gmail.com>
5
7
  Maintainer-email: "Dima Gerasimov (@karlicoss)" <karlicoss@gmail.com>
6
8
  License: The MIT License (MIT)
@@ -24,24 +26,16 @@ License: The MIT License (MIT)
24
26
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
27
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
28
  SOFTWARE.
27
-
28
- Project-URL: Homepage, https://github.com/karlicoss/dron
29
- Requires-Python: >=3.9
30
29
  License-File: LICENSE.txt
30
+ Requires-Python: >=3.12
31
31
  Requires-Dist: click
32
+ Requires-Dist: dbus-python; platform_system != 'Darwin'
33
+ Requires-Dist: loguru
34
+ Requires-Dist: mypy
32
35
  Requires-Dist: prompt-toolkit
33
- Requires-Dist: tzlocal
34
- Requires-Dist: textual
35
36
  Requires-Dist: tabulate
36
37
  Requires-Dist: termcolor
37
- Requires-Dist: mypy
38
- Requires-Dist: loguru
39
- Requires-Dist: dbus-python ; platform_system != "Darwin"
38
+ Requires-Dist: textual
39
+ Requires-Dist: tzlocal
40
40
  Provides-Extra: notify-telegram
41
- Requires-Dist: telegram-send >=0.37 ; extra == 'notify-telegram'
42
- Provides-Extra: testing
43
- Requires-Dist: pytest ; extra == 'testing'
44
- Requires-Dist: ruff ; extra == 'testing'
45
- Requires-Dist: mypy ; extra == 'testing'
46
- Requires-Dist: lxml ; extra == 'testing'
47
-
41
+ Requires-Dist: telegram-send>=0.37; extra == 'notify-telegram'
@@ -0,0 +1,23 @@
1
+ dron/__main__.py,sha256=M4GBcqkUcwPRIZIUmoK_lCq5eYghDkte9Avzj1ythG4,126
2
+ dron/api.py,sha256=ca2aLoV1Q7ForYGrIpEUKQBlKNpSglmb_0G1Ws-oOWg,2741
3
+ dron/cli.py,sha256=q0kwLMLT5gr_yBLvhR7vFpS8X4mgCnLX0v7oq1f3P60,7313
4
+ dron/common.py,sha256=Na-lNf_U8NHsNNHTBvtLIcQQic27jAIp0TAQHj9cjRo,4232
5
+ dron/conftest.py,sha256=qI8vwJ6NpTaNofjYDLghCeKjps-i_1DflVTY5Xugqmc,435
6
+ dron/dron.py,sha256=vU5LLSZlJsRos-sl0pmFu8LK3KExOVdO60BbPdUkcPo,12521
7
+ dron/launchd.py,sha256=qZUAg502KYUt7W0s-QcnZ2k-4tiymCEhbtpa2BeP4NA,13091
8
+ dron/launchd_wrapper.py,sha256=0GfrlHt0LpGZ1aZ3vCdLMsL-lwpZHTZIP4ysiK_mujg,3591
9
+ dron/monitor.py,sha256=zIWS8r-D-mcA0PNndwmI7Q8DFqwEXbcW9D8DkyKxCQk,5033
10
+ dron/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ dron/systemd.py,sha256=XAHVTufpMB8zQAK3n8jh--lx7g9TpTwZLlUTeutqvy0,17159
12
+ dron/notify/common.py,sha256=UHcO1DIWMm6_y8ZNoMLYDT3Fe7yo603odoka6LBTgnA,1831
13
+ dron/notify/email.py,sha256=RColplMF4xmkudW3P8VMX30fZbCjLE61SWwpJcB7jt0,1089
14
+ dron/notify/ntfy_common.py,sha256=cY_OXHGbhWxbZchF2PpqnmKs7_BSc-zlkrrNu9qfDhc,754
15
+ dron/notify/ntfy_desktop.py,sha256=-dxwgKtMFlI3U_eg5vDiVZ6IqrAIIheYSpXK8X551pk,278
16
+ dron/notify/ntfy_telegram.py,sha256=DNscsx_qucuqFvkDVOkwMTZouATz-Jpke6HoAc682Is,221
17
+ dron/notify/telegram.py,sha256=UPcjNOCz3W1qDSzCzraPqhQaLy5hnFs2D1esJDbO5QA,916
18
+ dron/tests/test_dron.py,sha256=mVtNtWx7kD7X1peEJRdACkis4MJl7suERbsTWNnYldQ,4400
19
+ dron-0.2.20251011.dist-info/METADATA,sha256=E23BAzLpep_kr6lB7H7Txb5ghQkZwxo8A6ev-eDrTUI,1933
20
+ dron-0.2.20251011.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
+ dron-0.2.20251011.dist-info/entry_points.txt,sha256=GR-EicMii9C6-3vaenLjUNoJvCksPIEx7y3qppT2YuM,44
22
+ dron-0.2.20251011.dist-info/licenses/LICENSE.txt,sha256=J8WsmQpd6swLqaeiFNct4MCsLdSYKX8T5uGw1p7WC8Q,1081
23
+ dron-0.2.20251011.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
dron/__init__.py DELETED
@@ -1,6 +0,0 @@
1
- # NOTE: backwards compatibility, now relying on dron.__main__
2
- # should probably remove this later
3
- from .dron import main
4
-
5
- if __name__ == '__main__':
6
- main()
@@ -1,25 +0,0 @@
1
- dron/__init__.py,sha256=o_sIdoGVpOxBO1sSvKj9ftfEtRF4Af4CUhGCuaElMHI,160
2
- dron/__main__.py,sha256=M4GBcqkUcwPRIZIUmoK_lCq5eYghDkte9Avzj1ythG4,126
3
- dron/api.py,sha256=G5sB60Hvdzl1domilH90NWyJ_MUkMKmxIxZXYitbCOE,2731
4
- dron/cli.py,sha256=NH_cXcDPpzcOy5Pmq0GJB9ciLdrYXpRTwKtEWWtthRs,11801
5
- dron/common.py,sha256=D7xXSeab74N6iKCiQ0jTzIxMs7kTQyM0PhZbZbulEoI,4230
6
- dron/conftest.py,sha256=qI8vwJ6NpTaNofjYDLghCeKjps-i_1DflVTY5Xugqmc,435
7
- dron/dron.py,sha256=z8lXVfOmMn5_67vpU4vfW0r9_NXQW-SgPaUVNkVtWb0,14665
8
- dron/launchd.py,sha256=eQkNjUBQOeZk32Zy4RcTcMT5wcnE3vshu-xylv1H9C0,12681
9
- dron/launchd_wrapper.py,sha256=gv-t_yyNj_xRAOv0qWgrZUPkmShWX4SySXZtfzFJe_g,2847
10
- dron/monitor.py,sha256=3bUk18YRGysYKOe2EsMRLBDOGo-G6HZG4Qa_vToJ_3Y,5041
11
- dron/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- dron/systemd.py,sha256=NT9EnbAEv7rIq1VTFGKAogEnRRhRrPqL0qe6Gv0z2Ds,16797
13
- dron/notify/common.py,sha256=J-EFJNSUc001H-HBj8kMEve8zQ9eFOi-eFQ5YKRfyrY,1822
14
- dron/notify/email.py,sha256=INSjvQXp4ze4b_Cdhw3bdpmrCAn7GtjzFKY0Lw5dYtA,1080
15
- dron/notify/ntfy_common.py,sha256=ZXwg7oowpfJt8pmhhFMsworLDHK5CEDKPkGP7yIken0,621
16
- dron/notify/ntfy_desktop.py,sha256=-dxwgKtMFlI3U_eg5vDiVZ6IqrAIIheYSpXK8X551pk,278
17
- dron/notify/ntfy_telegram.py,sha256=DNscsx_qucuqFvkDVOkwMTZouATz-Jpke6HoAc682Is,221
18
- dron/notify/telegram.py,sha256=-Cv52rPh9x1Y58A4bcJ2Snjf4KzVpiKNGQFGTaAlcxU,900
19
- dron/tests/test_dron.py,sha256=0s6joSFfOfoA6JJoOUjNL8_tzuPYFiUQls4tHynaSvc,2806
20
- dron-0.1.20241008.dist-info/LICENSE.txt,sha256=J8WsmQpd6swLqaeiFNct4MCsLdSYKX8T5uGw1p7WC8Q,1081
21
- dron-0.1.20241008.dist-info/METADATA,sha256=cDy255T8UsmUknk2cPUjANdQ0RqjoDxifATHV_FkZVM,2089
22
- dron-0.1.20241008.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
23
- dron-0.1.20241008.dist-info/entry_points.txt,sha256=GR-EicMii9C6-3vaenLjUNoJvCksPIEx7y3qppT2YuM,44
24
- dron-0.1.20241008.dist-info/top_level.txt,sha256=pgU6oqDihvZc9UeOnpnhZ7LmhOoOaBRQAccMTlnEYwI,5
25
- dron-0.1.20241008.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- dron