prim-ctrl 0.8.1__tar.gz → 0.8.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prim-ctrl
3
- Version: 0.8.1
3
+ Version: 0.8.2
4
4
  Summary: Primitive Ctrl - Remote control of your phone's Primitive FTPd Android SFTP server and optionally Tailscale VPN.
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -37,7 +37,7 @@ class LevelFormatter(logging.Formatter):
37
37
 
38
38
  def __init__(self, fmts: dict[int, str], fmt: str, **kwargs):
39
39
  super().__init__()
40
- self.formatters = dict({level: logging.Formatter(fmt, **kwargs) for level, fmt in fmts.items()})
40
+ self.formatters = {level: logging.Formatter(fmt, **kwargs) for level, fmt in fmts.items()}
41
41
  self.default_formatter = logging.Formatter(fmt, **kwargs)
42
42
 
43
43
  def format(self, record: logging.LogRecord) -> str:
@@ -132,7 +132,7 @@ class SignalFence():
132
132
  if self.on_deferred_signal is not None:
133
133
  try:
134
134
  self.on_deferred_signal(signum, frame)
135
- except:
135
+ except: # NOSONAR(S5754)
136
136
  pass
137
137
 
138
138
  def disable(self) -> None:
@@ -151,11 +151,11 @@ class SignalFence():
151
151
  self.deferred_signal = None
152
152
  logger.debug("Handling deferred signal %d", self.signum)
153
153
  if isinstance(self.original_handler, signal.Handlers):
154
- if self.original_handler is signal.Handlers.SIG_IGN:
155
- pass
156
- elif self.original_handler is signal.Handlers.SIG_DFL:
154
+ if self.original_handler == signal.Handlers.SIG_DFL:
157
155
  signal.signal(self.signum, signal.SIG_DFL)
158
156
  os.kill(os.getpid(), self.signum)
157
+ # elif self.original_handler == signal.Handlers.SIG_IGN:
158
+ # pass
159
159
  elif callable(self.original_handler):
160
160
  self.original_handler(*deferred_signal)
161
161
 
@@ -193,7 +193,7 @@ class Subprocess:
193
193
  # return result.returncode == 0
194
194
 
195
195
  @staticmethod
196
- async def ping(host, packets: int = 1, timeout: float = 1):
196
+ async def ping(host, packets: int = 1, timeout: float = 1): # NOSONAR(S7483)
197
197
  if platform.system().lower() == 'windows':
198
198
  command = ['ping', '-n', str(packets), '-w', str(int(timeout*1000)), host]
199
199
  # don't use text=True, the async version will raise ValueError("text must be False"), who knows why
@@ -214,7 +214,7 @@ class Subprocess:
214
214
  try:
215
215
  proc = await asyncio.create_subprocess_exec(*command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=creationflags)
216
216
  except FileNotFoundError as e:
217
- e.add_note(f"Please check that Tailscale is installed properly")
217
+ e.add_note("Please check that Tailscale is installed properly")
218
218
  raise
219
219
  stdout, stderr = await proc.communicate()
220
220
  return proc.returncode == 0, stdout.decode(), stderr.decode()
@@ -228,7 +228,7 @@ class ExternalDnsResolver(DnsResolver):
228
228
  def __init__(self, where: str):
229
229
  self.where = where
230
230
  self.dns_resolver = None
231
- self.cache = dict[tuple[str, int, socket.AddressFamily], tuple[float, list[ResolveResult]]]()
231
+ self.cache: dict[tuple[str, int, socket.AddressFamily], tuple[float, list[ResolveResult]]] = {}
232
232
 
233
233
  async def resolve(self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_UNSPEC) -> list[ResolveResult]:
234
234
  logger.debug("Resolving DNS at %s for %s:%i (%s)", self.where, host, port, "ipv6" if family == socket.AF_INET6 else "ipv4")
@@ -291,7 +291,7 @@ class Pingable(ABC):
291
291
  def get_state_name(available: bool):
