oafuncs 0.0.98.4__tar.gz → 0.0.98.5__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.
- {oafuncs-0.0.98.4/oafuncs.egg-info → oafuncs-0.0.98.5}/PKG-INFO +1 -1
- oafuncs-0.0.98.4/oafuncs/oa_down/hycom_3hourly_20250416.py → oafuncs-0.0.98.5/oafuncs/oa_down/hycom_3hourly.py +111 -102
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5/oafuncs.egg-info}/PKG-INFO +1 -1
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs.egg-info/SOURCES.txt +0 -2
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/setup.py +1 -1
- oafuncs-0.0.98.4/oafuncs/oa_down/hycom_3hourly.py +0 -1216
- oafuncs-0.0.98.4/oafuncs/oa_down/hycom_3hourly_20250407.py +0 -1295
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/LICENSE.txt +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/MANIFEST.in +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/README.md +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/__init__.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/_data/hycom.png +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/_data/oafuncs.png +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/_script/cprogressbar.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/_script/email.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/_script/netcdf_merge.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/_script/netcdf_modify.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/_script/netcdf_write.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/_script/parallel.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/_script/parallel_test.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/_script/plot_dataset.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/_script/replace_file_content.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_cmap.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_data.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_date.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_down/User_Agent-list.txt +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_down/__init__.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_down/idm.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_down/literature.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_down/test_ua.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_down/user_agent.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_draw.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_file.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_help.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_model/__init__.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_model/roms/__init__.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_model/roms/test.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_model/wrf/__init__.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_model/wrf/little_r.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_nc.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_python.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_sign/__init__.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_sign/meteorological.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_sign/ocean.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_sign/scientific.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs/oa_tool.py +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs.egg-info/dependency_links.txt +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs.egg-info/requires.txt +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/oafuncs.egg-info/top_level.txt +0 -0
- {oafuncs-0.0.98.4 → oafuncs-0.0.98.5}/setup.cfg +0 -0
@@ -2,9 +2,9 @@
|
|
2
2
|
# coding=utf-8
|
3
3
|
"""
|
4
4
|
Author: Liu Kun && 16031215@qq.com
|
5
|
-
Date: 2025-04-
|
5
|
+
Date: 2025-04-07 10:51:09
|
6
6
|
LastEditors: Liu Kun && 16031215@qq.com
|
7
|
-
LastEditTime: 2025-04-
|
7
|
+
LastEditTime: 2025-04-07 10:51:09
|
8
8
|
FilePath: \\Python\\My_Funcs\\OAFuncs\\oafuncs\\oa_down\\hycom_3hourly copy.py
|
9
9
|
Description:
|
10
10
|
EditPlatform: vscode
|
@@ -13,9 +13,9 @@ SystemInfo: Windows 11
|
|
13
13
|
Python Version: 3.12
|
14
14
|
"""
|
15
15
|
|
16
|
-
|
17
|
-
|
16
|
+
import asyncio
|
18
17
|
import datetime
|
18
|
+
import logging
|
19
19
|
import os
|
20
20
|
import random
|
21
21
|
import re
|
@@ -25,11 +25,11 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
25
25
|
from pathlib import Path
|
26
26
|
from threading import Lock
|
27
27
|
|
28
|
+
import httpx
|
28
29
|
import matplotlib.pyplot as plt
|
29
30
|
import netCDF4 as nc
|
30
31
|
import numpy as np
|
31
32
|
import pandas as pd
|
32
|
-
import requests
|
33
33
|
import xarray as xr
|
34
34
|
from rich import print
|
35
35
|
from rich.progress import Progress
|
@@ -40,6 +40,9 @@ from oafuncs.oa_file import file_size
|
|
40
40
|
from oafuncs.oa_nc import check as check_nc
|
41
41
|
from oafuncs.oa_nc import modify as modify_nc
|
42
42
|
|
43
|
+
logging.getLogger("httpx").setLevel(logging.WARNING) # 关闭 httpx 的 INFO 日志,只显示 WARNING 及以上
|
44
|
+
|
45
|
+
|
43
46
|
warnings.filterwarnings("ignore", category=RuntimeWarning, message="Engine '.*' loading failed:.*")
|
44
47
|
|
45
48
|
__all__ = ["draw_time_range", "download"]
|
@@ -416,13 +419,13 @@ def _check_time_in_dataset_and_version(time_input, time_end=None):
|
|
416
419
|
trange_list.append(f"{time_s}-{time_e}")
|
417
420
|
have_data = True
|
418
421
|
|
419
|
-
# 输出结果
|
420
|
-
if match_time is None:
|
421
|
-
print(f"[bold red]{time_input_str} is in the following dataset and version:")
|
422
422
|
if have_data:
|
423
423
|
if match_time is None:
|
424
|
+
print(f"[bold red]Time {time_input_str} included in:")
|
425
|
+
dv_num = 1
|
424
426
|
for d, v, trange in zip(d_list, v_list, trange_list):
|
425
|
-
print(f"[bold blue]{d} {v} {trange}")
|
427
|
+
print(f"{dv_num} -> [bold blue]{d} - {v} : {trange}")
|
428
|
+
dv_num += 1
|
426
429
|
if is_single_time:
|
427
430
|
return True
|
428
431
|
else:
|
@@ -434,7 +437,7 @@ def _check_time_in_dataset_and_version(time_input, time_end=None):
|
|
434
437
|
print(f"[bold red]{time_start} to {time_end} is in different datasets or versions, so you can't download them together")
|
435
438
|
return False
|
436
439
|
else:
|
437
|
-
print(f"[bold red]{time_input_str}
|
440
|
+
print(f"[bold red]Time {time_input_str} has no data")
|
438
441
|
return False
|
439
442
|
|
440
443
|
|
@@ -509,7 +512,8 @@ def _direct_choose_dataset_and_version(time_input, time_end=None):
|
|
509
512
|
|
510
513
|
if dataset_name_out is not None and version_name_out is not None:
|
511
514
|
if match_time is None:
|
512
|
-
print(f"[bold purple]dataset: {dataset_name_out}, version: {version_name_out} is chosen")
|
515
|
+
# print(f"[bold purple]dataset: {dataset_name_out}, version: {version_name_out} is chosen")
|
516
|
+
print(f"[bold purple]Chosen dataset: {dataset_name_out} - {version_name_out}")
|
513
517
|
|
514
518
|
# 如果没有找到匹配的数据集和版本,会返回 None
|
515
519
|
return dataset_name_out, version_name_out
|
@@ -664,117 +668,122 @@ def _correct_time(nc_file):
|
|
664
668
|
modify_nc(nc_file, "time", None, time_difference)
|
665
669
|
|
666
670
|
|
671
|
+
class _HycomDownloader:
|
672
|
+
def __init__(self, tasks, delay_range=(3, 6), timeout_factor=120, max_var_count=5, max_retries=3):
|
673
|
+
"""
|
674
|
+
:param tasks: List of (url, save_path)
|
675
|
+
"""
|
676
|
+
self.tasks = tasks
|
677
|
+
self.delay_range = delay_range
|
678
|
+
self.timeout_factor = timeout_factor
|
679
|
+
self.max_var_count = max_var_count
|
680
|
+
self.max_retries = max_retries
|
681
|
+
self.count = {"success": 0, "fail": 0}
|
682
|
+
|
683
|
+
def user_agent(self):
|
684
|
+
return get_ua()
|
685
|
+
|
686
|
+
async def _download_one(self, url, save_path):
|
687
|
+
file_name = os.path.basename(save_path)
|
688
|
+
headers = {"User-Agent": self.user_agent()}
|
689
|
+
var_count = min(max(url.count("var="), 1), self.max_var_count)
|
690
|
+
timeout_max = self.timeout_factor * var_count
|
691
|
+
timeout = random.randint(timeout_max // 2, timeout_max)
|
692
|
+
|
693
|
+
retry = 0
|
694
|
+
while retry <= self.max_retries:
|
695
|
+
try:
|
696
|
+
await asyncio.sleep(random.uniform(*self.delay_range))
|
697
|
+
start = datetime.datetime.now()
|
698
|
+
async with httpx.AsyncClient(
|
699
|
+
timeout=httpx.Timeout(timeout),
|
700
|
+
limits=httpx.Limits(max_connections=2, max_keepalive_connections=2),
|
701
|
+
transport=httpx.AsyncHTTPTransport(retries=2),
|
702
|
+
) as client:
|
703
|
+
print(f"[bold #f0f6d0]Requesting {file_name} (Attempt {retry + 1}) ...")
|
704
|
+
response = await client.get(url, headers=headers, follow_redirects=True)
|
705
|
+
response.raise_for_status()
|
706
|
+
if not response.content:
|
707
|
+
raise ValueError("Empty response received")
|
708
|
+
|
709
|
+
print(f"[bold #96cbd7]Downloading {file_name} ...")
|
710
|
+
with open(save_path, "wb") as f:
|
711
|
+
async for chunk in response.aiter_bytes(32 * 1024):
|
712
|
+
f.write(chunk)
|
713
|
+
|
714
|
+
elapsed = datetime.datetime.now() - start
|
715
|
+
print(f"[#3dfc40]File [bold #dfff73]{file_name} [#3dfc40]downloaded, Time: [#39cbdd]{elapsed}")
|
716
|
+
self.count["success"] += 1
|
717
|
+
count_dict["success"] += 1
|
718
|
+
return
|
719
|
+
|
720
|
+
except Exception as e:
|
721
|
+
print(f"[bold red]Failed ({type(e).__name__}): {e}")
|
722
|
+
if retry < self.max_retries:
|
723
|
+
backoff = 2**retry
|
724
|
+
print(f"[yellow]Retrying in {backoff:.1f}s ...")
|
725
|
+
await asyncio.sleep(backoff)
|
726
|
+
retry += 1
|
727
|
+
else:
|
728
|
+
print(f"[red]Giving up on {file_name}")
|
729
|
+
self.count["fail"] += 1
|
730
|
+
count_dict["fail"] += 1
|
731
|
+
return
|
732
|
+
|
733
|
+
async def run(self):
|
734
|
+
print(f"📥 Starting download of {len(self.tasks)} files ...")
|
735
|
+
for url, save_path in self.tasks:
|
736
|
+
await self._download_one(url, save_path)
|
737
|
+
|
738
|
+
print("\n✅ All tasks completed.")
|
739
|
+
print(f"✔️ Success: {self.count['success']} | ❌ Fail: {self.count['fail']}")
|
740
|
+
|
741
|
+
|
667
742
|
def _download_file(target_url, store_path, file_name, cover=False):
|
668
|
-
|
743
|
+
save_path = Path(store_path) / file_name
|
669
744
|
file_name_split = file_name.split("_")
|
670
745
|
file_name_split = file_name_split[:-1]
|
671
746
|
same_file = "_".join(file_name_split) + "*nc"
|
672
747
|
|
673
748
|
if match_time is not None:
|
674
|
-
if check_nc(
|
675
|
-
if not _check_ftime(
|
749
|
+
if check_nc(save_path, print_messages=False):
|
750
|
+
if not _check_ftime(save_path, if_print=True):
|
676
751
|
if match_time:
|
677
|
-
_correct_time(
|
752
|
+
_correct_time(save_path)
|
678
753
|
count_dict["skip"] += 1
|
679
754
|
else:
|
680
|
-
_clear_existing_file(
|
755
|
+
_clear_existing_file(save_path)
|
681
756
|
count_dict["no_data"] += 1
|
682
757
|
else:
|
683
758
|
count_dict["skip"] += 1
|
684
759
|
print(f"[bold green]{file_name} is correct")
|
685
760
|
return
|
686
761
|
|
687
|
-
if not cover and os.path.exists(
|
688
|
-
print(f"[bold #FFA54F]{
|
762
|
+
if not cover and os.path.exists(save_path):
|
763
|
+
print(f"[bold #FFA54F]{save_path} exists, skipping ...")
|
689
764
|
count_dict["skip"] += 1
|
690
765
|
return
|
691
766
|
|
692
767
|
if same_file not in fsize_dict.keys():
|
693
|
-
check_nc(
|
768
|
+
check_nc(save_path, delete_if_invalid=True, print_messages=False)
|
694
769
|
|
695
|
-
get_mean_size = _get_mean_size_move(same_file,
|
770
|
+
get_mean_size = _get_mean_size_move(same_file, save_path)
|
696
771
|
|
697
|
-
if _check_existing_file(
|
772
|
+
if _check_existing_file(save_path, get_mean_size):
|
698
773
|
count_dict["skip"] += 1
|
699
774
|
return
|
700
775
|
|
701
|
-
_clear_existing_file(
|
776
|
+
_clear_existing_file(save_path)
|
702
777
|
|
703
778
|
if not use_idm:
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
request_times = 0
|
708
|
-
|
709
|
-
def calculate_wait_time(time_str, target_url):
|
710
|
-
time_pattern = r"\d{10}"
|
711
|
-
times_in_str = re.findall(time_pattern, time_str)
|
712
|
-
num_times_str = len(times_in_str)
|
713
|
-
|
714
|
-
if num_times_str > 1:
|
715
|
-
delta_t = datetime.datetime.strptime(times_in_str[1], "%Y%m%d%H") - datetime.datetime.strptime(times_in_str[0], "%Y%m%d%H")
|
716
|
-
delta_t = delta_t.total_seconds() / 3600
|
717
|
-
delta_t = delta_t / 3 + 1
|
718
|
-
else:
|
719
|
-
delta_t = 1
|
720
|
-
num_var = int(target_url.count("var="))
|
721
|
-
if num_var <= 0:
|
722
|
-
num_var = 1
|
723
|
-
return int(delta_t * 5 * 60 * num_var)
|
724
|
-
|
725
|
-
max_timeout = calculate_wait_time(file_name, target_url)
|
726
|
-
print(f"[bold #912dbc]Max timeout: {max_timeout} seconds")
|
727
|
-
|
728
|
-
download_time_s = datetime.datetime.now()
|
729
|
-
order_list = ["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"]
|
730
|
-
while not download_success:
|
731
|
-
if request_times >= 10:
|
732
|
-
print(f"[bold #ffe5c0]Download failed after {request_times} times\nYou can skip it and try again later")
|
733
|
-
count_dict["fail"] += 1
|
734
|
-
break
|
735
|
-
if request_times > 0:
|
736
|
-
print(f"[bold #ffe5c0]Retrying the {order_list[request_times - 1]} time...")
|
737
|
-
try:
|
738
|
-
referer_center = target_url.split("?")[0].split("ncss/")[-1]
|
739
|
-
headers = {
|
740
|
-
"User-Agent": get_ua(), # 后面几项可以不加,依旧能下载
|
741
|
-
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
742
|
-
"Accept-Encoding": "gzip, deflate, br, zstd",
|
743
|
-
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
744
|
-
"Referer": rf"https://ncss.hycom.org/thredds/ncss/grid/{referer_center}/dataset.html",
|
745
|
-
}
|
746
|
-
response = s.get(target_url, headers=headers, stream=True, timeout=random.randint(5, max_timeout))
|
747
|
-
response.raise_for_status()
|
748
|
-
with open(fname, "wb") as f:
|
749
|
-
print(f"[bold #96cbd7]Downloading {file_name} ...")
|
750
|
-
for chunk in response.iter_content(chunk_size=1024):
|
751
|
-
if chunk:
|
752
|
-
f.write(chunk)
|
753
|
-
|
754
|
-
f.close()
|
755
|
-
|
756
|
-
if os.path.exists(fname):
|
757
|
-
download_success = True
|
758
|
-
download_time_e = datetime.datetime.now()
|
759
|
-
download_delta = download_time_e - download_time_s
|
760
|
-
print(f"[#3dfc40]File [bold #dfff73]{fname} [#3dfc40]has been downloaded successfully, Time: [#39cbdd]{download_delta}")
|
761
|
-
count_dict["success"] += 1
|
762
|
-
|
763
|
-
except requests.exceptions.HTTPError as errh:
|
764
|
-
print(f"Http Error: {errh}")
|
765
|
-
except requests.exceptions.ConnectionError as errc:
|
766
|
-
print(f"Error Connecting: {errc}")
|
767
|
-
except requests.exceptions.Timeout as errt:
|
768
|
-
print(f"Timeout Error: {errt}")
|
769
|
-
except requests.exceptions.RequestException as err:
|
770
|
-
print(f"OOps: Something Else: {err}")
|
771
|
-
|
772
|
-
time.sleep(3)
|
773
|
-
request_times += 1
|
779
|
+
python_downloader = _HycomDownloader([(target_url, save_path)])
|
780
|
+
asyncio.run(python_downloader.run())
|
781
|
+
time.sleep(3 + random.uniform(0, 10))
|
774
782
|
else:
|
775
783
|
idm_downloader(target_url, store_path, file_name, given_idm_engine)
|
776
|
-
idm_download_list.append(
|
777
|
-
print(f"[bold #3dfc40]File [bold #dfff73]{
|
784
|
+
idm_download_list.append(save_path)
|
785
|
+
# print(f"[bold #3dfc40]File [bold #dfff73]{save_path} [#3dfc40]has been submit to IDM for downloading")
|
786
|
+
time.sleep(3 + random.uniform(0, 10))
|
778
787
|
|
779
788
|
|
780
789
|
def _check_hour_is_valid(ymdh_str):
|
@@ -1167,20 +1176,20 @@ if __name__ == "__main__":
|
|
1167
1176
|
|
1168
1177
|
options = {
|
1169
1178
|
"variables": var_list,
|
1170
|
-
"start_time": "
|
1171
|
-
"end_time": "
|
1172
|
-
"output_dir": r"
|
1179
|
+
"start_time": "2018010100",
|
1180
|
+
"end_time": "2019063000",
|
1181
|
+
"output_dir": r"G:\Data\HYCOM\china_sea\hourly_24",
|
1173
1182
|
"lon_min": 105,
|
1174
|
-
"lon_max":
|
1175
|
-
"lat_min":
|
1183
|
+
"lon_max": 135,
|
1184
|
+
"lat_min": 10,
|
1176
1185
|
"lat_max": 45,
|
1177
1186
|
"workers": 1,
|
1178
1187
|
"overwrite": False,
|
1179
1188
|
"depth": None,
|
1180
1189
|
"level": None,
|
1181
|
-
"validate_time":
|
1182
|
-
"idm_path": r"D:\Programs\Internet Download Manager\IDMan.exe",
|
1183
|
-
"interval_hours":
|
1190
|
+
"validate_time": None,
|
1191
|
+
# "idm_path": r"D:\Programs\Internet Download Manager\IDMan.exe",
|
1192
|
+
"interval_hours": 24,
|
1184
1193
|
}
|
1185
1194
|
|
1186
1195
|
if single_var:
|
@@ -31,8 +31,6 @@ oafuncs/_script/replace_file_content.py
|
|
31
31
|
oafuncs/oa_down/User_Agent-list.txt
|
32
32
|
oafuncs/oa_down/__init__.py
|
33
33
|
oafuncs/oa_down/hycom_3hourly.py
|
34
|
-
oafuncs/oa_down/hycom_3hourly_20250407.py
|
35
|
-
oafuncs/oa_down/hycom_3hourly_20250416.py
|
36
34
|
oafuncs/oa_down/idm.py
|
37
35
|
oafuncs/oa_down/literature.py
|
38
36
|
oafuncs/oa_down/test_ua.py
|
@@ -18,7 +18,7 @@ URL = "https://github.com/Industry-Pays/OAFuncs"
|
|
18
18
|
EMAIL = "liukun0312@stu.ouc.edu.cn"
|
19
19
|
AUTHOR = "Kun Liu"
|
20
20
|
REQUIRES_PYTHON = ">=3.9.0" # 2025/03/13
|
21
|
-
VERSION = "0.0.98.
|
21
|
+
VERSION = "0.0.98.5"
|
22
22
|
|
23
23
|
# What packages are required for this module to be executed?
|
24
24
|
REQUIRED = [
|