ddns 4.1.0b2__py2.py3-none-any.whl → 4.1.0b3__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.

Potentially problematic release.


This version of ddns might be problematic. Click here for more details.

ddns/__builtins__.pyi CHANGED
@@ -1,5 +1,6 @@
1
1
  # coding=utf-8
2
2
  # flake8: noqa: F401
3
+ # ruff: noqa: F403
3
4
  from typing import *
4
5
  from .provider import SimpleProvider
5
6
  import logging
ddns/__init__.py CHANGED
@@ -6,7 +6,7 @@ ddns Package
6
6
  __description__ = "automatically update DNS records to my IP [域名自动指向本机IP]"
7
7
 
8
8
  # 编译时,版本会被替换
9
- __version__ = "4.1.0b2"
9
+ __version__ = "4.1.0b3"
10
10
 
11
11
  # 时间也会被替换掉
12
- build_date = "2025-07-15T02:27:02Z"
12
+ build_date = "2025-08-11T15:00:49Z"
ddns/__main__.py CHANGED
@@ -10,14 +10,12 @@ from logging import getLogger
10
10
  import sys
11
11
 
12
12
  from .__init__ import __version__, __description__, build_date
13
- from .config import load_config, Config # noqa: F401
14
- from .provider import get_provider_class, SimpleProvider
13
+ from .config import load_configs, Config # noqa: F401
14
+ from .provider import get_provider_class, SimpleProvider # noqa: F401
15
15
  from . import ip
16
16
  from .cache import Cache
17
17
 
18
18
  logger = getLogger()
19
- # Set user agent for All Providers
20
- SimpleProvider.user_agent = SimpleProvider.user_agent.format(version=__version__)
21
19
 
22
20
 
23
21
  def get_ip(ip_type, rules):
@@ -94,7 +92,7 @@ def run(config):
94
92
  # dns provider class
95
93
  provider_class = get_provider_class(config.dns)
96
94
  dns = provider_class(
97
- config.id, config.token, endpoint=config.endpoint, logger=logger, proxy=config.proxy, verify_ssl=config.ssl
95
+ config.id, config.token, endpoint=config.endpoint, logger=logger, proxy=config.proxy, ssl=config.ssl
98
96
  )
99
97
  cache = Cache.new(config.cache, config.md5(), logger)