292
292
  return 'up' if available else 'down'
293
293
 
294
- async def wait_for(self, available: bool, timeout: float):
294
+ async def wait_for(self, available: bool, timeout: float): # NOSONAR(S7483)
295
295
  logger.debug("Waiting for %s to be %s (timeout is %ds)", LazyStr(self.get_class_name), LazyStr(Pingable.get_state_name, available), int(timeout))
296
296
  async with asyncio.timeout(timeout):
297
297
  while await self.ping(available) != available:
@@ -315,7 +315,7 @@ class Manageable(Pingable):
315
315
  super().__init__()
316
316
  self.manager = manager
317
317
 
318
- async def _set_state(self, available: bool, repeat: float, timeout: float):
318
+ async def _set_state(self, available: bool, repeat: float, timeout: float): # NOSONAR(S7483)
319
319
  action_name = LazyStr(lambda: 'Starting' if available else 'Stopping')
320
320
  class_name = LazyStr(self.get_class_name)
321
321
  available_name = LazyStr(Pingable.get_state_name, available)
@@ -344,10 +344,10 @@ class Manageable(Pingable):
344
344
  logger.info("%s is %s", LazyStr(self.get_class_name), LazyStr(Pingable.get_state_name, available))
345
345
  return available
346
346
 
347
- async def start(self, repeat: float, timeout: float):
347
+ async def start(self, repeat: float, timeout: float): # NOSONAR(S7483)
348
348
  return await self._set_state(True, repeat, timeout)
349
349
 
350
- async def stop(self, repeat: float, timeout: float):
350
+ async def stop(self, repeat: float, timeout: float): # NOSONAR(S7483)
351
351
  return await self._set_state(False, repeat, timeout)
352
352
 
353
353
  class Service(Manageable):
@@ -367,11 +367,11 @@ class Service(Manageable):
367
367
  writer.close()
368
368
  await writer.wait_closed()
369
369
 
370
- async def _ping(self, availability_hint: bool | None = None):
370
+ async def _ping(self, availability_hint: bool | None = None): # NOSONAR(S1172)
371
371
  logger.debug("Pinging %s (%s:%s)", LazyStr(self.get_class_name), str(self.host), str(self.port))
372
372
  await self._connect(self.host, self.port)
373
373
 
374
- async def ping(self, availability_hint: bool | None = None):
374
+ async def ping(self, availability_hint: bool | None = None): # NOSONAR(S3776)
375
375
  try:
376
376
  await self._ping(availability_hint)
377
377
  return True
@@ -426,9 +426,9 @@ class SshService(Service):
426
426
  asyncssh.connect(host, port, options=asyncssh.SSHClientConnectionOptions(
427
427
  host_key_alias=self.host_name,
428
428
  client_keys=_client_key(),
429
- connect_timeout=self._connect_timeout)) as conn
429
+ connect_timeout=self._connect_timeout))
430
430
  ):
431
- pass
431
+ pass # NOSONAR(S108)
432
432
 
433
433
  class Device(Manageable):
434
434
  def __init__(self, host: str, manager: Manager):
@@ -458,7 +458,7 @@ class StateSerializer:
458
458
  @staticmethod
459
459
  def loads(s: str):
460
460
  try:
461
- return dict({k: StateSerializer.load_value(v) for k, v in [s.split('=') for s in s.split(',')]})
461
+ return {k: StateSerializer.load_value(v) for k, v in [s.split('=') for s in s.split(',')]}
462
462
  except ValueError as e:
463
463
  e.add_note("Missing '=' in state")
464
464
  raise
@@ -468,7 +468,7 @@ class PhoneState(ABC):
468
468
  PFTPD = 'pftpd'
469
469
 
470
470
  @abstractmethod
471
- async def get(self, repeat: float, timeout: float) -> dict:
471
+ async def get(self, repeat: float, timeout: float) -> dict: # NOSONAR(S7483)
472
472
  pass
473
473
 
474
474
  ########
