pywaybackup 1.3.2__tar.gz → 1.4.0__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 (33) hide show
  1. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/.github/ISSUE_TEMPLATE/bug.md +3 -1
  2. {pywaybackup-1.3.2/pywaybackup.egg-info → pywaybackup-1.4.0}/PKG-INFO +14 -4
  3. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/README.md +13 -3
  4. pywaybackup-1.4.0/pywaybackup/Arguments.py +102 -0
  5. pywaybackup-1.4.0/pywaybackup/Converter.py +182 -0
  6. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/pywaybackup/SnapshotCollection.py +2 -2
  7. pywaybackup-1.4.0/pywaybackup/Verbosity.py +114 -0
  8. pywaybackup-1.4.0/pywaybackup/__version__.py +1 -0
  9. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/pywaybackup/archive.py +132 -133
  10. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/pywaybackup/helper.py +20 -5
  11. pywaybackup-1.4.0/pywaybackup/main.py +37 -0
  12. {pywaybackup-1.3.2 → pywaybackup-1.4.0/pywaybackup.egg-info}/PKG-INFO +14 -4
  13. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/pywaybackup.egg-info/SOURCES.txt +2 -1
  14. pywaybackup-1.3.2/pywaybackup/Verbosity.py +0 -50
  15. pywaybackup-1.3.2/pywaybackup/__version__.py +0 -1
  16. pywaybackup-1.3.2/pywaybackup/arguments.py +0 -45
  17. pywaybackup-1.3.2/pywaybackup/main.py +0 -63
  18. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/.gitignore +0 -0
  19. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/LICENSE +0 -0
  20. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/dev/pip_build.sh +0 -0
  21. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/dev/venv_create.sh +0 -0
  22. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/pywaybackup/Exception.py +0 -0
  23. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/pywaybackup/__init__.py +0 -0
  24. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/pywaybackup.egg-info/dependency_links.txt +0 -0
  25. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/pywaybackup.egg-info/entry_points.txt +0 -0
  26. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/pywaybackup.egg-info/requires.txt +0 -0
  27. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/pywaybackup.egg-info/top_level.txt +0 -0
  28. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/requirements.txt +0 -0
  29. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/setup.cfg +0 -0
  30. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/setup.py +0 -0
  31. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/test/cdx.example.com.200 +0 -0
  32. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/test/cdx.example.com.404 +0 -0
  33. {pywaybackup-1.3.2 → pywaybackup-1.4.0}/test/cdx.example.com.small.200 +0 -0
@@ -1,12 +1,14 @@
1
1
  ---
2
2
  name: Bug
3
3
  about: Describe a faulty behavior
4
- title: ''
4
+ title: "[BUG]"
5
5
  labels: bug
6
6
  assignees: ''
7
7
 
8
8
  ---
9
9
 
10
+ <ins><b>Please select the according OS-Label</b></ins>
11
+
10
12
  **Describe**
11
13
  Describe the bug / unexpected event
12
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pywaybackup
3
- Version: 1.3.2
3
+ Version: 1.4.0
4
4
  Summary: Download snapshots from the Wayback Machine
5
5
  Home-page: https://github.com/bitdruid/python-wayback-machine-downloader
6
6
  Author: bitdruid
@@ -99,12 +99,22 @@ Disables following redirects of snapshots. Useful for preventing timestamp-folde
99
99
 
100
100
  - **`--verbosity`** `<level>`:<br>
101
101
  Sets verbosity level. Options are `json` (prints JSON response) or `progress` (shows progress bar).
102
+ <!-- Alternatively set verbosity level to `trace` for a very detailed output. -->
103
+
104
+ - **`--log`** `<path>`:<br>
105
+ Path defaults to output-dir. Saves a log file with the output of the tool. Named as `waybackup_<sanitized_url>.log`.
106
+
107
+ - **`--workers`** `<count>`:<br>
108
+ Sets the number of simultaneous download workers. Default is 1, safe range is about 10. Be cautious as too many workers may lead to refused connections from the Wayback Machine.
102
109
 
