ddns 4.1.0b4__py2.py3-none-any.whl → 4.1.1__py2.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.
ddns/scheduler/cron.py ADDED
@@ -0,0 +1,111 @@
1
+ # -*- coding:utf-8 -*-
2
+ """
3
+ Cron-based task scheduler for Unix-like systems
4
+ @author: NewFuture
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ import tempfile
10
+
11
+ from ..util.fileio import write_file
12
+ from ..util.try_run import try_run
13
+ from ._base import BaseScheduler
14
+
15
+
16
+ class CronScheduler(BaseScheduler):
17
+ """Cron-based task scheduler for Unix-like systems"""
18
+
19
+ SCHEDULER_NAME = "cron"
20
+
21
+ KEY = "# DDNS:"
22
+
23
+ def _update_crontab(self, lines): # type: (list[str]) -> bool
24
+ """Update crontab with new content"""
25
+ try:
26
+ temp_path = tempfile.mktemp(suffix=".cron")
27
+ write_file(temp_path, u"\n".join(lines) + u"\n") # fmt: skip
28
+ subprocess.check_call(["crontab", temp_path])
29
+ os.unlink(temp_path)
30
+ return True
31
+ except Exception as e:
32
+ self.logger.error("Failed to update crontab: %s", e)
33
+ return False
34
+
35
+ def is_installed(self, crontab_content=None): # type: (str | None) -> bool
36
+ result = crontab_content or try_run(["crontab", "-l"], logger=self.logger) or ""
37
+ return self.KEY in result
38
+
39
+ def get_status(self):
40
+ status = {"scheduler": "cron", "installed": False} # type: dict[str, str | bool | int | None]
41
+ # Get crontab content once and reuse it for all checks
42
+ crontab_content = try_run(["crontab", "-l"], logger=self.logger) or ""
43
+ lines = crontab_content.splitlines()
44
+ line = next((i for i in lines if self.KEY in i), "").strip()
45
+
46
+ if line: # Task is installed
47
+ status["installed"] = True
48
+ status["enabled"] = bool(line and not line.startswith("#"))
49
+ else: # Task not installed
50
+ status["enabled"] = False
51
+
52
+ cmd_groups = line.split(self.KEY, 1) if line else ["", ""]
53
+ parts = cmd_groups[0].strip(" #\t").split() if cmd_groups[0] else []
54
+ status["interval"] = int(parts[0][2:]) if len(parts) >= 5 and parts[0].startswith("*/") else None
55
+ status["command"] = " ".join(parts[5:]) if len(parts) >= 6 else None
56
+ status["description"] = cmd_groups[1].strip() if len(cmd_groups) > 1 else None
57
+
58
+ return status
59
+
60
+ def install(self, interval, ddns_args=None):
61
+ ddns_commands = self._build_ddns_command(ddns_args)
62
+ # Convert array to properly quoted command string for cron
63
+ ddns_command = self._quote_command_array(ddns_commands)
64
+ description = self._get_description()
65
+ cron_entry = '*/{} * * * * cd "{}" && {} # DDNS: {}'.format(interval, os.getcwd(), ddns_command, description)
66
+
67
+ crontext = try_run(["crontab", "-l"], logger=self.logger) or ""
68
+ lines = [line for line in crontext.splitlines() if self.KEY not in line]
69
+ lines.append(cron_entry)
70
+
71
+ if self._update_crontab(lines):
72
+ return True
73
+ else:
74
+ self.logger.error("Failed to install DDNS cron job")
75
+ return False
76
+
77
+ def uninstall(self):
78
+ return self._modify_cron_lines("uninstall")
79
+
80
+ def enable(self):
81
+ return self._modify_cron_lines("enable")
82
+
83
+ def disable(self):
84
+ return self._modify_cron_lines("disable")
85
+
86
+ def _modify_cron_lines(self, action): # type: (str) -> bool
87
+ """Helper to enable, disable, or uninstall cron lines"""
88
+ crontext = try_run(["crontab", "-l"], logger=self.logger)
89
+ if not crontext or self.KEY not in crontext:
90
+ self.logger.info("No crontab found")
91
+ return False
92
+
93
+ modified_lines = []
94
+ for line in crontext.rstrip("\n").splitlines():
95
+ if self.KEY not in line:
96
+ modified_lines.append(line)
97
+ elif action == "uninstall":
98
+ continue # Skip DDNS lines (remove them)
99
+ elif action == "enable" and line.strip().startswith("#"):
100
+ uncommented = line.lstrip(" #\t").lstrip() # Enable: uncomment the line
101
+ modified_lines.append(uncommented if uncommented else line)
102
+ elif action == "disable" and not line.strip().startswith("#"):
103
+ modified_lines.append("# " + line) # Disable: comment the line
104
+ else:
105
+ raise ValueError("Invalid action: {}".format(action))
106
+
107
+ if self._update_crontab(modified_lines):
108
+ return True
109
+ else:
110
+ self.logger.error("Failed to %s DDNS cron job", action)
111
+ return False
@@ -0,0 +1,130 @@
1
+ # -*- coding:utf-8 -*-
2
+ """
3
+ macOS launchd-based task scheduler
4
+ @author: NewFuture
5
+ """
6
+
7
+ import os
8
+ import re
9
+
10
+ from ..util.fileio import read_file_safely, write_file
11
+ from ..util.try_run import try_run
12
+ from ._base import BaseScheduler
13
+
14
+
15
+ class LaunchdScheduler(BaseScheduler):
16
+ """macOS launchd-based task scheduler"""
17
+
18
+ LABEL = "cc.newfuture.ddns"
19
+
20
+ def _get_plist_path(self):
21
+ return os.path.expanduser("~/Library/LaunchAgents/{}.plist".format(self.LABEL))
22
+
23
+ def is_installed(self):
24
+ return os.path.exists(self._get_plist_path())
25
+
26
+ def get_status(self):
27
+ # Read plist content once and use it to determine installation status
28
+ content = read_file_safely(self._get_plist_path())
29
+ status = {"scheduler": "launchd", "installed": bool(content)}
30
+ if not content:
31
+ return status
32
+
33
+ # For launchd, check if service is actually loaded/enabled
34
+ result = try_run(["launchctl", "list"], logger=self.logger)
35
+ status["enabled"] = bool(result) and self.LABEL in result
36
+
37
+ # Get interval
38
+ interval_match = re.search(r"<key>StartInterval</key>\s*<integer>(\d+)</integer>", content)
39
+ status["interval"] = int(interval_match.group(1)) // 60 if interval_match else None
40
+
41
+ # Get command
42
+ program_match = re.search(r"<key>Program</key>\s*<string>([^<]+)</string>", content)
43
+ if program_match:
44
+ status["command"] = program_match.group(1)
45
+ else:
46
+ args_section = re.search(r"<key>ProgramArguments</key>\s*<array>(.*?)</array>", content, re.DOTALL)
47
+ if args_section:
48
+ strings = re.findall(r"<string>([^<]+)</string>", args_section.group(1))
49
+ if strings:
50
+ status["command"] = " ".join(strings)
51
+
52
+ # Get comments
53
+ desc_match = re.search(r"<key>Description</key>\s*<string>([^<]+)</string>", content)
54
+ status["description"] = desc_match.group(1) if desc_match else None
55
+
56
+ return status
57
+
58
+ def install(self, interval, ddns_args=None):
59
+ plist_path = self._get_plist_path()
60
+ program_args = self._build_ddns_command(ddns_args)
61
+
62
+ # Create comment with version and install date (consistent with Windows)
63
+ comment = self._get_description()
64
+
65
+ # Generate plist XML using template string for proper macOS plist format
66
+ program_args_xml = "".join(" <string>{}</string>\n".format(arg) for arg in program_args)
67
+
68
+ plist_content = """<?xml version="1.0" encoding="UTF-8"?>
69
+ <plist version="1.0">
70
+ <dict>
71
+ <key>Label</key>
72
+ <string>{label}</string>
73
+ <key>Description</key>
74
+ <string>{description}</string>
75
+ <key>ProgramArguments</key>
76
+ <array>{program}</array>
77
+ <key>StartInterval</key>
78
+ <integer>{interval}</integer>
79
+ <key>RunAtLoad</key>
80
+ <true/>
81
+ <key>WorkingDirectory</key>
82
+ <string>{dir}</string>
83
+ </dict>
84
+ </plist>""".format(
85
+ label=self.LABEL, description=comment, program=program_args_xml, interval=interval * 60, dir=os.getcwd()
86
+ )
87
+
88
+ write_file(plist_path, plist_content)
89
+ result = try_run(["launchctl", "load", plist_path], logger=self.logger)
90
+ if result is not None:
91
+ self.logger.info("DDNS launchd service installed successfully")
92
+ return True
93
+ else:
94
+ self.logger.error("Failed to load launchd service")
95
+ return False
96
+
97
+ def uninstall(self):
98
+ plist_path = self._get_plist_path()
99
+ try_run(["launchctl", "unload", plist_path], logger=self.logger) # Ignore errors
100
+ try:
101
+ os.remove(plist_path)
102
+ except OSError:
103
+ pass
104
+
105
+ self.logger.info("DDNS launchd service uninstalled successfully")
106
+ return True
107
+
108
+ def enable(self):
109
+ plist_path = self._get_plist_path()
110
+ if not os.path.exists(plist_path):
111
+ self.logger.error("DDNS launchd service not found")
112
+ return False
113
+
114
+ result = try_run(["launchctl", "load", plist_path], logger=self.logger)
115
+ if result is not None:
116
+ self.logger.info("DDNS launchd service enabled successfully")
117
+ return True
118
+ else:
119
+ self.logger.error("Failed to enable launchd service")
120
+ return False
121
+
122
+ def disable(self):
123
+ plist_path = self._get_plist_path()
124
+ result = try_run(["launchctl", "unload", plist_path], logger=self.logger)
125
+ if result is not None:
126
+ self.logger.info("DDNS launchd service disabled successfully")
127
+ return True
128
+ else:
129
+ self.logger.error("Failed to disable launchd service")
130
+ return False
@@ -0,0 +1,120 @@
1
+ # -*- coding:utf-8 -*-
2
+ """
3
+ schtasks-based task scheduler
4
+ @author: NewFuture
5
+ """
6
+
7
+ import os
8
+ import re
9
+ import tempfile
10
+
11
+ from ..util.try_run import try_run
12
+ from ._base import BaseScheduler
13
+
14
+
15
+ class SchtasksScheduler(BaseScheduler):
16
+ """schtasks-based task scheduler"""
17
+
18
+ NAME = "DDNS"
19
+
20
+ def _schtasks(self, *args):
21
+ """Helper to run schtasks commands with consistent error handling"""
22
+ result = try_run(["schtasks"] + list(args), logger=self.logger)
23
+ return result is not None
24
+
25
+ def _extract_xml(self, xml_text, tag_name):
26
+ """Extract XML tag content using regex for better performance and flexibility"""
27
+ pattern = r"<{0}[^>]*>(.*?)</{0}>".format(re.escape(tag_name))
28
+ match = re.search(pattern, xml_text, re.DOTALL)
29
+ return match.group(1).strip() if match else None
30
+
31
+ def is_installed(self):
32
+ result = try_run(["schtasks", "/query", "/tn", self.NAME], logger=self.logger) or ""
33
+ return self.NAME in result
34
+
35
+ def get_status(self):
36
+ # Use XML format for language-independent parsing
37
+ task_xml = try_run(["schtasks", "/query", "/tn", self.NAME, "/xml"], logger=self.logger)
38
+ status = {"scheduler": "schtasks", "installed": bool(task_xml)}
39
+
40
+ if not task_xml:
41
+ return status # Task not installed, return minimal status
42
+
43
+ status["enabled"] = self._extract_xml(task_xml, "Enabled") != "false"
44
+ command = self._extract_xml(task_xml, "Command")
45
+ arguments = self._extract_xml(task_xml, "Arguments")
46
+ status["command"] = "{} {}".format(command, arguments) if command and arguments else command
47
+
48
+ # Parse interval: PT10M -> 10, fallback to original string
49
+ interval_str = self._extract_xml(task_xml, "Interval")
50
+ interval_match = re.search(r"PT(\d+)M", interval_str) if interval_str else None
51
+ status["interval"] = int(interval_match.group(1)) if interval_match else interval_str
52
+
53
+ # Show description if exists, otherwise show installation date
54
+ description = self._extract_xml(task_xml, "Description") or self._extract_xml(task_xml, "Date")
55
+ if description:
56
+ status["description"] = description
57
+ return status
58
+
59
+ def install(self, interval, ddns_args=None):
60
+ # Build command line as array: prefer pythonw for script mode, or compiled exe directly
61
+ cmd_array = self._build_ddns_command(ddns_args)
62
+ workdir = os.getcwd()
63
+
64
+ # Split array into executable and arguments for schtasks XML format
65
+ # For Windows scheduler, prefer pythonw.exe to avoid console window
66
+ executable = cmd_array[0].replace("python.exe", "pythonw.exe")
67
+ arguments = self._quote_command_array(cmd_array[1:]) if len(cmd_array) > 1 else ""
68
+
69
+ # Create XML task definition with working directory support
70
+ description = self._get_description()
71
+
72
+ # Use template string to generate Windows Task Scheduler XML
73
+ xml_content = """<?xml version="1.0" encoding="UTF-16"?>
74
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
75
+ <RegistrationInfo>
76
+ <Description>{description}</Description>
77
+ </RegistrationInfo>
78
+ <Triggers>
79
+ <TimeTrigger>
80
+ <StartBoundary>1900-01-01T00:00:00</StartBoundary>
81
+ <Repetition>
82
+ <Interval>PT{interval}M</Interval>
83
+ </Repetition>
84
+ <Enabled>true</Enabled>
85
+ </TimeTrigger>
86
+ </Triggers>
87
+ <Actions>
88
+ <Exec>
89
+ <Command>{exe}</Command>
90
+ <Arguments>{arguments}</Arguments>
91
+ <WorkingDirectory>{dir}</WorkingDirectory>
92
+ </Exec>
93
+ </Actions>
94
+ <Settings>
95
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
96
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
97
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
98
+ </Settings>
99
+ </Task>""".format(description=description, interval=interval, exe=executable, arguments=arguments, dir=workdir)
100
+
101
+ # Write XML to temp file and use it to create task
102
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".xml", delete=False) as f:
103
+ f.write(xml_content)
104
+ xml_file = f.name
105
+
106
+ try:
107
+ success = self._schtasks("/Create", "/XML", xml_file, "/TN", self.NAME, "/F")
108
+ return success
109
+ finally:
110
+ os.unlink(xml_file)
111
+
112
+ def uninstall(self):
113
+ success = self._schtasks("/Delete", "/TN", self.NAME, "/F")
114
+ return success
115
+
116
+ def enable(self):
117
+ return self._schtasks("/Change", "/TN", self.NAME, "/Enable")
118
+
119
+ def disable(self):
120
+ return self._schtasks("/Change", "/TN", self.NAME, "/Disable")
@@ -0,0 +1,139 @@
1
+ # -*- coding:utf-8 -*-
2
+ """
3
+ Systemd timer-based task scheduler for Linux
4
+ @author: NewFuture
5
+ """
6
+
7
+ import os
8
+ import re
9
+
10
+ from ..util.fileio import read_file_safely, write_file
11
+ from ..util.try_run import try_run
12
+ from ._base import BaseScheduler
13
+
14
+ try: # python 3
15
+ PermissionError # type: ignore
16
+ except NameError: # python 2 doesn't have PermissionError, use OSError instead
17
+ PermissionError = IOError
18
+
19
+
20
+ class SystemdScheduler(BaseScheduler):
21
+ """Systemd timer-based task scheduler for Linux"""
22
+
23
+ SERVICE_NAME = "ddns.service"
24
+ TIMER_NAME = "ddns.timer"
25
+ SERVICE_PATH = "/etc/systemd/system/ddns.service"
26
+ TIMER_PATH = "/etc/systemd/system/ddns.timer"
27
+
28
+ def _systemctl(self, *args):
29
+ """Run systemctl command and return success status"""
30
+ result = try_run(["systemctl"] + list(args), logger=self.logger)
31
+ return result is not None
32
+
33
+ def is_installed(self):
34
+ """Check if systemd timer files exist"""
35
+ return os.path.exists(self.SERVICE_PATH) and os.path.exists(self.TIMER_PATH)
36
+
37
+ def get_status(self):
38
+ """Get comprehensive status information"""
39
+ installed = self.is_installed()
40
+ status = {"scheduler": "systemd", "installed": installed}
41
+ if not installed:
42
+ return status
43
+
44
+ # Check if timer is enabled
45
+ result = try_run(["systemctl", "is-enabled", self.TIMER_NAME], logger=self.logger)
46
+ status["enabled"] = bool(result and result.strip() == "enabled")
47
+
48
+ # Extract interval from timer file
49
+ timer_content = read_file_safely(self.TIMER_PATH) or ""
50
+ match = re.search(r"OnUnitActiveSec=(\d+)m", timer_content)
51
+ status["interval"] = int(match.group(1)) if match else None
52
+
53
+ # Extract command and description from service file
54
+ service_content = read_file_safely(self.SERVICE_PATH) or ""
55
+ match = re.search(r"ExecStart=(.+)", service_content)
56
+ status["command"] = match.group(1).strip() if match else None
57
+ desc_match = re.search(r"Description=(.+)", service_content)
58
+ status["description"] = desc_match.group(1).strip() if desc_match else None
59
+
60
+ return status
61
+
62
+ def install(self, interval, ddns_args=None):
63
+ """Install systemd timer with specified interval"""
64
+ ddns_commands = self._build_ddns_command(ddns_args)
65
+ # Convert array to properly quoted command string for ExecStart
66
+ ddns_command = self._quote_command_array(ddns_commands)
67
+ work_dir = os.getcwd()
68
+ description = self._get_description()
69
+
70
+ # Create service file content
71
+ service_content = u"""[Unit]
72
+ Description={}
73
+ After=network.target
74
+
75
+ [Service]
76
+ Type=oneshot
77
+ WorkingDirectory={}
78
+ ExecStart={}
79
+ """.format(description, work_dir, ddns_command) # fmt: skip
80
+
81
+ # Create timer file content
82
+ timer_content = u"""[Unit]
83
+ Description=DDNS automatic IP update timer
84
+ Requires={}
85
+
86
+ [Timer]
87
+ OnUnitActiveSec={}m
88
+ Unit={}
89
+
90
+ [Install]
91
+ WantedBy=multi-user.target
92
+ """.format(self.SERVICE_NAME, interval, self.SERVICE_NAME) # fmt: skip
93
+
94
+ try:
95
+ # Write service and timer files
96
+ write_file(self.SERVICE_PATH, service_content)
97
+ write_file(self.TIMER_PATH, timer_content)
98
+ except PermissionError as e:
99
+ self.logger.debug("Permission denied when writing systemd files: %s", e)
100
+ print("Permission denied. Please run as root or use sudo.")
101
+ print("or use cron scheduler (with --scheduler=cron) instead.")
102
+ return False
103
+ except Exception as e:
104
+ self.logger.error("Failed to write systemd files: %s", e)
105
+ return False
106
+
107
+ if self._systemctl("daemon-reload") and self._systemctl("enable", self.TIMER_NAME):
108
+ self._systemctl("start", self.TIMER_NAME)
109
+ return True
110
+ else:
111
+ self.logger.error("Failed to enable/start systemd timer")
112
+ return False
113
+
114
+ def uninstall(self):
115
+ """Uninstall systemd timer and service"""
116
+ self.disable() # Stop and disable timer
117
+ # Remove systemd files
118
+ try:
119
+ os.remove(self.SERVICE_PATH)
120
+ os.remove(self.TIMER_PATH)
121
+ self._systemctl("daemon-reload") # Reload systemd configuration
122
+ return True
123
+
124
+ except PermissionError as e:
125
+ self.logger.debug("Permission denied when removing systemd files: %s", e)
126
+ print("Permission denied. Please run as root or use sudo.")
127
+ return False
128
+ except Exception as e:
129
+ self.logger.error("Failed to remove systemd files: %s", e)
130
+ return False
131
+
132
+ def enable(self):
133
+ """Enable and start systemd timer"""
134
+ return self._systemctl("enable", self.TIMER_NAME) and self._systemctl("start", self.TIMER_NAME)
135
+
136
+ def disable(self):
137
+ """Disable and stop systemd timer"""
138
+ self._systemctl("stop", self.TIMER_NAME)
139
+ return self._systemctl("disable", self.TIMER_NAME)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddns
3
- Version: 4.1.0b4
3
+ Version: 4.1.1
4
4
  Summary: Dynamic DNS client for multiple providers, supporting IPv4 and IPv6.
5
5
  Author-email: NewFuture <python@newfuture.cc>
6
6
  License-Expression: MIT
@@ -1,15 +1,20 @@
1
1
  ddns/__builtins__.pyi,sha256=9ZCOh51Aq6nBUZTt_0TBKpzcUeTxi-KBw-9TkXIGSH4,128
2
- ddns/__init__.py,sha256=KJr4XPaFaRCOLq4Fwvs0mOh6lIRhExsFH6w4ms402u8,258
3
- ddns/__main__.py,sha256=VgANBMJ01nI5-nRYyKkIcLG0GOO_AKjHRdqVV0cwLrk,5366
2
+ ddns/__init__.py,sha256=t_uBV-9inA0NtqwVSHDiz-m-g1jrsaxmLtM31mTnFYs,256
3
+ ddns/__main__.py,sha256=gZjSw-FUxr4Z8l0h0hPpdp2B46ANiCtyAXAtL1myVa0,5423
4
4
  ddns/cache.py,sha256=A1s3rnOJbPrGEjbGbbISrVg46lZFzZ84WPdzHaCtYBk,5971
5
5
  ddns/ip.py,sha256=6H5jYv-TT-o0wSoFAGOlTAbf-TAS92Kx6vafi3LXTA4,5151
6
+ ddns/config/__init__.py,sha256=p8AoT2ZJ4LXAf0ij3i2zq4sq2rbIkv70lf_NQQVugKM,6871
7
+ ddns/config/cli.py,sha256=LcNXWTYimCCHm0kUVAb6ERdJVq9SsadEZyRiPSlT2PU,14447
8
+ ddns/config/config.py,sha256=Qhlz3UNcyQ8Evcp4T3xvM54LvMMD-wSEI4Xq3YgtGpY,7234
9
+ ddns/config/env.py,sha256=RSf4XfMN50zCJQ7QxLQE_4gQdNCiD-z3Gu8UCZHfqPM,2503
10
+ ddns/config/file.py,sha256=TzSU2qIgHKeJetkfM7doQVS16TfPcnD4ji-wOnp8ZRo,6450
6
11
  ddns/provider/__init__.py,sha256=qAw-R-l7nUA9L96Tmr2n-T7_g8T9Mah7u615yg2zScY,2793
7
12
  ddns/provider/_base.py,sha256=q31rqiiqEmM5Kt7SLdRjcREDrlGdRAQX6mNoe8ULPz8,20065
8
13
  ddns/provider/_signature.py,sha256=fF8XxMDkXjia96d1gIVYWc72MVVe65AAnD7qr51qKXA,4140
9
14
  ddns/provider/alidns.py,sha256=zymb1lNaj8yfUaQr8xfSxKojsLPfbo5orZcxBigcblM,6123
10
15
  ddns/provider/aliesa.py,sha256=-4pBmV03NWot5CVMQYluDHChk-nwb1kQpmUdlZSLy6A,4722
11
16
  ddns/provider/callback.py,sha256=QAOSWr-LWN8UG3NIEMWTvjax1g9R0rCJz6Xtr4KbQDo,2925
12
- ddns/provider/cloudflare.py,sha256=BGV_73QqKYyOWkG5ijphrsZJx2C8Y_fdo0NElwFVx9c,4497
17
+ ddns/provider/cloudflare.py,sha256=_O_0H3mEehue8fBqQIhP_3hga71lb4or7SBQaezUMLE,5532
13
18
  ddns/provider/debug.py,sha256=JKneEkOMasYxis9kcU97mHJMo3jdLRuU1w7AAdAqMhY,586
14
19
  ddns/provider/dnscom.py,sha256=4r2ijL158dnSojj2WuLRWyxvUtGvibuowHvDzUdV-QU,3681
15
20
  ddns/provider/dnspod.py,sha256=kgFsTULdooPDd6LmNt2cmLJjw8JRsmq8sfMpTs8REPc,4423
@@ -20,14 +25,20 @@ ddns/provider/huaweidns.py,sha256=upKKfBXYhQIzqxuoSRQYBXCx4lpHVjHJJQL0BS8rFsE,56
20
25
  ddns/provider/namesilo.py,sha256=jgqkQUcHVCyVjYoxazmcN9Gu_Zd-EAv3aSnNmHHJ1jk,5843
21
26
  ddns/provider/noip.py,sha256=RivoLhWJJJuiBzEbtRnnHiBkcLfbvAGglZKKY9s_lcY,4035
22
27
  ddns/provider/tencentcloud.py,sha256=aEWqQ00EWUG-JWW2D0IuJDWZ1OdbTPheGeYSlIh7bqQ,7159
28
+ ddns/scheduler/__init__.py,sha256=yBoxs0cnCVdGKk4T5m1tZe75do-iH2806bq8ZHW-dG4,2454
29
+ ddns/scheduler/_base.py,sha256=PSWiDQ3FCb5eFT4ZO9zuPu2ADKswlLwhrpgc2AVLEoY,2813
30
+ ddns/scheduler/cron.py,sha256=X317yye2Kg_Ow9W1OOaPBipRmJTBZYetyM9CaqPkW6I,4372
31
+ ddns/scheduler/launchd.py,sha256=HP7G6K9CGtsqWcSGl25NFKquSv9Ak0oDEhxBSmyOr5Y,4648
32
+ ddns/scheduler/schtasks.py,sha256=P5APkh6eWwZh8AedYNq4HQWFajleTo-nezcZxP_NyLI,4608
33
+ ddns/scheduler/systemd.py,sha256=m3P_TJYMSgZZH_CndrfKVu0B3o7lXZw7D9oA5c2yguc,4960
23
34
  ddns/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
35
  ddns/util/comment.py,sha256=_B8sRpCJksNspsdld4ha03W1TAi-MyPRK7pL7ym0j9k,2199
25
36
  ddns/util/fileio.py,sha256=bykJgmdy3gZ79iUQSVpMprRej1PGMJSDSHedIngFdG4,3169
26
37
  ddns/util/http.py,sha256=qxfvpN0xt7GZ-GsHFhij4yjH0frWAC52XfaX5ZigFMw,12942
27
38
  ddns/util/try_run.py,sha256=juDPxvT5xUabK2DiQNtFIyaATehtserHCVkP7_joSyU,1304
28
- ddns-4.1.0b4.dist-info/licenses/LICENSE,sha256=MI-ECjp-Vl7WZLiSPY6r5VwrOReNiICVB1QCXiUGt_s,1111
29
- ddns-4.1.0b4.dist-info/METADATA,sha256=CtRAgOz4l9v8t-l5QxIgBWV2tF2jGtglSfeEbbOC4yg,19636
30
- ddns-4.1.0b4.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
31
- ddns-4.1.0b4.dist-info/entry_points.txt,sha256=2-VbA-WZcjebkZrGKvUCuBBRYF4xQNMoLIoGaS234WU,44
32
- ddns-4.1.0b4.dist-info/top_level.txt,sha256=Se0wn3T8Bc4pj55dGwVrCe8BFwmFCBwQVHF1bTyV0o0,5
33
- ddns-4.1.0b4.dist-info/RECORD,,
39
+ ddns-4.1.1.dist-info/licenses/LICENSE,sha256=MI-ECjp-Vl7WZLiSPY6r5VwrOReNiICVB1QCXiUGt_s,1111
40
+ ddns-4.1.1.dist-info/METADATA,sha256=eC7krFPlc8yBCUD_Z741HQZ7e5YavCz3r-Iapgx7oiw,19634
41
+ ddns-4.1.1.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
42
+ ddns-4.1.1.dist-info/entry_points.txt,sha256=2-VbA-WZcjebkZrGKvUCuBBRYF4xQNMoLIoGaS234WU,44
43
+ ddns-4.1.1.dist-info/top_level.txt,sha256=Se0wn3T8Bc4pj55dGwVrCe8BFwmFCBwQVHF1bTyV0o0,5
44
+ ddns-4.1.1.dist-info/RECORD,,
File without changes