@@ -530,7 +530,7 @@ class ServiceResolver:
530
530
  self.zeroconf = zeroconf
531
531
  self.service_type = service_type
532
532
 
533
- async def get(self, service_name: str, timeout: float = 3):
533
+ async def get(self, service_name: str, timeout: float = 3): # NOSONAR(S7483)
534
534
  service_info = await self.zeroconf.async_get_service_info(self.service_type, f"{service_name}.{self.service_type}", timeout=int(timeout*1000))
535
535
  if not service_info or not service_info.port:
536
536
  raise TimeoutError("Unable to resolve zeroconf (DNS-SD) service information")
@@ -647,6 +647,7 @@ class PftpdServiceListener(ServiceListener):
647
647
  logger.debug(" (ServiceListener) Resolved %s to %s:%d", service_name, host, port)
648
648
 
649
649
  def del_service(self, service_name: str):
650
+ # we intentionally do not remove anything from the cache
650
651
  pass
651
652
 
652
653
  class RemotePftpd(SshService):
@@ -861,7 +862,7 @@ class Webhooks:
861
862
  def __init__(self, host: str, port: int):
862
863
  self.host = host
863
864
  self.port = port
864
- self.variables = dict[str, asyncio.Queue[str]]()
865
+ self.variables: dict[str, asyncio.Queue[str]] = {}
865
866
 
866
867
  @staticmethod
867
868
  def get_ping_path():
@@ -872,7 +873,7 @@ class Webhooks:
872
873
  return f'/{Webhooks.VARIABLE_PATH}/{variable}'
873
874
 
874
875
  async def _start(self):
875
- async def _ping(request: web.Request):
876
+ async def _ping(request: web.Request): # NOSONAR(S7503)
876
877
  return web.Response(text='pong')
877
878
  async def _receive_variable(request: web.Request):
878
879
  queue = self.variables.get(request.match_info['name'])
@@ -899,7 +900,7 @@ class Webhooks:
899
900
  def unsubscribe_variable(self, variable: str):
900
901
  self.variables.pop(variable)
901
902
 
902
- async def get_variable(self, variable: str, timeout: float):
903
+ async def get_variable(self, variable: str, timeout: float): # NOSONAR(S7483)
903
904
  queue = self.variables.get(variable)
904
905
  if not queue:
905
906
  raise ValueError(f"The {variable} is unknown")
@@ -913,6 +914,7 @@ class Webhooks:
913
914
  def __enter__(self):
914
915
  raise TypeError("Use async with instead")
915
916
  def __exit__(self, exc_type, exc_value, exc_tb):
917
+ # can't be called
916
918
  pass
917
919
  async def __aenter__(self):
918
920
  await self._start()
@@ -936,7 +938,7 @@ class Automate:
936
938
  "payload": f"prim-ctrl;{time.time()};" + message
937
939
  }
938
940
  logger.debug("Messaging Automate with: %s", message)
939
- async with self.session.post(f'https://llamalab.com/automate/cloud/message', json=data) as response:
941
+ async with self.session.post('https://llamalab.com/automate/cloud/message', json=data) as response:
940
942
  await response.text()
941
943
 
942
944
  class AutomatePftpdManager(Manager):
@@ -1072,9 +1074,9 @@ class WideHelpFormatter(argparse.RawTextHelpFormatter):
1072
1074
 
1073
1075
  async def gather_with_taskgroup(*coros):
1074
1076
  try:
1075
- async with asyncio.TaskGroup() as tg:
1077
+ async with asyncio.TaskGroup() as tg: # NOSONAR(S7513)
1076
1078
  tasks = [tg.create_task(coro) for coro in coros]
1077
- return tuple([task.result() for task in tasks])
1079
+ return tuple(task.result() for task in tasks)
1078
1080
  except ExceptionGroup as eg:
1079
1081
  exc = eg.exceptions[0]
1080
1082
  # this can be captured in another TaskGroup that drops traceback information from "from e"