100
98
  return (
@@ -110,8 +108,38 @@ def main():
110
108
  sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
111
109
  sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
112
110
  logger.name = "ddns"
113
- config = load_config(__description__, __version__, build_date)
114
- run(config)
111
+
112
+ # 使用多配置加载器,它会自动处理单个和多个配置
113
+ configs = load_configs(__description__, __version__, build_date)
114
+
115
+ if len(configs) == 1:
116
+ # 单个配置,使用原有逻辑(向后兼容)
117
+ config = configs[0]
118
+ success = run(config)
119
+ if not success:
120
+ sys.exit(1)
121
+ else:
122
+ # 多个配置,使用新的批处理逻辑
123
+ overall_success = True
124
+ for i, config in enumerate(configs):
125
+ # 如果log_level有值则设置setLevel
126
+ if hasattr(config, "log_level") and config.log_level:
127
+ logger.setLevel(config.log_level)
128
+ logger.info("Running configuration %d/%d", i + 1, len(configs))
129
+ # 记录当前provider
130
+ logger.info("Using DNS provider: %s", config.dns)
131
+ success = run(config)
132
+ if not success:
133
+ overall_success = False
134
+ logger.error("Configuration %d failed", i + 1)
135
+ else:
136
+ logger.info("Configuration %d completed successfully", i + 1)
137
+
138
+ if not overall_success:
139
+ logger.error("Some configurations failed")
140
+ sys.exit(1)
141
+ else:
142
+ logger.info("All configurations completed successfully")
115
143
 
116
144
 
117
145
  if __name__ == "__main__":
ddns/ip.py CHANGED
@@ -5,7 +5,7 @@ from os import name as os_name, popen
5
5
  from socket import socket, getaddrinfo, gethostname, AF_INET, AF_INET6, SOCK_DGRAM
6
6
  from logging import debug, error
7
7
 
8
- from .util.http import send_http_request
8
+ from .util.http import request
9
9
 
10
10
  # 模块级别的SSL验证配置,默认使用auto模式
11
11
  ssl_verify = "auto"
@@ -48,9 +48,8 @@ def local_v4(i=0): # 本地ipv4地址
48
48
  def _open(url, reg):
49
49
  try:
50
50
  debug("open: %s", url)
51
- response = send_http_request(
52
- method="GET", url=url, headers={"User-Agent": "Mozilla/5.0 ddns"}, verify_ssl=ssl_verify
53
- )
51
+ # IP 模块重试3次
52
+ response = request("GET", url, verify=ssl_verify, retries=2)
54
53
  res = response.body
55
54
  debug("response: %s", res)
56
55
  match = compile(reg).search(res)
@@ -70,7 +69,6 @@ def public_v6(url="https://api-ipv6.ip.sb/ip", reg=IPV6_REG): # 公网IPV6地
70
69
 
71
70
 
72
71
  def _ip_regex_match(parrent_regex, match_regex):
73
-
74
72
  ip_pattern = compile(parrent_regex)
75
73
  matcher = compile(match_regex)
76
74
 
ddns/provider/_base.py CHANGED
@@ -58,7 +58,7 @@ Defines a unified interface to support extension and adaptation across providers
58
58
  from abc import ABCMeta, abstractmethod
59
59
  from json import loads as jsondecode, dumps as jsonencode
60
60
  from logging import Logger, getLogger # noqa:F401 # type: ignore[no-redef]
61
- from ..util.http import send_http_request, quote, urlencode
61
+ from ..util.http import request, quote, urlencode
62
62
 
63
63
  TYPE_FORM = "application/x-www-form-urlencoded"
64
64
  TYPE_JSON = "application/json"
@@ -102,13 +102,11 @@ class SimpleProvider(object):
102
102
  accept = TYPE_JSON # type: str | None
103
103
  # Decode Response as JSON by default
104
104
  decode_response = True # type: bool
105
- # UA 可自定义, 可在子类中覆盖,空则不设置
106
- user_agent = "DDNS/{version} (ddns@newfuture.cc)"
107
105
  # Description
108
106
  remark = "Managed by [DDNS](https://ddns.newfuture.cc)"
109
107
 
110
- def __init__(self, id, token, logger=None, verify_ssl="auto", proxy=None, endpoint=None, **options):
111
- # type: (str, str, Logger | None, bool|str, Sequence[str|None]|None, str|None, **object) -> None
108
+ def __init__(self, id, token, logger=None, ssl="auto", proxy=None, endpoint=None, **options):
109
+ # type: (str, str, Logger | None, bool|str, list[str]|None, str|None, **object) -> None
112
110
  """
113
111
  初始化服务商对象
114
112
 
@@ -117,14 +115,18 @@ class SimpleProvider(object):
117
115
  Args:
118
116
  id (str): 身份认证 ID / Authentication ID
119
117
  token (str): 密钥 / Authentication Token
118
+ proxy (list[str | None] | None): 代理配置,支持代理列表
120
119
  options (dict): 其它参数 / Additional options
121
120
  """
122
121
  self.id = id
123
122
  self.token = token
124
123
  if endpoint:
125
124
  self.endpoint = endpoint
126
- self._proxy = proxy if proxy and len(proxy) else [None]
127
- self._ssl = verify_ssl
125
+
126
+ # 处理代理配置
127
+ self._proxy = proxy # 代理列表或None
128
+
129
+ self._ssl = ssl
128
130
 
129
131
  self.options = options
130
132
  name = self.__class__.__name__
@@ -227,26 +229,11 @@ class SimpleProvider(object):
227
229
  # 处理headers
228
230
  if self.accept and "accept" not in headers and "Accept" not in headers:
229
231
  headers["accept"] = self.accept
230
- if "user-agent" not in headers and "User-Agent" not in headers and self.user_agent:
231
- headers["user-agent"] = self.user_agent
232
- if len(headers) > 3:
232
+ if len(headers) > 2:
233
233
  self.logger.debug("headers:\n%s", {k: self._mask_sensitive_data(v) for k, v in headers.items()})
234
234
 
235
- response = None # type: Any
236
- for p in self._proxy:
237
- if p:
238
- self.logger.debug("Using proxy: %s", p)
239
- try:
240
- response = send_http_request(
241
- method, url, body=body_data, headers=headers, proxy=p, verify_ssl=self._ssl
242
- )
243
- break # 成功发送请求,跳出循环
244
- except Exception as e:
245
- self.logger.warning("Failed to send request: %s", e)
246
- if not response:
247
- if len(self._proxy) > 1:
248
- self.logger.error("Failed to send request via all proxies: %s", self._proxy)
249
- raise RuntimeError("Failed to send request to {}".format(url))
235
+ # 直接传递代理列表给request函数
236
+ response = request(method, url, body_data, headers=headers, proxies=self._proxy, verify=self._ssl, retries=2)
250
237
  # 处理响应
251
238
  status_code = response.status
252
239
  if not (200 <= status_code < 300):
@@ -257,7 +244,7 @@ class SimpleProvider(object):
257
244
  if status_code >= 500 or status_code in (400, 401, 403):
258
245
  self.logger.error("HTTP error:\n%s", res)
259
246
  if status_code == 400:
260
- raise RuntimeError("请求参数错误 [400]: " + response.reason)
247
+ raise RuntimeError("参数错误 [400]: " + response.reason)
261
248
  elif status_code == 401:
262
249
  raise RuntimeError("认证失败 [401]: " + response.reason)
263
250
  elif status_code == 403:
ddns/provider/alidns.py CHANGED
@@ -5,9 +5,10 @@ AliDNS API
5
5
  @author: NewFuture
6
6
  """
7
7
 
8
- from ._base import TYPE_FORM, BaseProvider, join_domain, encode_params
8
+ from time import gmtime, strftime, time
9
+
10
+ from ._base import TYPE_FORM, BaseProvider, encode_params, join_domain
9
11
  from ._signature import hmac_sha256_authorization, sha256_hash
10
- from time import strftime, gmtime, time
11
12
 
12
13
 
13
14
  class AliBaseProvider(BaseProvider):
@@ -116,7 +117,7 @@ class AlidnsProvider(AliBaseProvider):
116
117
  TTL=ttl,
117
118
  Line=line,
118
119
  **extra
119
- )
120
+ ) # fmt: skip
120
121
  if data and data.get("RecordId"):
121
122
  self.logger.info("Record created: %s", data)
122
123
  return True
@@ -143,7 +144,7 @@ class AlidnsProvider(AliBaseProvider):
143
144
  TTL=ttl,
144
145
  Line=line or old_record.get("Line"),
145
146
  **extra
146
- )
147
+ ) # fmt: skip
147
148
  if data and data.get("RecordId"):
148
149
  self.logger.info("Record updated: %s", data)
149
150
  return True
ddns/provider/aliesa.py CHANGED
@@ -5,10 +5,11 @@ AliESA API
5
5
  @author: NewFuture, GitHub Copilot
6
6
  """
7
7
 
8
- from .alidns import AliBaseProvider
9
- from ._base import join_domain, TYPE_JSON
10
8
  from time import strftime
11
9
 
10
+ from ._base import TYPE_JSON, join_domain
11
+ from .alidns import AliBaseProvider
12
+
12
13
 
13
14
  class AliesaProvider(AliBaseProvider):
14
15
  """阿里云边缘安全加速(ESA) DNS Provider"""
@@ -82,7 +83,7 @@ class AliesaProvider(AliBaseProvider):
82
83
  Data={"Value": value},
83
84
  Ttl=ttl or 1,
84
85
  **extra
85
- )
86
+ ) # fmt: skip
86
87
 