103
110
  - **`--retry`** `<attempts>`:<br>
104
111
  Specifies number of retry attempts for failed downloads.
105
-
106
- - **`--workers`** `<count>`:<br>
107
- Sets the number of simultaneous download workers. Default is 1, safe range is about 10. Be cautious as too many workers may lead to refused connections from the Wayback Machine.
112
+
113
+ - **`--delay`** `<seconds>`:<br>
114
+ Specifies delay between download requests in seconds. Default is no delay (0).
115
+
116
+ <!-- - **`--convert-links`**:<br>
117
+ If set, all links in the downloaded files will be converted to local links. This is useful for offline browsing. The links are converted to the local path structure. Show output with `--verbosity trace`. -->
108
118
 
109
119
  **CDX Query Handling:**
110
120
  - **`--cdxbackup`** `<path>`:<br>
@@ -83,12 +83,22 @@ Disables following redirects of snapshots. Useful for preventing timestamp-folde
83
83
 
84
84
  - **`--verbosity`** `<level>`:<br>
85
85
  Sets verbosity level. Options are `json` (prints JSON response) or `progress` (shows progress bar).
86
+ <!-- Alternatively set verbosity level to `trace` for a very detailed output. -->
87
+
88
+ - **`--log`** `<path>`:<br>
89
+ Path defaults to output-dir. Saves a log file with the output of the tool. Named as `waybackup_<sanitized_url>.log`.
90
+
91
+ - **`--workers`** `<count>`:<br>
92
+ Sets the number of simultaneous download workers. Default is 1, safe range is about 10. Be cautious as too many workers may lead to refused connections from the Wayback Machine.
86
93
 
87
94
  - **`--retry`** `<attempts>`:<br>
88
95
  Specifies number of retry attempts for failed downloads.
89
-
90
- - **`--workers`** `<count>`:<br>
91
- Sets the number of simultaneous download workers. Default is 1, safe range is about 10. Be cautious as too many workers may lead to refused connections from the Wayback Machine.
96
+
97
+ - **`--delay`** `<seconds>`:<br>
98
+ Specifies delay between download requests in seconds. Default is no delay (0).
99
+
100
+ <!-- - **`--convert-links`**:<br>
101
+ If set, all links in the downloaded files will be converted to local links. This is useful for offline browsing. The links are converted to the local path structure. Show output with `--verbosity trace`. -->
92
102
 
93
103
  **CDX Query Handling:**
94
104
  - **`--cdxbackup`** `<path>`:<br>