@@ -1111,6 +1113,7 @@ class Control:
1111
1113
 
1112
1114
  @staticmethod
1113
1115
  def setup_parser_options(parser):
1116
+ # no options
1114
1117
  pass
1115
1118
 
1116
1119
  @staticmethod
@@ -1145,12 +1148,12 @@ class Control:
1145
1148
  self.phone = phone
1146
1149
  self.keyboard_interrupt = keyboard_interrupt
1147
1150
 
1148
- async def _stop(self, restore_state: dict | None, stop_only_started: bool = False):
1151
+ async def _stop(self, restore_state: dict | None, stop_only_started: bool = False): # NOSONAR(S3776)
1149
1152
  with self.keyboard_interrupt.protect():
1150
1153
  async def _suppress(coro, default: bool):
1151
1154
  try:
1152
1155
  return await coro
1153
- except:
1156
+ except: # NOSONAR(S5754)
1154
1157
  return default
1155
1158
  if self.local.vpn and self.phone.vpn and self.phone.remote_sftp and await _suppress(self.local.vpn.test(), True) and await _suppress(self.phone.vpn.test(), True):
1156
1159
  if (restore_state is None or not restore_state.get(Control.PHONE_SFTP, stop_only_started)) and await _suppress(self.phone.remote_sftp.test(), True):
@@ -1176,7 +1179,7 @@ class Control:
1176
1179
  except Exception as e:
1177
1180
  logger.exception_or_error(e)
1178
1181
 
1179
- async def run(self):
1182
+ async def run(self): # NOSONAR(S3776)
1180
1183
  match self.args.intent:
1181
1184
  case 'test':
1182
1185
  if self.local.vpn and self.phone.vpn and self.phone.remote_sftp and self.phone.state:
@@ -1188,7 +1191,7 @@ class Control:
1188
1191
  await self.phone.zeroconf_sftp.test()
1189
1192
  case 'start':
1190
1193
  if self.local.vpn and self.phone.vpn and self.phone.remote_sftp:
1191
- state = dict()
1194
+ state = {}
1192
1195
  try:
1193
1196
  # gather local state info
1194
1197
  local_vpn_state = await self.local.vpn.test()
@@ -1208,7 +1211,7 @@ class Control:
1208
1211
  state[Control.PHONE_VPN] = phone_vpn_state
1209
1212
  state[Control.PHONE_SFTP] = phone_state[PhoneState.PFTPD]
1210
1213
  if not state[Control.PHONE_WIFI] and not self.args.accept_cellular:
1211
- raise RuntimeError(f"Phone is not on Wi-Fi network")
1214
+ raise RuntimeError("Phone is not on Wi-Fi network")
1212
1215
  else:
1213
1216
  state[Control.PHONE_VPN] = phone_vpn_state = await self.phone.vpn.test()
1214
1217
  if phone_vpn_state:
@@ -1259,7 +1262,7 @@ class Control:
1259
1262
  raise RuntimeError(f"Even when {self.phone.vpn.get_class_name()} and {self.phone.remote_sftp.get_class_name()} is started, {self.phone.remote_sftp.get_class_name()} is still not accessible")
1260
1263
  # print out result on stdout
1261
1264
  if not self.args.backup_state:
1262
- state = dict()
1265
+ state = {}
1263
1266
  state[Control.CONNECTED] = Control.ZEROCONF if zeroconf_accessible else Control.REMOTE
1264
1267
  print(StateSerializer.dumps(state))
1265
1268
  except:
@@ -3,7 +3,7 @@ packages = [{include = "prim_ctrl"}]
3
3
 
4
4
  [project]
5
5
  name = "prim-ctrl"
6
- version = "0.8.1"
6
+ version = "0.8.2"
7
7
  description = "Primitive Ctrl - Remote control of your phone's Primitive FTPd Android SFTP server and optionally Tailscale VPN."
8
8
  license = "Apache-2.0"
9
9
  authors = [
File without changes
File without changes