87
88
  if data and data.get("RecordId"):
88
89
  self.logger.info("Record created: %s", data)
@@ -115,7 +116,7 @@ class AliesaProvider(AliBaseProvider):
115
116
  Data={"Value": value},
116
117
  Ttl=ttl,
117
118
  **extra
118
- )
119
+ ) # fmt: skip
119
120
 
120
121
  if data and data.get("RecordId"):
121
122
  self.logger.info("Record updated: %s", data)
ddns/provider/callback.py CHANGED
@@ -5,6 +5,7 @@ Custom Callback API
5
5
 
6
6
  @author: 老周部落, NewFuture
7
7
  """
8
+
8
9
  from ._base import TYPE_JSON, SimpleProvider
9
10
  from time import time
10
11
  from json import loads as jsondecode
@@ -4,7 +4,7 @@ CloudFlare API
4
4
  @author: TongYifan, NewFuture
5
5
  """
6
6
 
7
- from ._base import BaseProvider, TYPE_JSON, join_domain
7
+ from ._base import TYPE_JSON, BaseProvider, join_domain
8
8
 
9
9
 
10
10
  class CloudflareProvider(BaseProvider):
@@ -92,7 +92,7 @@ class CloudflareProvider(BaseProvider):
92
92
  content=value,
93
93
  ttl=ttl,