@@ -0,0 +1,102 @@
1
+
2
+ import sys
3
+ import os
4
+ import argparse
5
+
6
+ from pywaybackup.helper import url_split, sanitize_filename
7
+
8
+ from pywaybackup.__version__ import __version__
9
+
10
+ class Arguments:
11
+
12
+ def __init__(self):
13
+
14
+ parser = argparse.ArgumentParser(description='Download from wayback machine (archive.org)')
15
+ parser.add_argument('-a', '--about', action='version', version='%(prog)s ' + __version__ + ' by @bitdruid -> https://github.com/bitdruid')
16
+ parser.add_argument('-d', '--debug', action='store_true', help='Debug mode (Always full traceback and creates an error.log')
17
+
18
+ required = parser.add_argument_group('required (one exclusive)')
19
+ required.add_argument('-u', '--url', type=str, metavar="", help='url (with subdir/subdomain) to download')
20
+ exclusive_required = required.add_mutually_exclusive_group(required=True)
21
+ exclusive_required.add_argument('-c', '--current', action='store_true', help='download the latest version of each file snapshot')
22
+ exclusive_required.add_argument('-f', '--full', action='store_true', help='download snapshots of all timestamps')
23
+ exclusive_required.add_argument('-s', '--save', action='store_true', help='save a page to the wayback machine')
24
+
25
+ optional = parser.add_argument_group('optional query parameters')
26
+ optional.add_argument('-l', '--list', action='store_true', help='only print snapshots (opt range in y)')
27
+ optional.add_argument('-e', '--explicit', action='store_true', help='search only for the explicit given url')
28
+ optional.add_argument('-o', '--output', type=str, metavar="", help='output folder - defaults to current directory')
29
+ optional.add_argument('-r', '--range', type=int, metavar="", help='range in years to search')
30
+ optional.add_argument('--start', type=int, metavar="", help='start timestamp format: YYYYMMDDhhmmss')
31
+ optional.add_argument('--end', type=int, metavar="", help='end timestamp format: YYYYMMDDhhmmss')
32
+
33
+ special = parser.add_argument_group('manipulate behavior')
34
+ special.add_argument('--csv', type=str, nargs='?', const=True, metavar='path', help='save a csv file with the json output - defaults to output folder')
35
+ special.add_argument('--skip', type=str, nargs='?', const=True, metavar='path', help='skips existing files in the output folder by checking the .csv file - defaults to output folder')
36
+ special.add_argument('--no-redirect', action='store_true', help='do not follow redirects by archive.org')
37
+ special.add_argument('--verbosity', type=str, default="info", metavar="", help='["progress", "json"] for different output or ["trace"] for very detailed output')
38
+ special.add_argument('--log', type=str, nargs='?', const=True, metavar='path', help='save a log file - defaults to output folder')
39
+ special.add_argument('--retry', type=int, default=0, metavar="", help='retry failed downloads (opt tries as int, else infinite)')
40
+ special.add_argument('--workers', type=int, default=1, metavar="", help='number of workers (simultaneous downloads)')
41
+ # special.add_argument('--convert-links', action='store_true', help='Convert all links in the files to local paths. Requires -c/--current')
42
+ special.add_argument('--delay', type=int, default=0, metavar="", help='delay between each download in seconds')
43
+
44
+ cdx = parser.add_argument_group('cdx (one exclusive)')
45
+ exclusive_cdx = cdx.add_mutually_exclusive_group()
46
+ exclusive_cdx.add_argument('--cdxbackup', type=str, nargs='?', const=True, metavar='path', help='Save the cdx query-result to a file for recurent use - defaults to output folder')
47
+ exclusive_cdx.add_argument('--cdxinject', type=str, nargs='?', const=True, metavar='path', help='Inject a cdx backup-file to download according to the given url')
48
+
49
+ auto = parser.add_argument_group('auto')
50
+ auto.add_argument('--auto', action='store_true', help='includes automatic csv, skip and cdxbackup/cdxinject to resume a stopped download')
51
+
52
+ args = parser.parse_args(args=None if sys.argv[1:] else ['--help']) # if no arguments are given, print help
53
+
54
+ # if args.convert_links and not args.current:
55
+ # parser.error("--convert-links can only be used with the -c/--current option")
56
+
57
+ self.args = args
58
+
59
+ def get_args(self):
60
+ return self.args
61
+
62
+ class Configuration:
63
+
64
+ @classmethod
65
+ def init(cls):
66
+
67
+ cls.args = Arguments().get_args()
68
+ for key, value in vars(cls.args).items():
69
+ setattr(Configuration, key, value)
70
+
71
+ # args now attributes of Configuration // Configuration.output, ...
72
+ cls.command = ' '.join(sys.argv[1:])
73
+ cls.domain, cls.subdir, cls.filename = url_split(cls.url)
74
+
75
+ if cls.output is None:
76
+ cls.output = os.path.join(os.getcwd(), "waybackup_snapshots")
77
+ os.makedirs(cls.output, exist_ok=True)
78
+
79
+ if cls.log is True:
80
+ cls.log = os.path.join(cls.output, f"waybackup_{sanitize_filename(cls.url)}.log")
81
+
82
+ if cls.full:
83
+ cls.mode = "full"
84
+ if cls.current:
85
+ cls.mode = "current"
86
+
87
+ if cls.auto:
88
+ cls.skip = cls.output
89
+ cls.csv = cls.output
90
+ cls.cdxbackup = cls.output
91
+ cls.cdxinject = os.path.join(cls.output, f"waybackup_{sanitize_filename(cls.url)}.cdx")
92
+ else:
93
+ if cls.skip is True:
94
+ cls.skip = cls.output
95
+ if cls.csv is True:
96
+ cls.csv = cls.output
97
+ if cls.cdxbackup is True:
98
+ cls.cdxbackup = cls.output
99
+ if cls.cdxinject is True:
100
+ cls.cdxinject = cls.output
101
+
102
+
@@ -0,0 +1,182 @@
1
+ import os
2
+ import errno
3
+ import magic
4
+ from pywaybackup.helper import url_split
5
+
6
+ from pywaybackup.Arguments import Configuration as config
7
+ from pywaybackup.Verbosity import Verbosity as vb
8
+ import re
9
+
10
+ class Converter:
11
+
12
+ @classmethod
13
+ def define_root_steps(cls, filepath) -> str:
14
+ """
15
+ Define the steps (../) to the root directory.
16
+ """
17
+ abs_path = os.path.abspath(filepath)
18
+ webroot_path = os.path.abspath(f"{config.output}/{config.domain}/") # webroot is the domain folder in the output
19
+ # common path between the two
20
+ common_path = os.path.commonpath([abs_path, webroot_path])
21
+ # steps up to the common path
22
+ rel_path_from_common = os.path.relpath(abs_path, common_path)
23
+ steps_up = rel_path_from_common.count(os.path.sep)
24
+ if steps_up <= 1: # if the file is in the root of the domain
25
+ return "./"
26
+ return "../" * steps_up
27
+
28
+
29
+
30
+
31
+
32
+ @classmethod
33
+ def links(cls, filepath, status_message=None):
34
+ """
35
+ Convert all links in a HTML / CSS / JS file to local paths.
36
+ """
37
+
38
+
39
+ def extract_urls(content) -> list:
40
+ """
41
+ Extract all links from a file.
42
+ """
43
+
44
+ #content = re.sub(r'\s+', '', content)
45
+ #content = re.sub(r'\n', '', content)
46
+
47
+ html_types = ["src", "href", "poster", "data-src"]
48
+ css_types = ["url"]
49
+ links = []
50
+ for html_type in html_types:
51
+ # possible formatings of the value: "url", 'url', url
52
+ matches = re.findall(f"{html_type}=[\"']?([^\"'>]+)", content)
53
+ links += matches
54
+ for css_type in css_types:
55
+ # possible formatings of the value: url(url) url('url') url("url") // ends with )
56
+ matches = re.findall(rf"{css_type}\((['\"]?)([^'\"\)]+)\1\)", content)
57
+ links += [match[1] for match in matches]
58
+ links = list(set(links))
59
+ return links
60
+
61
+
62
+ def local_url(original_url, domain, count) -> str:
63
+ """
64
+ Convert a given url to a local path.
65
+ """
66
+ original_url_domain = url_split(original_url)[0]
67
+
68
+ # check if the url is external or internal (external is returned as is because no need to convert)
69
+ external = False
70
+ if original_url_domain != domain:
71
+ if "://" in original_url:
72
+ external = True
73
+ if original_url.startswith("//"):
74
+ external = True
75
+ if external:
76
+ status_message.trace(status="", type=f"{count}/{len(links)}", message="External url")
77
+ return original_url
78
+
79
+ # convert the url to a relative path to the local root (download dir) if it's a valid path, else return the original url
80
+ original_url_file = os.path.join(config.output, config.domain, normalize_url(original_url))
81
+ if validate_path(original_url_file):
82
+ if original_url.startswith("/"): # if only starts with /
83
+ original_url = f"{cls.define_root_steps(filepath)}{original_url.lstrip('/')}"
84
+ if original_url.startswith(".//"):
85
+ original_url = f"{cls.define_root_steps(filepath)}{original_url.lstrip('./')}"
86
+ if original_url_domain == domain: # if url is like https://domain.com/path/to/file
87
+ original_url = f"{cls.define_root_steps(filepath)}{original_url.split(domain)[1].lstrip('/')}"
88
+ if original_url.startswith("../"): # if file is already ../ check if it's not too many steps up
89
+ original_url = f"{cls.define_root_steps(filepath)}{original_url.split('../')[-1].lstrip('/')}"
90
+ else:
91
+ status_message.trace(status="", type="", message=f"{count}/{len(links)}: URL is not a valid path")
92
+
93
+ return original_url
94
+
95
+
96
+
97
+
98
+
99
+ def normalize_url(url) -> str:
100
+ """
101
+ Normalize a given url by removing it's protocol, domain and parent directorie references.
102
+
103
+ Example1:
104
+ - Example input: https://domain.com/path/to/file
105
+ - Example output: /path/to/file
106
+
107
+ Example2
108
+ - input: ../path/to/file
109
+ - output: /path/to/file
110
+ """
111
+ try:
112
+ url = "/" + url.split("../")[-1]
113
+ except IndexError:
114
+ pass
115
+ if url.startswith("//"):
116
+ url = "/" + url.split("//")[1]
117
+ parsed_url = url_split(url)
118
+ return f"{parsed_url[1]}/{parsed_url[2]}"
119
+
120
+
121
+ def is_pathname_valid(pathname: str) -> bool:
122
+ """
123
+ Check if a given pathname is valid.
124
+ """
125
+ if not isinstance(pathname, str) or not pathname:
126
+ return False
127
+
128
+ try:
129
+ os.lstat(pathname)
130
+ except OSError as exc:
131
+ if exc.errno == errno.ENOENT:
132
+ return True
133
+ elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}:
134
+ return False
135
+ return True
136
+
137
+ def is_path_creatable(pathname: str) -> bool:
138
+ """
139
+ Check if a given path is creatable.
140
+ """
141
+ dirname = os.path.dirname(pathname) or os.getcwd()
142
+ return os.access(dirname, os.W_OK)
143
+
144
+ def is_path_exists_or_creatable(pathname: str) -> bool:
145
+ """
146
+ Check if a given path exists or is creatable.
147
+ """
148
+ return is_pathname_valid(pathname) or is_path_creatable(pathname)
149
+
150
+ def validate_path(filepath: str) -> bool:
151
+ """
152
+ Validate if a given path can exist.
153
+ """
154
+ return is_path_exists_or_creatable(filepath)
155
+
156
+
157
+
158
+
159
+
160
+ if os.path.isfile(filepath):
161
+ if magic.from_file(filepath, mime=True).split("/")[1] == "javascript":
162
+ status_message.trace(status="Error", type="", message="JS-file is not supported")
163
+ return
164
+ try:
165
+ with open(filepath, "r") as file:
166
+ domain = config.domain
167
+ content = file.read()
168
+ links = extract_urls(content)
169
+ status_message.store(message=f"\n-----> Convert: [{len(links)}] links in file")
170
+ count = 1
171
+ for original_link in links:
172
+ status_message.trace(status="ORIG", type=f"{count}/{len(links)}", message=original_link)
173
+ new_link = local_url(original_link, domain, count)
174
+ if new_link != original_link:
175
+ status_message.trace(status="CONV", type=f"{count}/{len(links)}", message=new_link)
176
+ content = content.replace(original_link, new_link)
177
+ count += 1
178
+ file = open(filepath, "w")
179
+ file.write(content)
180
+ file.close()
181
+ except UnicodeDecodeError:
182
+ status_message.trace(status="Error", type="", message="Could not decode file to convert links")
@@ -70,10 +70,10 @@ class SnapshotCollection:
70
70
  download_dir = os.path.join(output, domain, timestamp, subdir)