94
94
  **extra
95
- )
95
+ ) # fmt: skip
96
96
  self.logger.debug("Record updated: %s", data)
97
97
  if data:
98
98
  return True
ddns/provider/debug.py CHANGED
@@ -8,7 +8,6 @@ from ._base import SimpleProvider
8
8
 
9
9
 
10
10
  class DebugProvider(SimpleProvider):
11
-
12
11
  def _validate(self):
13
12
  """无需任何验证"""
14
13
  pass
ddns/provider/dnscom.py CHANGED
@@ -5,10 +5,11 @@ www.51dns.com (原dns.com)
5
5
  @author: Bigjin<i@bigjin.com>, NewFuture
6
6
  """
7
7
 
8
- from ._base import BaseProvider, TYPE_FORM, encode_params
9
8
  from hashlib import md5
10
9
  from time import time
11
10
 
11
+ from ._base import TYPE_FORM, BaseProvider, encode_params
12
+
12
13
 
13
14
  class DnscomProvider(BaseProvider):
14
15
  """
@@ -82,7 +83,7 @@ class DnscomProvider(BaseProvider):
82
83
  TTL=ttl,
83
84
  viewID=line,
84
85
  **extra
85
- )
86
+ ) # fmt: skip
86
87
  if res and res.get("recordID"):
87
88
  self.logger.info("Record created: %s", res)
88
89
  return True
ddns/provider/dnspod.py CHANGED
@@ -101,9 +101,7 @@ class DnspodProvider(BaseProvider):
101
101
  def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
102
102
  # type: (str, str, str, str, str | None, dict) -> dict | None
103
103
  """查询记录 list 然后逐个查找 https://docs.dnspod.cn/api/record-list/"""
104
- res = self._request(
105
- "Record.List", domain_id=zone_id, sub_domain=subdomain, record_type=record_type, line=line
106
- )
104
+ res = self._request("Record.List", domain_id=zone_id, sub_domain=subdomain, record_type=record_type, line=line)
107
105
  # length="3000"
108
106
  records = res.get("records", [])
109
107
  n = len(records)
ddns/provider/edgeone.py CHANGED
@@ -5,6 +5,7 @@ Tencent Cloud EdgeOne API
5
5
  API Documentation: https://cloud.tencent.com/document/api/1552/80731
6
6
  @author: NewFuture
7
7
  """
8
+
8
9
  from ddns.provider._base import join_domain
9
10
  from .tencentcloud import TencentCloudProvider
10
11
 
@@ -5,9 +5,10 @@ HuaweiDNS API
5
5
  @author: NewFuture
6
6
  """
7
7
 
8
- from ._base import BaseProvider, TYPE_JSON, join_domain, encode_params
8
+ from time import gmtime, strftime
9
+
10
+ from ._base import TYPE_JSON, BaseProvider, encode_params, join_domain
9
11
  from ._signature import hmac_sha256_authorization, sha256_hash
10
- from time import strftime, gmtime
11
12
 
12
13
 
13
14
  class HuaweiDNSProvider(BaseProvider):
@@ -114,7 +115,7 @@ class HuaweiDNSProvider(BaseProvider):
114
115
  ttl=ttl,
115
116
  line=line,
116
117
  **extra
117
- )
118
+ ) # fmt: skip
118
119
  if res and res.get("id"):
119
120
  self.logger.info("Record created: %s", res)
120
121
  return True
@@ -134,7 +135,7 @@ class HuaweiDNSProvider(BaseProvider):
134
135
  records=[value],
135
136
  ttl=ttl if ttl is not None else old_record.get("ttl"),
136
137
  **extra
137
- )
138
+ ) # fmt: skip
138
139
  if res and res.get("id"):
139
140
  self.logger.info("Record updated: %s", res)
140
141
  return True
ddns/provider/noip.py CHANGED
@@ -4,8 +4,7 @@ No-IP (noip.com) Dynamic DNS API
4
4
  @author: GitHub Copilot