71
71
  download_file = os.path.abspath(os.path.join(download_dir, filename))
72
72
  return download_file
73
-
73
+
74
74
 
75
75
  @classmethod
76
- def snapshot_entry_modify(cls, collection_entry: dict, key: str, value: str):
76
+ def entry_modify(cls, collection_entry: dict, key: str, value: str):
77
77
  """
78
78
  Modify a key-value pair in a snapshot entry of the collection (dict).
79
79
 
@@ -0,0 +1,114 @@
1
+ import tqdm
2
+ import json
3
+ from pywaybackup.SnapshotCollection import SnapshotCollection as sc
4
+
5
+ class Verbosity:
6
+
7
+ LEVELS = ["trace", "info"]
8
+ level = None
9
+
10
+ mode = None
11
+ args = None
12
+ pbar = None
13
+
14
+ log = None
15
+
16
+ @classmethod
17
+ def init(cls, v_args: list, log=None):
18
+ cls.args = v_args
19
+ cls.log = open(log, "w") if log else None
20
+ if cls.args == "progress":
21
+ cls.mode = "progress"
22
+ elif cls.args == "json":
23
+ cls.mode = "json"
24
+ cls.level = cls.args if cls.args in cls.LEVELS else "info"
25
+
26
+ @classmethod
27
+ def fini(cls):
28
+ if cls.mode == "progress":
29
+ if cls.pbar is not None:
30
+ cls.pbar.close()
31
+ if cls.mode == "json":
32
+ print(json.dumps(sc.SNAPSHOT_COLLECTION, indent=4, sort_keys=True))
33
+ if cls.log:
34
+ cls.log.close()
35
+
36
+ @classmethod
37
+ def write(cls, status="", type="", message=""):
38
+ """
39
+ Write a log line based on the provided status, type, and message.
40
+
41
+ Args:
42
+ status (str): The status of the log line. (e.g. "SUCCESS", "REDIRECT")
43
+ type (str): The type of the log line. (e.g. "URL", "FILE")
44
+ message (str): The message to be logged. (e.g. actual url, file path)
45
+ """
46
+ logline = cls.generate_logline(status=status, type=type, message=message)
47
+ if cls.mode != "progress" and cls.mode != "json":
48
+ if logline:
49
+ print(logline)
50
+ if cls.log:
51
+ cls.log.write(logline + "\n")
52
+ cls.log.flush()
53
+
54
+ @classmethod
55
+ def progress(cls, progress: int):
56
+ if cls.mode == "progress":
57
+ if cls.pbar is None and progress == 0:
58
+ maxval = sc.count(collection=True)
59
+ cls.pbar = tqdm.tqdm(total=maxval, desc="Downloading", unit=" snapshot", ascii="░▒█")
60
+ if cls.pbar is not None and progress is not None and progress > 0:
61
+ cls.pbar.update(progress)
62
+ cls.pbar.refresh()
63
+
64
+ @classmethod
65
+ def generate_logline(cls, status: str = "", type: str = "", message: str = ""):
66
+
67
+ if not status and not type:
68
+ return message
69
+
70
+ status_length = 11
71
+ type_length = 5
72
+
73
+ status = status.ljust(status_length)
74
+ type = type.ljust(type_length)
75
+
76
+ log_entry = f"{status} -> {type}: {message}"
77
+
78
+ return log_entry
79
+
80
+ class Message(Verbosity):
81
+ """
82
+ Message class representing a message-buffer for the Verbosity class.
83
+
84
+ If a message should be stored and stacked for later output.
85
+ """
86
+
87
+ def __init__(self):
88
+ self.message = {}
89
+
90
+ def __str__(self):
91
+ return self.message
92
+
93
+ def store(self, status: str = "", type: str = "", message: str = "", level: str = "info"):
94
+ if level not in self.message:
95
+ self.message[level] = []
96
+ self.message[level].append(super().generate_logline(status, type, message))
97
+
98
+ def clear(self):
99
+ self.message = {}
100
+
101
+ def write(self):
102
+ for level in self.message:
103
+ if self.check_level(level):
104
+ for message in self.message[level]:
105
+ super().write(message=message)
106
+ self.clear()
107
+
108
+ def check_level(self, level: str):
109
+ return super().LEVELS.index(level) >= super().LEVELS.index(self.level)
110
+
111
+ def trace(self, status: str = "", type: str = "", message: str = ""):
112
+ self.store(status, type, message, "trace")
113
+
114
+
@@ -0,0 +1 @@
1
+ __version__ = "1.4.0"