5
5
  """
6
6
 
7
- import base64
8
- from ._base import SimpleProvider, TYPE_FORM
7
+ from ._base import SimpleProvider, TYPE_FORM, quote
9
8
 
10
9
 
11
10
  class NoipProvider(SimpleProvider):
@@ -24,13 +23,23 @@ class NoipProvider(SimpleProvider):
24
23
 
25
24
  def _validate(self):
26
25
  """
27
- Validate authentication credentials for No-IP
26
+ Validate authentication credentials for No-IP and update endpoint with auth
28
27
  """
28
+ # Check endpoint first
29
+ if not self.endpoint or "://" not in self.endpoint:
30
+ raise ValueError("API endpoint must be defined and contain protocol")
31
+
29
32
  if not self.id:
30
33
  raise ValueError("No-IP requires username as 'id'")
31
34
  if not self.token:
32
35
  raise ValueError("No-IP requires password as 'token'")
33
36
 
37
+ # Update endpoint with URL-encoded auth credentials
38
+ protocol, domain = self.endpoint.split("://", 1)
39
+ self.endpoint = "{0}://{1}:{2}@{3}".format(
40
+ protocol, quote(self.id, safe=""), quote(self.token, safe=""), domain
41
+ )
42
+
34
43
  def set_record(self, domain, value, record_type="A", ttl=None, line=None, **extra):
35
44
  """
36
45
  Update DNS record using No-IP Dynamic Update API
@@ -59,21 +68,10 @@ class NoipProvider(SimpleProvider):
59
68
  # Prepare request parameters
60
69
  params = {"hostname": domain, "myip": value}
61
70
 
62
- # Prepare HTTP Basic Authentication headers
63
- auth_string = "{0}:{1}".format(self.id, self.token)
64
- if not isinstance(auth_string, bytes): # Python 3
65
- auth_bytes = auth_string.encode("utf-8")
66
- else: # Python 2
67
- auth_bytes = auth_string
68
-
69
- auth_b64 = base64.b64encode(auth_bytes).decode("ascii")
70
- headers = {
71
- "Authorization": "Basic {0}".format(auth_b64),
72
- }
73
-
74
71
  try:
75
72
  # Use GET request as it's the most common method for DDNS
76
- response = self._http("GET", "/nic/update", queries=params, headers=headers)
73
+ # Endpoint already includes auth credentials from _validate()
74
+ response = self._http("GET", "/nic/update", queries=params)
77
75
 
78
76
  if response is not None:
79
77
  response_str = str(response).strip()
@@ -5,9 +5,11 @@ Tencent Cloud DNSPod API
5
5
 
6
6
  @author: NewFuture
7
7
  """
8
- from ._base import BaseProvider, TYPE_JSON
9
- from ._signature import hmac_sha256_authorization, sha256_hash, hmac_sha256
10
- from time import time, strftime, gmtime
8
+
9
+ from time import gmtime, strftime, time
10
+
11
+ from ._base import TYPE_JSON, BaseProvider
12
+ from ._signature import hmac_sha256, hmac_sha256_authorization, sha256_hash
11
13
 
12
14
 
13
15
  class TencentCloudProvider(BaseProvider):
@@ -45,10 +47,7 @@ class TencentCloudProvider(BaseProvider):
45
47
  body = self._encode_body(params)
46
48
 
47
49
  # 构建请求头,小写 腾讯云只签名特定头部
48
- headers = {
49
- "content-type": self.content_type,
50
- "host": self.endpoint.split("://", 1)[1].strip("/"),
51
- }
50
+ headers = {"content-type": self.content_type, "host": self.endpoint.split("://", 1)[1].strip("/")}
52
51
 
53
52
  # 腾讯云特殊的密钥派生过程
54
53
  date = strftime("%Y-%m-%d", gmtime())
@@ -132,7 +131,7 @@ class TencentCloudProvider(BaseProvider):
132
131
  RecordType=record_type,
133
132
  RecordLine=line,
134
133
  **extra
135
- )
134
+ ) # fmt: skip
136
135
  if not response or "RecordList" not in response:
137
136
  self.logger.debug("No records found or query failed")
138
137
  return None
@@ -165,7 +164,7 @@ class TencentCloudProvider(BaseProvider):
165
164
  RecordLine=line or "默认",
166
165
  TTL=int(ttl) if ttl else None,
167
166
  **extra
168
- )
167
+ ) # fmt: skip
169
168
  if response and "RecordId" in response:
170
169
  self.logger.info("Record created successfully with ID: %s", response["RecordId"])
171
170
  return True
@@ -186,7 +185,7 @@ class TencentCloudProvider(BaseProvider):
186
185
  Value=value,
187
186
  TTL=int(ttl) if ttl else None,
188
187
  **extra
189
- )
188
+ ) # fmt: skip
190
189
  if response and "RecordId" in response:
191
190
  self.logger.info("Record updated successfully")
192
191
  return True
ddns/util/fileio.py ADDED
@@ -0,0 +1,113 @@
1
+ # -*- coding:utf-8 -*-
2
+ """
3
+ File I/O utilities for DDNS with Python 2/3 compatibility
4
+ @author: NewFuture
5
+ """
6
+
7
+ import os
8
+ from io import open # Python 2/3 compatible UTF-8 file operations
9
+
10
+
11
+ def _ensure_directory_exists(file_path): # type: (str) -> None
12
+ """
13
+ Internal helper to ensure directory exists for the given file path
14
+
15
+ Args:
16
+ file_path (str): File path whose directory should be created
17
+
18
+ Raises:
19
+ OSError: If directory cannot be created
20
+ """
21
+ directory = os.path.dirname(file_path)
22
+ if directory and not os.path.exists(directory):
23
+ os.makedirs(directory)
24
+
25
+
26
+ def read_file_safely(file_path, encoding="utf-8", default=None): # type: (str, str, str|None) -> str | None
27
+ """
28
+ Safely read file content with UTF-8 encoding, return None if file doesn't exist or can't be read
29
+
30
+ Args:
31
+ file_path (str): Path to the file to read
32
+ encoding (str): File encoding (default: utf-8)
33
+
34
+ Returns:
35
+ str | None: File content or None if failed
36
+ """
37
+ try:
38
+ return read_file(file_path, encoding)
39
+ except Exception:
40
+ return default
41
+
42
+
43
+ def write_file_safely(file_path, content, encoding="utf-8"): # type: (str, str, str) -> bool
44
+ """
45
+ Safely write content to file with UTF-8 encoding
46
+
47
+ Args:
48
+ file_path (str): Path to the file to write
49
+ content (str): Content to write
50
+ encoding (str): File encoding (default: utf-8)
51
+
52
+ Returns:
53
+ bool: True if write successful, False otherwise
54
+ """
55
+ try:
56
+ write_file(file_path, content, encoding)
57
+ return True
58
+ except Exception:
59
+ return False
60
+
61
+
62
+ def read_file(file_path, encoding="utf-8"): # type: (str, str) -> str
63
+ """
64
+ Read file content with UTF-8 encoding, raise exception if failed
65
+
66
+ Args:
67
+ file_path (str): Path to the file to read
68
+ encoding (str): File encoding (default: utf-8)
69
+
70
+ Returns:
71
+ str: File content
72
+
73
+ Raises:
74
+ IOError: If file cannot be read
75
+ UnicodeDecodeError: If file cannot be decoded with specified encoding
76
+ """
77
+ with open(file_path, "r", encoding=encoding) as f:
78
+ return f.read()
79
+
80
+
81
+ def write_file(file_path, content, encoding="utf-8"): # type: (str, str, str) -> None
82
+ """
83
+ Write content to file with UTF-8 encoding, raise exception if failed
84
+
85
+ Args:
86
+ file_path (str): Path to the file to write
87
+ content (str): Content to write
88
+ encoding (str): File encoding (default: utf-8)
89
+
90
+ Raises:
91
+ IOError: If file cannot be written
92
+ UnicodeEncodeError: If content cannot be encoded with specified encoding
93
+ """
94
+ _ensure_directory_exists(file_path)
95
+ with open(file_path, "w", encoding=encoding) as f:
96
+ f.write(content)
97
+
98
+
99
+ def ensure_directory(file_path): # type: (str) -> bool
100
+ """
101
+ Ensure the directory for the given file path exists
102
+
103
+ Args:
104
+ file_path (str): File path whose directory should be created
105
+
106
+ Returns:
107
+ bool: True if directory exists or was created successfully
108
+ """
109
+ try:
110
+ _ensure_directory_exists(file_path)
111
+ return True
112
+ except (OSError, IOError):
113
+ return False